Archives Wild Code School
Java Avancé

Stream 02 : Map

Quête 15 sur 15

Comme tu as pu le découvrir précédemment avec filter, les Stream facilitent la vie lorsqu'il s'agit de travailler avec des collections.

Imagine que tu aies le besoin suivant : tu possèdes une List<Person>, avec pour chaque élément des attributs firstName et lastName. Tu aimerais récupérer une nouvelle List<String> avec les valeurs de firstname et lastname concaténées.

shell
Person:

* firstName:  "Jim"
* lastName:   "Power"

String: "Jim Power"

Tu vas avoir besoin de parcourir chaque élément de la liste d'origine, appliquer une même opération à chaque élément, et ajouter le résultat à une nouvelle liste.

Tu vas découvrir dans cette quête comment réaliser ce type d'opérations avec la méthode map, appliquée à un Stream.

Objectifs

  • Transformer une liste d'objets avec map
  • Utiliser l'interface fonctionnelle Function

Map

Avant de voir comment utiliser les Stream pour transformer les éléments d'une liste, regarde d'abord comment procéder avec tes connaissances actuelles. Partons d'une liste de personnes :

java
List<Person> list = new ArrayList<>(Arrays.asList(
    new Person("Kari", "Parmentier"),
    new Person("Gérard", "Laurent"),
    new Person("Apolline", "Truchon")
));

Tu veux concaténer les attributs de chaque Person. Il va donc falloir parcourir la liste :

01. Itération

java
List<String> listMapped = new ArrayList<>();

for (Person item : list) {
    listMapped.add(item.getFirstName() + " " + item.getLastName());
}

Si tu affiches le contenu de la nouvelle liste transformée, tu obtiens le résultat suivant :

java
System.out.println(listMapped.toString());

[Kari Parmentier, Gérard Laurent, Apolline Truchon]

02. Créer une méthode

Maintenant, tu peux isoler le code te permettant de transformer les éléments dans une méthode, de la façon suivante :

java
private static List<String> map(List<Person> list) {
    List<String> listMapped = new ArrayList<>();
    for (Person item : list) {
        listMapped.add(item.getFirstName() + " " + item.getLastName());
    }
    return listMapped;
}

Tu pourras désormais transformer n'importe quelle liste de personnes de la façon suivante :

java
List<String> listMapped = map(list);

Function

Dans le cas que tu as vu précédemment, la transformation est contenue dans le parcours de la boucle, et donc dans la méthode map. C'est la ligne :

java
item.getFirstName() + " " + item.getLastName()

Imaginons que tu veuilles améliorer ta méthode afin de passer en paramètre la transformation à effectuer : tu pourrais, par exemple, utiliser la même méthode map afin de ne récupérer que le nom et le mettre en majuscules.

Il va te falloir appliquer une fonction de transformation sur chacun des éléments. En Java, on résout cette problématique en créant une interface.

01. Créer son interface de transformation

Ton interface va concaténer les valeurs des attributs firstName et lastName d'une instance de Person. Pour cet exemple, elle va s'appeler Concat.

Cette interface ne va avoir qu'une signature de méthode : apply (qui appliquera la transformation).

java
private interface Concat {

    String apply(Person item);
}

C'est une interface, on ne renseigne donc pas de corps à la méthode. C'est logique, la transformation à appliquer dépendra de la liste : comme dit plus haut, on pourrait vouloir ne garder que le nom, ou que le prénom, ou transformer l'un d'entre eux en majuscules.

Il va te falloir modifier ta méthode map afin d'accepter en argument une instance de l'interface Concat :

java
private static List<String> map(List<Person> list, Concat concat) {
    List<String> listMapped = new ArrayList<>();
    for (Person item : list) {
        listMapped.add(concat.apply(item));
    }
    return listMapped;
}

Afin d'appeler la méthode map, tu vas créer une classe anonyme, qui implémente une interface et en crée une instance en même temps.

Tu peux alors passer la classe anonyme en paramètre de la méthode map :

java
List<String> listMapped = map(list, new Concat() {
    @Override
    public String apply(Person item) {
        return item.getFirstName() + " " + item.getLastName();
    }
});

02. Interface fonctionnelle

Tu as vu dans la quête sur filter que, depuis Java 8, il est possible d'utiliser un certain nombre d'interfaces fonctionnelles déjà existantes. Tu sais déjà utiliser Predicate, qui retourne une valeur booléenne selon une condition de filtrage.

Dans le cas de la transformation, tu vas avoir besoin du type d'éléments à transformer et du type de l'élément obtenu après la transformation. Ces deux types peuvent différer : dans notre exemple, on part d'une Person pour obtenir une String.

Tu vas donc avoir besoin de l'interface fonctionnelle Function :

java
@FunctionalInterface
public interface Function<T, R> {
    R apply(T var1);

    // few other default methods here
}

Ici T et R représentent des classes génériques (qui peuvent être n'importe quel objet)

  • T est le type de l'élément à transformer

  • R est le type obtenu après transformation

Comme tu peux le remarquer, elle est presque identique à l'interface Concat créée précédemment. Tu peux donc créer une classe anonyme à sa place, de la façon suivante :

java
List<String> listMapped = map(list, new Function<Person, String>() {
    @Override
    public String apply(Person item) {
        return item.getFirstName() + " " + item.getLastName();
    }
});

Cependant, comme ta méthode map s'attend à recevoir un objet de type Concat, il va aussi falloir modifier l'argument par une Function :

java
private static List<String> map(List<Person> list, Function<Person, String> concat) {
    List<String> listMapped = new ArrayList<>();
    for (Person item : list) {
        listMapped.add(concat.apply(item));
    }
    return listMapped;
}

03. Lambda

Comme vu dans la quête précédente, tu peux utiliser des lambdas lors de l'appel d'une interface fonctionnelle.

Voici l'actuel appel de ta Function :

java
new Function<Person, String>() {
    @Override
    public String apply(Person item) {
        return item.getFirstName() + " " + item.getLastName()
    }
}

Rappelle-toi, une Lambda se présente sous la forme suivante :

java
(arguments, ...) -> {
    // body
}

Ici, tu peux donc modifier le code de la façon suivante :

java
(item) -> {
    return item.getFirstName() + " " + item.getLastName();
}

Il est possible de simplifier encore plus ! Pour rappel, si une méthode ne possède qu'un seul argument, les lambdas permettent d'enlever les parenthèses. De plus, si le corps de la méthode ne contient que le retour d'une expression, il t'est possible d'enlever les accolades et le mot-clé return (le retour sera fait de manière implicite) :

java
item -> item.getFirstName() + " " + item.getLastName()

Il t'est alors possible de simplifier l'appel de map afin d'arriver au code suivant :

java
List<String> listMapped = map(list, item -> item.getFirstName() + " " + item.getLastName());

↗
Oracle : Interface Function<T,R>
docs.oracle.com

Stream

L'étape précédente t'a montré comment on faisait, avant Java 8, pour transformer les éléments d'une liste, ainsi que l'utilisation d'une interface fonctionnelle et des lambdas.

Maintenant, tu vas utiliser la méthode map de Stream pour simplifier tout ça !

Rappelle-toi, l'utilisation d'un Stream comporte trois étapes :

  • Source : on crée un flux (stream), par exemple à partir d'une liste d'éléments, d'un tableau...

  • Intermediate : on enchaîne une ou plusieurs opérations, par exemple filtrer, trier...

  • Terminal : on regroupe les résultats, par exemple on les collecte sous forme de liste, on en fait une somme...

Dans le cas qui nous intéresse pour cette quête, nous allons voir un exemple avec l'étape intermédiaire map.

01. Source : List

Tu le sais déjà, il suffit d'appeler la méthode stream sur la liste :

java
list.stream()

Ici list est un nom de variable, il faudra le modifier par ta liste !

02. Intermediate : Map

Tu vas appliquer ta transformation avec un appel de la méthode map : elle attend en argument une Function, contenant la transformation à effectuer.

java
list.stream()
        .map(item -> item.getFirstName() + " " + item.getLastName())

03. Terminal : Collect

Le simple fait de filtrer n'est pas suffisant : en utilisant un Stream, la liste d'origine ne sera pas modifiée. Il faut collecter le résultat.

Cela se fait avec l'utilisation de la méthode collect, en lui précisant qu'on veut un résultat sous forme de List. Il faut alors stocker le retour dans une nouvelle liste :

java
List<String> listMapped = list.stream()
        .map(item -> item.getFirstName() + " " + item.getLastName())
        .collect(Collectors.toList());

*Important : * tu trouveras en ressource de cette section le code de toutes les étapes vues dans la quête. Les noms des classes vont de 1 à 6, progressant de la méthode "à l'ancienne" jusqu'à l'utilisation des Stream et lambdas.

↗
Dépôt contenant tous les exemples de la quête
github.com
↗
GeeksforGeeks : Stream map in Java with examples
geeksforgeeks.org
↗
Mkyong : Java 8 Streams map examples
mkyong.com

Challenge

Fichage des agents

Ta mission précédente (et surtout les donuts offerts à la dernière réunion) t'ont rendu populaire au S.H.I.E.L.D... Mais pas assez pour t'éviter les basses besognes qui reviennent à un bleu tel que toi !

L'accueil est muni d'un système de reconnaissance vocale, qui permet à chaque agent de s'identifier. Le système reconnaît les noms et prénoms des agents, puis les stockent dans une chaîne de caractères.

Cependant, la base de données des agents enregistre ces derniers dans des instances de type Agent, qui possèdent des attributs firstName et lastName.

Ta mission est de transformer les chaînes de caractères fournies en instances d'Agent.

Pour démarrer, fais un Fork du dépôt suivant puis clone-le en local.

Pense bien à faire un Fork, sinon tu ne pourras rien pousser !

  1. Ouvre les classes Shield et Agent afin d'en étudier les contenus respectifs
  2. Dans Shield.java, utilise un Stream et la méthode map pour transformer une liste de String en liste d'Agent
  3. Utilise une Function, dans laquelle tu pourras appeler la méthode split sur la chaîne de caractère afin d'en extraire de nom et le prénom : utilise ces derniers pour créer une instance d'Agent et la retourner
  4. Ensuite utilise une lambda
  5. Tu ne dois pas modifier la classe Agent !
  6. Compile et exécute le code, afin de vérifier que les deux listes s'affichent bien dans le terminal

Résultat attendu lors de l'exécution de Shield :

bash
Agents:
Coulson Phil
May Melinda
Johnson Daisy
Fitz Leopold
Simmons Jemma

Critères de validation

  • Le dépôt GitHub contient bien les fichiers Agent.java et Shield.java. Seul Shield.java doit avoir été modifié.
  • La classe Shield contient bien un Stream qui transforme une liste d'Agent, en utilisant la méthode map et une Function
  • La classe Shield se compile sans erreur et affiche dans le terminal le même résultat que celui présenté précédemment