Dernière modification : 29/08/2022

 Collections et Stream avec Java

Utilisation

 

Après avoir vu dans l'article précédent comment créer un Stream, nous allons maintenant aborder comment les manipuler.

1. Avant de commencer

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 :

  • Il ne stock pas les données car elles sont obtenues "à la volée".
  • Il ne modifie pas les données en entrées (et on ne doit pas modifier les données en entrées sous peine de le perturber)
  • Comme vu dans l'article précédent, il est possible de créer des Stream infini.
  • Le Stream exécute deux types de traitements :
    • intermédiaires : map / filter
    • terminal : sum, max, foreach (reduce)
  • Il s'exécute uniquement quand cela est nécessaire : lors de l'appel aux traitements terminaux (lazyness).
  • Un Stream ne peut être utilisé qu'une seule fois.
  • Les traitements peuvent (non obligatoire) s'exécuter en parallèle.

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;
	}
}

 

2. Parcourir un Stream

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 */

 

3. Le mapping

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);

 

3.1 Récupération d'un objet A d'un objet B

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());

 

3.2 Transformation/manipulation d'un objet

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]

 

3.3 Mapping primitif

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.

 

3.4 Aplatissement de Stream

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]

 

4. Réduction

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.

 

4.1 Les méthodes de réductions utiles

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

 

5. La méthode collect

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);

 

5.1 Convertir un Stream en une Collection, une List ou un Set

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

 

5.2 Convertir un Stream en String

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}

 

5.3 Extraire des somme, des moyennes et des statistiques

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}

 

5.4 Regrouper les données

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
 */

 

6. Les filtres

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);

 

7. Les Stream parallèles

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

 

8. Conclusion

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