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<Person> persons;
public Department(long id, String name, List<Person> persons) {
super();
this.id = id;
this.name = name;
this.persons = persons;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public List<Person> 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<String> 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<T> en Stream<R>. Définition de la méthode map dans java.util.stream.Stream :
<R> Stream<R> 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<Person> persons = Arrays.asList(new Person(0,"Alex", "Brown", 30), new Person(1, "Claire", "Jones", 29));
List<String> 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<String> firstName = Arrays.asList("Alex", "Claire", "Angel");
List<String> 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<Person> 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<Long>. Exemple d'utilisation :
List<Person> persons = Arrays.asList(new Person(0, "Alex", "Brown", 30), new Person(1, "Claire", "Jones", 29));
List<Integer> 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 :
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
Exemples d'utilisation :
List<String> myList1 = Arrays.asList("Alex", "Claire", "Angel");
List<String> myList2 = Arrays.asList("John", "Miller", "Brown");
// Liste de liste
List<List<String>> myListOfList = Arrays.asList(myList1, myList2);
List<String> 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<String> myResult2 = Optional.ofNullable(myList1).stream().flatMap(List::stream).collect(Collectors.toList());
System.out.println(myResult2); // Output: [Alex, Claire, Angel]
// Stream.OfNullable (Java 9)
List<String> 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<T> accumulator);
Optional<T> reduce(BinaryOperator<T> accumulator);
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> 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 <TypePrimitif>SummaryStatistics encapsulant différentes statistiques calculées 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 |
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> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);
<R, A> R collect(Collector<? super T, A, R> collector);
Stream<String> myStream = Stream.of("Alex", "Claire", "Angel");
List<String> myList = myStream.map(String::toUpperCase).collect(Collectors.toList()); // Java >= 8
List<String> 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<CharSequence, ?, String> joining();
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter);
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix);
Exemple d'utilisation :
Stream<String> 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<Person> 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<Integer, List<Person>> myList1 = persons.stream().collect(Collectors.groupingBy(Person::getAge));
Map<Integer, Set<Person>> 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<Integer, Map<String, List<Person>>> 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<Integer, Double> 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<Person> 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<Long, Optional<Person>> 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<T> 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<Department> departements = Arrays.asList(departement1, departement2);
// Filtre des personne de moins de 40 ans
List<String> 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<String> firstName = Arrays.asList("Alex", "Claire", "Angel");
List<String> 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<String> firstName = Arrays.asList("Alex", "Claire", "Angel");
List<String> firstNameUpperCase = firstName.stream().parallel().map(String::toUpperCase).collect(Collectors.toList());
System.out.println(firstNameUpperCase);
// Output: [ALEX, CLAIRE, ANGEL]
// Parallele pour un type primitif
List<Integer> 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.
LauLem.com - Conditions Générales d'Utilisation - Informations Légales - Charte relative aux cookies - Charte sur la protection des données personnelles - A propos