Les slides de ma présentation à Devoxx France 2017.
Introduite en Java 8, l'API Collector vit dans l'ombre de l'API Stream, ce qui est logique puisqu'un collecteur doit se connecter à un stream pour fonctionner. Le JDK est organisé de sorte que l'on utilise surtout les collectors sur étagère : groupingBy, counting et quelques autres. Ces deux éléments masquent non seulement le modèle de traitement de données des collectors, mais aussi sa puissance et ses performances.
Ces présentation parle des collectors qui existent et qu'il faut connaître, ceux que l'on peut créer, ceux dont on se doute que l'on peut les créer une fois que l'on comprend un peu les choses, et les autres, tant les possibilités offertes par cette API sont illimitées.
1. #DevoxxFR
L’API Collector
dans tous ses états
@JosePaumard
https://github.com/JosePaumard
https://www.slideshare.net/jpaumard
https://www.youtube.com/user/JPaumard
7. #DevoxxFR #ColJ8
Collectors ?
Pourquoi s’intéresser aux collectors ?
▪ Partie intégrante de l’API Stream
▪ En général un peu laissée de côté
▪ Bien comprise elle peut simplifier les traitements
9. #DevoxxFR #ColJ8
movies.stream()
.collect(
Collectors.groupingBy(
movie -> movie.releaseYear(),
Collector.of(
() -> new HashMap<Actor, AtomicLong>(),
(map, movie) -> {
movie.actors().forEach(
actor -> map.computeIfAbsent(actor, a -> new AtomicLong()).incrementAndGet()
) ;
},
(map1, map2) -> {
map2.entrySet().stream().forEach(
entry -> map1.computeIfAbsent(entry.getKey(), a -> new AtomicLong()).addAndGet(entry.getValue().get())
) ;
return map1 ;
},
new Collector.Characteristics [] {
Collector.Characteristics.CONCURRENT.CONCURRENT
}
)
)
)
.entrySet().stream()
.collect(
Collectors.toMap(
entry5 -> entry5.getKey(),
entry5 -> entry5.getValue()
.entrySet().stream()
.max(Map.Entry.comparingByValue(Comparator.comparing(l -> l.get())))
.get()
)
.entrySet()
.stream()
.max(Comparator.comparing(entry -> entry.getValue().getValue().get()))
.get();
10. #DevoxxFR #ColJ8
Collectors ?
Pourquoi s’intéresser aux collectors ?
▪ Partie intégrante de l’API Stream
▪ En général un peu laissée de côté
▪ Bien comprise elle peut simplifier les traitements
▪ Avec quelques précautions…
12. #DevoxxFR #ColJ8
Dualité dans les Streams
Dans les streams :
▪ Objet qui se connecte à une source
▪ Opérations intermédiaires / terminales
▪ Certaines opérations terminales peuvent être des
collectors
▪ Ces collectors peuvent prendre d’autres
collectors en paramètres…
13. #DevoxxFR #ColJ8
Dualité dans les Streams
Dans les streams :
▪ Toute opération sur un stream peut être
modélisée par un collector
▪ Quel intérêt ?
stream.collect(collector);
14. #DevoxxFR #ColJ8
Ou va-t-on ?
« Petit » rappel sur les streams
Les collectors qui existent
Comment étendre les collectors qui existent
Comment rendre le code lisible
Les collectors qui n’existent pas
16. #DevoxxFR #ColJ8
Un Stream c’est…
Un objet qui se connecte à une source de
données et les regarde passer
Un stream ne « contient pas » de données
stream
21. #DevoxxFR #ColJ8
Map, Filter, FlatMap
Trois opérations qui ne stockent aucune
information pour fonctionner
Un objet entre = un objet sort, sans délai
Pas de le cas de toutes les opérations…
22. #DevoxxFR #ColJ8
Tri en fonction d’un comparateur…
Nécessite de « voir » toutes les données
stream
24. #DevoxxFR #ColJ8
Distinct, sorted
Sorted :
a besoin de « tout voir » pour faire le tri
Distinct :
doit « retenir ce qui passe » et laisse passer
Dans les deux cas : besoin d’un buffer
26. #DevoxxFR #ColJ8
Cas de limit et skip
Deux méthodes qui dépendent de l’ordre des
éléments :
- Limit = conserve les n premiers éléments
- Skip = saute les n premiers éléments
Ne gère pas un buffer mais un compteur
Besoin de voir les données « dans l’ordre »
34. #DevoxxFR #ColJ8
Un premier collector
Et puis il y a collect !
Probablement le plus utilisé :
Prend un collector en paramètre
List<String> result =
strings.stream()
.filter(s -> s.itEmpty())
.collect(Collectors.toList());
35. #DevoxxFR #ColJ8
Un premier collector (bis)
Et puis il y a collect !
Probablement le plus utilisé :
Prend un collector en paramètre
Set<String> result =
strings.stream()
.filter(s -> s.itEmpty())
.collect(Collectors.toSet());
36. #DevoxxFR #ColJ8
Un deuxième collector
Et puis il y a collect !
Peut-être moins connu ?
Prend un collector en paramètre
String authors =
authors.stream()
.map(Author::getName)
.collect(Collectors.joining(", "));
38. #DevoxxFR #ColJ8
Un troisième collector
Construction de Map
Map<Integer, List<String>> result =
strings.stream()
.filter(s -> s.itEmpty())
.collect(
Collectors.groupingBy(
s -> s.length()
)
);
42. #DevoxxFR #ColJ8
Un troisième collector (bis)
Construction de Map
Map<Integer, Long> result =
strings.stream()
.filter(s -> s.itEmpty())
.collect(
Collectors.groupingBy(
s -> s.length(), Collectors.counting()
)
);
49. #DevoxxFR #ColJ8
Construction de listes
Reprenons le code :
List<String> result =
strings.stream()
.filter(s -> s.itEmpty())
.collect(Collectors.toList());
50. #DevoxxFR #ColJ8
stream a b b
collector
1) construire la liste
2) ajouter un élément
a b c
ArrayList
51. #DevoxxFR #ColJ8
Construction de listes
1) Construction de la liste : supplier
2) Ajout d’un élément à la liste : accumulateur
Supplier<List> supplier = () -> new ArrayList();
BiConsumer<List<E>, E> accumulator = (list, e) -> list.add(e);
53. #DevoxxFR #ColJ8
Construction de listes
1) Construction de la liste : supplier
2) Ajout d’un élément à la liste : accumulateur
3) Combinaison de deux listes
Supplier<List> supplier = ArrayList::new;
BiConsumer<List<E>, E> accumulator = List::add;
BiConsumer<List<E>, List<E>> combiner = List::addAll;
54. #DevoxxFR #ColJ8
Construction de listes
Ce qui donne :
List<String> result =
strings.stream()
.filter(s -> s.itEmpty())
.collect(ArrayList::new,
List::add,
List::adAll);
55. #DevoxxFR #ColJ8
Construction de listes
Ce qui donne :
List<String> result =
strings.stream()
.filter(s -> s.itEmpty())
.collect(ArrayList::new,
Collection::add,
Collection::adAll);
56. #DevoxxFR #ColJ8
Construction de sets
Ce qui donne :
Set<String> result =
strings.stream()
.filter(s -> s.itEmpty())
.collect(HashSet::new,
Collection::add,
Collection::adAll);
57. #DevoxxFR #ColJ8
Concaténation de String
Plutôt qu’une liste on veut construire le
résultat dans une chaîne de caractères séparée
par des virgules :
« one, two, six »
Ne fonctionne que sur les streams de String
65. #DevoxxFR #ColJ8
Un collector
3 opérations + 1
- supplier, construit un container de calcul
- accumulator
- combiner
- finisher, qui peut être la fonction identité
69. #DevoxxFR #ColJ8
Interface Collector
public interface Collector<T, A, R> {
public Supplier<A> supplier(); // A : container intermédiaire
public BiConsumer<A, T> accumulator();// T : éléments traités
public BinaryOperator<A> combiner(); // retourne un A
public Function<A, R> finisher(); // touche finale
}
70. #DevoxxFR #ColJ8
Interface Collector
public interface Collector<T, A, R> {
public Supplier<A> supplier(); // A : container intermédiaire
public BiConsumer<A, T> accumulator();// T : éléments traités
public BinaryOperator<A> combiner(); // retourne un A
public Function<A, R> finisher(); // touche finale
public Set<Characteristics> characteristics();
}
71. #DevoxxFR #ColJ8
Type d’un collector
Décodage
- T : type des éléments du stream
- A : type du container intermédiaire
- R : type du container final
On a souvent A = R
Le finisher est souvent la fonction identité
≠
72. #DevoxxFR #ColJ8
3
4
5
one, two, three, four, five, six, seven, eight, nine, ten
one, two, six, ten
four, five, nine
three, seven, eight
groupingBy(String::length)
73. #DevoxxFR #ColJ8
3
4
5
one, two, three, four, five, six, seven, eight, nine, ten
one, two, six, ten
four, five, nine
three, seven, eight
Collector<String, ?, Map<Integer, List<String>> > c =
groupingBy(String::length)
74. #DevoxxFR #ColJ8
3
4
5
one, two, three, four, five, six, seven, eight, nine, ten
one, two, six, ten
four, five, nine
three, seven, eight
Collector<String, ?, Map<Integer, List<String>> > c =
groupingBy(String::length)
75. #DevoxxFR #ColJ8
3
4
5
one, two, three, four, five, six, seven, eight, nine, ten
one, two, six, ten
four, five, nine
three, seven, eight
Collector<String, ?, Map<Integer, List<String>> > c =
groupingBy(String::length)
83. #DevoxxFR #ColJ8
Mapping et toMap
Un collector pour faire du mapping
Prend un downstream obligatoire
toMap :
stream.collect(mapping(function, downstream));
stream.collect(
toMapping(
t -> key,
t -> value));
85. #DevoxxFR #ColJ8
Collector intermédiaires
Le collector mapping intègre une opération
intermédiaire
Intérêt ?
Pouvoir créer des downstream collectors
Un collector = intégrer le traitement d’un
stream en totalité
87. #DevoxxFR #ColJ8
Interface Collector
public interface Collector<T, A, R> {
public Supplier<A> supplier(); // A : container intermédiaire
public BiConsumer<A, T> accumulator();// T : éléments traités
public BinaryOperator<A> combiner(); // retourne un A
public Function<A, R> finisher(); // touche finale
public Set<Characteristics> characteristics();
}
88. #DevoxxFR #ColJ8
Collector intermédiaires
Le collector mapping intègre une opération
intermédiaire
stream.collect(mapping(function, downstream));
stream.collect(filtering(predicate, downstream));
Stream<T> donc Predicate<T>
Et Collector<T, ?, R>
89. #DevoxxFR #ColJ8
Collector intermédiaires
Le collector mapping intègre une opération
intermédiaire
Stream<T> donc Function<T, Stream<TT>>
Et Collector<TT, ?, R>
stream.collect(mapping(function, downstream));
stream.collect(flatMapping(flatMapper, downstream));
91. #DevoxxFR #ColJ8
Caractéristiques
Trois caractéristiques pour les collectors:
- IDENTITY_FINISH : le finisher est la fonction
identité
- UNORDERED : le collector ne conserve pas
l’ordre des éléments
- CONCURRENT : il est thread safe
94. #DevoxxFR #ColJ8
Application
L’intérêt est de modéliser un traitement dans
un collecteur :
Pouvoir utiliser le collector en tant que
downstream pour des traitements plus
avancés
95. #DevoxxFR #ColJ8
Autre application
1) Les deux auteurs qui ont le plus publié
ensemble
2) Les deux auteurs qui ont le plus publié
ensemble en un an
Utilisation de StreamsUtils
98. #DevoxxFR #ColJ8
Problème ?
On peut avoir un problème si le stream des
paires d’auteurs est vide
Ce qui arrive si les seuls articles sont des
articles à un auteur
Sur toute la base, pas de risque
… mais quand on regroupe par année…
100. #DevoxxFR #ColJ8
Inversion d’une relation
On a travaillé sur la relation entre les articles et
leurs auteurs
On veut inverser cette relation : connaître les
articles par auteur
103. #DevoxxFR #ColJ8
API Collector
API très riche
Un peu complexe, nécessite quelques repères
Surtout, nécessite de comprendre le
traitement que l’on veut faire
104. #DevoxxFR #ColJ8
API Collector
Un collector = un objet qui modélise un
traitement de données dans sa totalité
Peut donc être utilisé comme traitement
partiel d’un traitement plus large