Après avoir vu dans l'article précédent comment créer un Stream, nous allons maintenant aborder comment les manipuler.
Grossièrement, le Stream permet de parcourir et traiter un ensemble d'éléments (filtre, mapping, ...). Cependant, il est intéressant avant toute chose, d'expliciter quelques bases sur le Stream :
Création de quelques classes utiles pour les futurs exemples. La classe Department représente le département et Person représente une personne. Un département a un id, un nom et une liste de personnes. Une personne a un id, un nom, un prénom et un âge.
import java.util.List;
class Department {
private long id;
private String name;
private List persons;
public Department(long id, String name, List persons) {
super();
this.id = id;
this.name = name;
this.persons = persons;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public List getPersons() {
return persons;
}
}
class Person {
private long id;
private String firstName;
private String lastName;
private int age;
public Person(long id, String firstName, String lastName, int age) {
super();
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public long getId() {
return id;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public int getAge() {
return age;
}
}
Le parcours d'un Stream peut s'effectuer via la méthode forEach.
Définition de la méthode forEach dans java.util.stream.Stream :
default void forEach(Consumer super T> action);
Exemple d'utilisation :
List firstName = Arrays.asList("Alex", "Claire", "Angel");
firstName.forEach(System.out::println); // Référence de méthode "::"
firstName.forEach(x -> System.out.println(x)); // Lambda expression
/* Output x 2 :
Alex
Claire
Angel */
Le mapping est la transformation d'un objet de type T en objet de type R. Ici, la méthode Stream.map permet de transformer un Stream
Stream map(Function super T, ? extends R> mapper);
Grâce au mapping et aux références des fonctions ( :: ), nous pouvons facilement récupérer un objet contenu dans un autre. Ici nous récupérons la valeur firstName contenu dans la liste d'objet Person.
List persons = Arrays.asList(new Person(0,"Alex", "Brown", 30), new Person(1, "Claire", "Jones", 29));
List firstNames = persons.stream()
.map(Person::getFirstName) // ou .map(person -> person.getFirstName())
.collect(Collectors.toList());
La méthode map sert également à réaliser des transformations d'objets. Ici, nous allons transformer une liste de String en majuscule.
List firstName = Arrays.asList("Alex", "Claire", "Angel");
List firstNameUpperCase = firstName.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(firstNameUpperCase); // Output: [ALEX, CLAIRE, ANGEL]
Les Stream permettent de réaliser des mapping avec des types primitifs int, long et double nativement. Définition des méthodes de mapping primitifs dans java.util.stream.Stream :
IntStream mapToInt(ToIntFunction super T> mapper);
LongStream mapToLong(ToLongFunction super T> mapper);
DoubleStream mapToDouble(ToDoubleFunction super T> mapper);
Exemple d'utilisation :
List persons = Arrays.asList(new Person(0, "Alex", "Brown", 30), new Person(1, "Claire", "Jones", 29));
Long maxAge = persons.stream().mapToLong(Person::getAge).max().orElse(0);
System.out.println(maxAge); // Output: 30
Attention, pour transformer un LongStream (IntStream ou DoubleStream) en List, il est nécessaire au préalable d'utiliser la méthode boxed() qui converti l'objet primitif en sa version objet Stream
List persons = Arrays.asList(new Person(0, "Alex", "Brown", 30), new Person(1, "Claire", "Jones", 29));
List ages = persons.stream().mapToInt(Person::getAge).boxed().collect(Collectors.toList());
System.out.println(ages); // Output: [30, 29]
Un des intérêt d'utiliser ces Stream est qu'ils implémentent déjà plusieurs méthodes comme max / min par exemple.
La méthode Stream.flatMap permet d'aplatir un Stream :
Stream flatMap(Function super T, ? extends Stream extends R>> mapper);
Exemples d'utilisation :
List myList1 = Arrays.asList("Alex", "Claire", "Angel");
List myList2 = Arrays.asList("John", "Miller", "Brown");
// Liste de liste
List> myListOfList = Arrays.asList(myList1, myList2);
List myResult = myListOfList.stream().flatMap(List::stream).collect(Collectors.toList());
System.out.println(myResult); // Output: [Alex, Claire, Angel, John, Miller, Brown]
// Optional.OfNullable (Java 9)
List myResult2 = Optional.ofNullable(myList1).stream().flatMap(List::stream).collect(Collectors.toList());
System.out.println(myResult2); // Output: [Alex, Claire, Angel]
// Stream.OfNullable (Java 9)
List myResult3 = Stream.ofNullable(myList1).flatMap(List::stream).collect(Collectors.toList());
System.out.println(myResult3); // Output: [Alex, Claire, Angel]
Afin d'agréger un Stream à un seul élément en appliquant une fonction, la méthode Stream.reduce peut être appelée. Le reduce est un traitement terminal, c'est à dire qu'il va déclencher l'exécution du Stream. Les méthode min et max par exemple appellent cette méthode.
Définitions des méthodes reduce :
T reduce(T identity, BinaryOperator accumulator);
Optional reduce(BinaryOperator accumulator);
U reduce(U identity, BiFunction accumulator, BinaryOperator combiner);
Exemples d'utilisation :
Integer maxAge = persons.stream().reduce((p1, p2) -> p1.getAge() > p2.getAge() ? p1 : p2).map(Person::getAge).orElse(0);
System.out.println(maxAge); // Output: 30
System.out.println(IntStream.range(0, 5).reduce((x, y) -> x + y)); // Output: OptionalInt[10] <= (1+2+3+4)
System.out.println(IntStream.range(0, 5).reduce(5, (x, y) -> x + y)); // Output: 15 <= 5 + (1+2+3+4)
System.out.println(Stream.of(1, 2, 3, 4).reduce(5, (a, b) -> a + b, (a, b) -> { // combiner not called, only in parralele. Output: 15 <= 5 + (1+2+3+4)
return a + b;
}));
System.out.println(Stream.of(1, 2, 3, 4).parallel().reduce(5, (a, b) -> a + b, (a, b) -> { // Combiner B Combiner B. Output: 30 <= (((5+1) + (5+2)) + ((5+3) + (5+4)))
return a + b;
}));
Attention : La fonction de combinaison appelée en mode parallèle peut facilement induire en erreur (voir les sommes ci-dessus : 30 et non 15) à cause des étapes de combinaisons.
Stream :
Méthode | Description |
min | Retourne l'élément minimale du stream. Comparateur à fournir |
max | Retourne l'élément maximale du stream. Comparateur à fournir |
count | Retourne le nombre d'éléments du stream |
collect | Renvoie un conteneur mutable contenant la transformation du stream contenant le résultat des traitements |
toArray | Renvoie un tableau contenant la transformation du stream contenant le résultat des traitements |
Types Stream primitifs (IntStream ...) :
Méthode | Description |
min | Retourne l'élément minimal du stream. |
max | Retourne l'élément maximal du stream. |
count | Retourne le nombre d'élément du stream |
sum | Somme |
average | Moyenne |
summaryStatistics | Retourne un |
collect | Renvoie un conteneur mutable contenant la transformation du stream contenant le résultat des traitements |
toArray | Renvoie un tableau contenant la transformation du stream contenant le résultat des traitements |
La méthode collect permet la transformation d'un Stream dans un conteneur (Collection, List, Set, Map) mutable (modifiable) contenant les traitements de réduction (reduce). Ses définitions sont les suivantes :
R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner);
R collect(Collector super T, A, R> collector);
Stream myStream = Stream.of("Alex", "Claire", "Angel");
List myList = myStream.map(String::toUpperCase).collect(Collectors.toList()); // Java >= 8
List myList = myStream.map(String::toUpperCase).toList(); // Java >= 16
La méthode Collectors.joining concatène le contenu du Stream. Ses définitions sont les suivantes :
public static Collector joining();
public static Collector joining(CharSequence delimiter);
public static Collector joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix);
Exemple d'utilisation :
Stream myStream = Stream.of("Alex", "Claire", "Angel");
String myString = myStream.map(String::toUpperCase).collect(Collectors.joining(",", "{", "}"));
System.out.println(myString); // Output: {ALEX,CLAIRE,ANGEL}
La classe Collectors offre pour les trois types primitifs int, long et double des méthodes pour sommer (Collectors.summarizingXXX), réaliser des moyennes (Collectors.averagingXXX) et obtenir des statistiques (Collectors.summarizingXXX) (XXX étant soit Int, Long ou Double).
Exemples d'utilisation :
// init
List persons = Arrays.asList(new Person(0, "Alex", "Brown", 30), new Person(1, "Claire", "Jones", 29));
// somme
System.out.println(persons.stream().collect(Collectors.summingInt(Person::getAge))); // Output: 59
// moyenne
System.out.println(persons.stream().collect(Collectors.averagingInt(Person::getAge))); // Output: 29.5
// stat
System.out.println(persons.stream().collect(Collectors.summarizingInt(Person::getAge))); // Output: IntSummaryStatistics{count=2, sum=59, min=29, average=29,500000, max=30}
Comme en SQL, il est possible de réaliser des regroupements sur les Stream. Ils sont réalisés via méthode Collectors.groupingBy.
Exemples d'utilisations :
// group by age
// Map result : {29=[tools.Person@4eec7777], 30=[tools.Person@3b07d329, tools.Person@41629346, tools.Person@404b9385, tools.Person@6d311334]}
Map> myList1 = persons.stream().collect(Collectors.groupingBy(Person::getAge));
Map> myList1_2 = persons.stream().collect(Collectors.groupingBy(Person::getAge, Collectors.toSet())); // Pareil pour avoir une liste
// group by age and firstName
// Map result : {29={Claire=[tools.Person@4eec7777]}, 30={Alex=[tools.Person@3b07d329], Angel=[tools.Person@41629346, tools.Person@404b9385, tools.Person@6d311334]}}
Map>> myList2 = persons.stream().collect(Collectors.groupingBy(Person::getAge, Collectors.groupingBy(Person::getFirstName)));
// group by age and gettint the average
// Map result : {29=29.0, 30=30.0}
Map myList3 = persons.stream().collect(Collectors.groupingBy(Person::getAge, Collectors.averagingInt(Person::getAge)));
Il est également possible de récupérer certains éléments issus de la comparaison :
// Init
List persons2 = Arrays.asList(new Person(0, "Alex", "Brown", 30),
new Person(0, "Claire", "Jones", 29),
new Person(1, "Angel", "Mike", 30),
new Person(1, "Angel", "Mike1", 31));
// Récupération de la personne ayant l'age maximale regroupé par ID
Map> myList4 = persons2.stream().collect(Collectors.groupingBy(Person::getId, Collectors.maxBy(Comparator.comparingInt(Person::getAge))));
myList4.entrySet().stream()
.forEach(entry -> System.out.println(entry.getKey() + " : " + entry.getValue().get().getAge()));
/**
* Output:
* 0 : 30
* 1 : 31
*/
Les filtres sont effectués via la méthode Stream.filter qui a la définition suivante :
Stream filter(Predicate super T> predicate);
Exemple d'utilisation :
// Initialisation
Department departement1 = new Department(13, "PACA", Arrays.asList(new Person(0, "Alex", "Brown", 30), new Person(1, "Claire", "Jones", 29)));
Department departement2 = new Department(03, "Alpes-de-Haute-Provence", Arrays.asList(new Person(0, "Angel", "Davies", 30), new Person(1, "Wilson", "Miller", 60)));
List departements = Arrays.asList(departement1, departement2);
// Filtre des personne de moins de 40 ans
List yongFirstNames = departements.stream().map(Department::getPersons).flatMap(List::stream)
.filter(person -> person.getAge() < 40).map(Person::getFirstName).collect(Collectors.toList());
// Affichage
System.out.println("FirstName de toutes les personnes agées de moins de 40 ans : " + yongFirstNames);
Pour paralléliser un Stream, Il est nécessaire d'utiliser la méthode Collection.parallelStream ou parallel.
Exemples d'utilisations :
// Creer directement un stream parallele
List firstName = Arrays.asList("Alex", "Claire", "Angel");
List firstNameUpperCase = firstName.parallelStream().map(String::toUpperCase).collect(Collectors.toList());
System.out.println(firstNameUpperCase);
// Output: [ALEX, CLAIRE, ANGEL]
// Modifier un stream pour qu'il devienne parallele
List firstName = Arrays.asList("Alex", "Claire", "Angel");
List firstNameUpperCase = firstName.stream().parallel().map(String::toUpperCase).collect(Collectors.toList());
System.out.println(firstNameUpperCase);
// Output: [ALEX, CLAIRE, ANGEL]
// Parallele pour un type primitif
List firstNameUpperCase = IntStream.range(0, 5).parallel().map(x -> x + 1).boxed().collect(Collectors.toList());
System.out.println(firstNameUpperCase);
// Output: [1, 2, 3, 4, 5]
// Parallele vers sequentiel
IntStream myStream = IntStream.range(0, 5).parallel();
System.out.println(myStream.isParallel()); // true
myStream = myStream.sequential();
System.out.println(myStream.isParallel()); // false
L'API Stream est élégante et permet de gagner du temps dans l'écriture d'un programme. De plus, lorsqu'ils sont correctement utilisés, les Stream rendent la lecture du code plus claire. Cependant, cela est à double tranchant. Ils peuvent aussi le rendre illisible (surtout pour trouver l'origine d'une anomalie) si l'API n'est pas appliquée correctement.