L'API Collector dans tous ses états

106
#DevoxxFR L’API Collector dans tous ses états @JosePaumard https://github.com/JosePaumard https://www.slideshare.net/jpaumard https://www.youtube.com/user/JPaumard

Transcript of L'API Collector dans tous ses états

Page 1: L'API Collector dans tous ses états

#DevoxxFR

L’API Collectordans tous ses états@JosePaumard

https://github.com/JosePaumard

https://www.slideshare.net/jpaumard

https://www.youtube.com/user/JPaumard

Page 2: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

@JosePaumard

Microsoft Virtual Academy

Page 3: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

@JosePaumard

Page 4: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Questions ?#ColJ8

Page 5: L'API Collector dans tous ses états

#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é

Page 6: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Collectors ?YouTube :

▪ Tutoriaux sur les Streams ~670k

▪ Tutoriaux sur les Collectors < 4,5k

Page 7: L'API Collector dans tous ses états

#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

Page 8: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

movies.stream().flatMap(movie -> movie.actors().stream()).collect(

Collectors.groupingBy(Function.identity(), Collectors.counting()

)).entrySet().stream().max(Map.Entry.comparingByValue()).get();

Page 9: L'API Collector dans tous ses états

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

Page 10: L'API Collector dans tous ses états

#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…

Page 11: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Quelques mots sur les Stream

Page 12: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Dualité dans les StreamsDans 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…

Page 13: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Dualité dans les StreamsDans les streams :

▪ Toute opération sur un stream peut être modélisée par un collector

▪ Quel intérêt ?

stream.collect(collector);

Page 14: L'API Collector dans tous ses états

#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

Page 15: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Opérations intermédiaires

Page 16: L'API Collector dans tous ses états

#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

Page 17: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

stream

Change le type des données = mapping

Page 18: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

stream

Ne laisse pas tout passer = filtrage

Page 19: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Mise à plat = flatMap

stream

Page 20: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Mise à plat = flatMap

stream

Page 21: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Map, Filter, FlatMapTrois 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…

Page 22: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Tri en fonction d’un comparateur…Nécessite de « voir » toutes les données

stream

Page 23: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

stream

DistinctLes données peuvent traverser une par

une, certaines sont éliminées

Page 24: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Distinct, sortedSorted :

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

Page 25: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Opérations intermédiaires2 catégories :

- Opérations directes = stateless

- Opérations avec buffer = stateful

Page 26: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Cas de limit et skipDeux 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 »

Page 27: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Opérations terminales

Page 28: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Intermédiaire vs terminaleSeule une opération terminale déclenche la consommation des éléments de la source

Page 29: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Opérations terminalesPremier paquet :

- forEach

- count

- max, min

- reduce

- toArray

Page 30: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Opérations terminalesPremier paquet :

- forEach

- count

- max, min

- reduce

- toArray

Consomment toutes les données

Page 31: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Opérations terminalesDeuxième paquet :

- allMatch

- anyMatch

- noneMatch

- findFirst

- findAny

Page 32: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Opérations terminalesDeuxième paquet :

- allMatch

- anyMatch

- noneMatch

- findFirst

- findAny

N’ont pas besoin de consommer toutes les données

Page 33: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Opérations terminalesCas particuliers :

- max

- min

- reduce

Retourne un optional (cas des streams vides)

https://www.youtube.com/watch?v=Ej0sss6cq14

Page 34: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Un premier collectorEt 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());

Page 35: L'API Collector dans tous ses états

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

Page 36: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Un deuxième collectorEt 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(", "));

Page 37: L'API Collector dans tous ses états

Demo Time

Page 38: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Un troisième collectorConstruction de Map

Map<Integer, List<String>> result = strings.stream()

.filter(s -> s.itEmpty())

.collect(Collectors.groupingBy(

s -> s.length())

);

Page 39: L'API Collector dans tous ses états

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

Map<Integer, List<String>>

Page 40: L'API Collector dans tous ses états

#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, downstream)

.stream().collect(downstream)

.stream().collect(downstream)

.stream().collect(downstream)

Page 41: L'API Collector dans tous ses états

#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, Collectors.counting())

4L

3L

3L

Map<Integer, Long>

Page 42: L'API Collector dans tous ses états

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

);

Page 43: L'API Collector dans tous ses états

Demo Time

Page 44: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Un collector qui compteNombre d’articles par auteur

Page 45: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Gent & Walsh, Beyond NP: The QSAT Phase TransitionGent & Hoos & Prosser & Walsh, Morphing: Combining…

A1 A2

Gent

Walsh

Gent

Hoos

Prosser

Walsh

flatMap(Article::getAuthors)

Page 46: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Gent & Walsh, Beyond NP: The QSAT Phase TransitionGent & Hoos & Prosser & Walsh, Morphing: Combining…

Gent, Walsh, Gent, Hoos, Prosser, Walsh

flatMap(Article::getAuthors)

Gent

Walsh

Hoos

2L

2L

1L

Prosser 1L

groupingBy(

)

groupingBy(identity(),counting()

)

groupingBy(identity(),

)

Page 47: L'API Collector dans tous ses états

Demo Time

Page 48: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Supply, accumulate and combine

Page 49: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Construction de listesReprenons le code :

List<String> result = strings.stream()

.filter(s -> s.itEmpty())

.collect(Collectors.toList());

Page 50: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

stream a b b

collector1) construire la liste2) ajouter un élément

a b c

ArrayList

Page 51: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Construction de listes1) 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);

Page 52: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Cas parallèle

Stream

Collector

collector1) construire la liste2) ajouter un élément3) fusionner

CPU 2

Stream

CollectorCPU 1

Page 53: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Construction de listes1) 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;

Page 54: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Construction de listesCe qui donne :

List<String> result = strings.stream()

.filter(s -> s.itEmpty())

.collect(ArrayList::new,List::add, List::adAll);

Page 55: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Construction de listesCe qui donne :

List<String> result = strings.stream()

.filter(s -> s.itEmpty())

.collect(ArrayList::new,Collection::add, Collection::adAll);

Page 56: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Construction de setsCe qui donne :

Set<String> result = strings.stream()

.filter(s -> s.itEmpty())

.collect(HashSet::new,Collection::add, Collection::adAll);

Page 57: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Concaténation de StringPlutô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

Page 58: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Concaténation de StringMéthode collect :

strings.stream().filter(s -> s.length() == 3).collect(() -> new String(),

(finalString, s) -> finalString.concat(s), (s1, s2) -> s1.concat(s2));

Page 59: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Concaténation de StringMéthode collect :

strings.stream().filter(s -> s.length() == 3).collect(() -> new String(),

(finalString, s) -> finalString.concat(s), (s1, s2) -> s1.concat(s2));

Page 60: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Concaténation de StringMéthode collect :

strings.stream().filter(s -> s.length() == 3).collect(() -> new StringBuilder(),

(sb, s) -> sb.append(s), (sb1, sb2) -> sb1.append(sb2));

Page 61: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Concaténation de StringMéthode collect :

strings.stream().filter(s -> s.length() == 3).collect(StringBuilder::new,

StringBuilder::append, StringBuilder::append);

Page 62: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Concaténation de StringMéthode collect :

StringBuilder stringBuilder = strings.stream()

.filter(s -> s.length() == 3)

.collect(StringBuilder::new,StringBuilder::append, StringBuilder::append);

Page 63: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Concaténation de StringMéthode collect :

String string = strings.stream()

.filter(s -> s.length() == 3)

.collect(StringBuilder::new,StringBuilder::append, StringBuilder::append)

.toString();

Page 64: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Un collector3 opérations

- supplier, construit un container de calcul

- accumulator

- combiner

Page 65: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Un collector3 opérations + 1

- supplier, construit un container de calcul

- accumulator

- combiner

- finisher, qui peut être la fonction identité

Page 66: L'API Collector dans tous ses états

Demo Time

Page 67: L'API Collector dans tous ses états

#DevoxxFR

Collectors custom :1) Mapping2) Filtrage, flat map3) Jointures

Coffee break!

Page 68: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

À propos des types

Page 69: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Interface Collectorpublic 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

}

Page 70: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Interface Collectorpublic 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();}

Page 71: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Type d’un collectorDé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é≠

Page 72: L'API Collector dans tous ses états

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

Page 73: L'API Collector dans tous ses états

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

Page 74: L'API Collector dans tous ses états

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

Page 75: L'API Collector dans tous ses états

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

Page 76: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

one, two, three, four, five, six, seven, eight, nine, ten

Collector<String, ?, Map<Integer, List<String>> > c = groupingBy(

String::length,?

)

Page 77: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

one, two, three, four, five, six, seven, eight, nine, ten

Collector<String, ?, Map<Integer, List<String>> > c = groupingBy(

String::length,Collector<String, ?, >

)

Page 78: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

one, two, three, four, five, six, seven, eight, nine, ten

Collector<String, ?, Map<Integer, Value>> c = groupingBy(

String::length,Collector<String, ?, Value>

)

counting() : Collector<T, ?, Long>

Page 79: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Opérations intermédiaires

Page 80: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

7634L {2004, 7634L}

Map<Long, List<Entry<Integer, Long>>>

Page 81: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

7634L {2004, 7634L}

Map<Long, List<Entry<Integer, Long>>>

Entry<Integer, Long> -> Integer = mapping

Function<> mapper = entry -> entry.getKey();

Collectors.mapping(mapper, toList());

Page 82: L'API Collector dans tous ses états

Demo Time

Page 83: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Mapping et toMapUn collector pour faire du mapping

Prend un downstream obligatoire

toMap :

stream.collect(mapping(function, downstream));

stream.collect(toMapping(

t -> key, t -> value));

Page 84: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Collector intermédiairesLe collector mapping intègre une opération intermédiairestream.collect(mapping(function, downstream));

Page 85: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Collector intermédiairesLe 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é

Page 86: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Collector intermédiairesSi l’on peut faire du mapping, pourquoi ne pas faire du filtrage ou du flatmap ?

… on aura ces deux collectors dans Java 9

Page 87: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Interface Collectorpublic 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();}

Page 88: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Collector intermédiairesLe collector mapping intègre une opération intermédiairestream.collect(mapping(function, downstream));

stream.collect(filtering(predicate, downstream));

Stream<T> donc Predicate<T>Et Collector<T, ?, R>

Page 89: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Collector intermédiairesLe 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));

Page 90: L'API Collector dans tous ses états

Demo Time

Page 91: L'API Collector dans tous ses états

#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

Page 92: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Application 1) L’auteur qui a le plus publié

2) L’auteur qui a le plus publié en un an

Page 93: L'API Collector dans tous ses états

Demo Time

Page 94: L'API Collector dans tous ses états

#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

Page 95: L'API Collector dans tous ses états

#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

Page 96: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Gent & Walsh, Beyond NP: The QSAT Phase TransitionGent & Hoos & Prosser & Walsh, Morphing: Combining…

Gent, Hoos, Prosser, Walsh

Gent, Walsh

{Gent, Walsh}

{Gent, Hoos} {Gent, Prosser} {Gent, Walsh}{Hoos, Prosser} {Hoos, Walsh}{Prosser, Walsh}

flatMap()

Page 97: L'API Collector dans tous ses états

Demo Time

Page 98: L'API Collector dans tous ses états

#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…

Page 99: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Inverser une relation

Page 100: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Inversion d’une relationOn a travaillé sur la relation entre les articles et leurs auteurs

On veut inverser cette relation : connaître les articles par auteur

Page 101: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Gent & Walsh, Beyond NP: The QSAT Phase Transition

{Art1, Gent}, {Art1, Walsh}

{Art1, {Gent, Walsh}}

flatMap()

groupingBy()

Page 102: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

Conclusion

Page 103: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

API CollectorAPI très riche

Un peu complexe, nécessite quelques repères

Surtout, nécessite de comprendre le traitement que l’on veut faire

Page 104: L'API Collector dans tous ses états

#DevoxxFR #ColJ8

API CollectorUn 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

Page 105: L'API Collector dans tous ses états

#DevoxxFR

Merci pour votre attention

Page 106: L'API Collector dans tous ses états

#DevoxxFR

Questions ?

@JosePaumard

https://github.com/JosePaumard

https://www.slideshare.net/jpaumard

https://www.youtube.com/user/JPaumard