Stream 02 : Map
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.
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 :
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
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 :
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 :
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 :
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 :
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).
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 :
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 :
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 :
@FunctionalInterface
public interface Function<T, R> {
R apply(T var1);
// few other default methods here
}
Ici
TetRreprésentent des classes génériques (qui peuvent être n'importe quel objet)
-
Test le type de l'élément à transformer -
Rest 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 :
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 :
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 :
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 :
(arguments, ...) -> {
// body
}
Ici, tu peux donc modifier le code de la façon suivante :
(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) :
item -> item.getFirstName() + " " + item.getLastName()
Il t'est alors possible de simplifier l'appel de map afin d'arriver au code suivant :
List<String> listMapped = map(list, item -> item.getFirstName() + " " + item.getLastName());
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 :
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.
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 :
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
Streamet lambdas.
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 !
- Ouvre les classes
ShieldetAgentafin d'en étudier les contenus respectifs - Dans
Shield.java, utilise unStreamet la méthodemappour transformer une liste deStringen liste d'Agent - Utilise une
Function, dans laquelle tu pourras appeler la méthodesplitsur 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'Agentet la retourner - Ensuite utilise une lambda
- Tu ne dois pas modifier la classe
Agent! - 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 :
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.javaetShield.java. SeulShield.javadoit avoir été modifié. - La classe
Shieldcontient bien unStreamqui transforme une liste d'Agent, en utilisant la méthodemapet uneFunction - La classe
Shieldse compile sans erreur et affiche dans le terminal le même résultat que celui présenté précédemment