JDK 8 & Lambdas
Lambdas, Streams, Collectors
Pourquoi ont-ils été introduits ?
Pourquoi ont-ils été introduits ?
Pourquoi sont-ils si importants ?
Pourquoi ont-ils été introduits ?
Pourquoi sont-ils si importants ?
Comment vont-ils changer nos habitudes de
programmation ?
Introduisons les lambdas
Introduisons les lambdas sur un exemple simple
Un exemple très simple
public class Person { private String name ; private int age ; // constructors // getters / setters }
List<Person> list = new ArrayList<>() ;
Un bon vieux bean
… et une bonne vieille liste
int sum = 0 ; // I need a default value in case // the list is empty int average = 0 ; for (Person person : list) { sum += person.getAge() ; } if (!list.isEmpty()) { average = sum / list.size() ; }
Calculons la moyenne des âges des personnes
int sum = 0 ; int n = 0 ; int average = 0 ; for (Person person : list) { if (person.getAge() > 20) { n++ ; sum += person.getAge() ; } } if (n > 0) { average = sum / n ; }
Plus dur : pour les personnes de plus de 20 ans
« programmation impérative »
int sum = 0 ; int n = 0 ; int average = 0 ; for (Person person : list) { if (person.getAge() > 20) { n++ ; sum += person.getAge() ; } } if (n > 0) { average = sum / n ; }
Plus dur : pour les personnes de plus de 20 ans
select avg(age) from Person where age > 20
… pas une obligation !
Ici on décrit le résultat
select avg(age) from Person where age > 20
Dans ce cas la base de données mène le calcul
comme elle l’entend
© SQL 1974
… pas une obligation !
Ici on décrit le résultat
Person age age > 20 sum
1ère étape : mapping
map
Mapping :
- prend une liste d’un type donné
- retourne une liste d’un autre type
- possède le même nombre d’éléments
1ère étape : mapping
map
Person age age > 20 sum
2ème étape : filtrage
filter map
Person age age > 20 sum
Filtrage :
- prend une liste d’un type donné
- retourne une liste du même type
- mais avec moins d’éléments
Person age age > 20 sum
2ème étape : filtrage
filter map
3ème étape : réduction
reduce filter map
Person age age > 20 sum
Reduction : agrégation des éléments d’une
liste dans un seul élément
Ex : moyenne, somme, min, max, etc…
Person age age > 20 sum
3ème étape : réduction
reduce filter map
Comment peut-on
modéliser ce traitement ?
La façon JDK 7
On crée une interface pour modéliser le mapper…
public interface Mapper<T, V> { public V map(T t) ; }
Mapper<Person, Integer> mapper = new Mapper<Person, Integer>() { public Integer map(Person p) { return p.getAge() ; } }
La façon JDK 7
… et on crée une classe anonyme
public interface Mapper<T, V> { public V map(T t) ; }
La façon JDK 7
On peut faire la même chose pour le filtrage
public interface Predicate<T> { public boolean filter(T t) ; }
AgePredicate predicate = new Predicate<Integer>() { public boolean filter(Integer i) { return i > 20 ; } }
public interface Predicate<T> { public boolean filter(T t) ; }
La façon JDK 7
On peut faire la même chose pour le filtrage
Et enfin pour la réduction
public interface Reducer<T> { public T reduce(T t1, T t2) ; }
La façon JDK 7
Reducer<Integer> reduction = new Reducer<Integer>() { public Integer reduce(Integer i1, Integer i2) { return i1 + i2 ; } }
public interface Reducer<T> { public T reduce(T t1, T t2) ; }
La façon JDK 7
Et enfin pour la réduction
Au final, le pattern map / filter / reduce en JDK 7 :
1) Créer 3 interfaces
public interface Mapper<T, V> { public V map(T t) ; }
public interface Predicate<T> { public boolean filter(T t) ; }
public interface Reducer<T> { public T reduce(T t1, T t2) ; }
La façon JDK 7
List<Person> persons = ... ; int sum = persons.map( new Mapper<Person, Integer>() { public Integer map(Person p) { return p.getAge() ; } }) .filter( new Filter<Integer>() { public boolean filter(Integer age) { return age > 20 ; } }) .reduce(0, new Reducer<Integer>() { public Integer recude(Integer i1, Integer i2) { return i1 + i2 ; } } }) ;
La façon JDK 7
Au final, le pattern map / filter / reduce en JDK 7 :
1) Créer 3 interfaces
2) Et on applique…
List<Person> persons = ... ; int sum = persons.map( new Mapper<Person, Integer>() { public Integer map(Person p) { return p.getAge() ; } }) .filter( new Filter<Integer>() { public boolean filter(Integer age) { return age > 20 ; } }) .reduce(0, new Reducer<Integer>() { public Integer recude(Integer i1, Integer i2) { return i1 + i2 ; } } }) ;
La façon JDK 7
Au final, le pattern map / filter / reduce en JDK 7 :
1) Créer 3 interfaces
2) Et on applique…
À la façon du JDK 8
mapper = new Mapper<Person, Integer>() { public Integer map(Person person) { return person.getAge() ; } }
La façon du JDK 8
Prenons l’exemple du Mapper
mapper = new Mapper<Person, Integer>() { public Integer map(Person person) { // 1 méthode return person.getAge() ; } }
Prenons l’exemple du Mapper
La façon du JDK 8
mapper = new Mapper<Person, Integer>() { public Integer map(Person person) { // 1 méthode return p.getAge() ; } }
mapper = (Person person) ;
On prend
person
La façon du JDK 8
Prenons l’exemple du Mapper
mapper = new Mapper<Person, Integer>() { public Integer map(Person person) { // 1 méthode return person.getAge() ; } }
mapper = (Person person) -> ;
et…
La façon du JDK 8
Prenons l’exemple du Mapper
mapper = new Mapper<Person, Integer>() { public Integer map(Person person) { // 1 méthode return person.getAge() ; } }
mapper = (Person person) -> person.getAge() ;
… on retourne son âge
La façon du JDK 8
Prenons l’exemple du Mapper
mapper = new Mapper<Person, Integer>() { public Integer map(Person person) { // 1 méthode return person.getAge() ; } }
mapper = (Person person) -> person.getAge() ;
La façon du JDK 8
Prenons l’exemple du Mapper
Prenons l’exemple du Mapper
Le compilateur reconnaît cette expression comme une
implémentation du mapper
mapper = new Mapper<Person, Integer>() { public Integer map(Person person) { // 1 méthode return person.getAge() ; } }
mapper = (Person person) -> person.getAge() ;
La façon du JDK 8
Que se passe-t-il si …
… il y a plus d’une ligne de code ?
Accolades, un return explicite
mapper = (Person person) -> { System.out.println("Mapping " + person) ; return person.getAge() ; }
Que se passe-t-il si …
…le type de retour est void ?
consumer = (Person person) -> p.setAge(p.getAge() + 1) ;
Que se passe-t-il si …
…la méthode prend plus d’un argument ?
Ou :
reducer = (int i1, int i2) -> { return i1 + i2 ; }
reducer = (int i1, int i2) -> i1 + i2 ;
La façon du JDK 8
Comment le compilateur reconnaît-il l’implémentation du
mapper ?
mapper = (Person person) -> person.getAge() ;
Comment le compilateur reconnaît-il l’implémentation du
mapper ?
1) Il ne faut qu’une méthode dans le mapper
mapper = (Person person) -> person.getAge() ;
La façon du JDK 8
Comment le compilateur reconnaît-il l’implémentation du
mapper ?
1) Il ne faut qu’une méthode dans le mapper
2) Les types des paramètres et le type de retour doivent
être compatible
mapper = (Person person) -> person.getAge() ;
La façon du JDK 8
Comment le compilateur reconnaît-il l’implémentation du
mapper ?
1) Il ne faut qu’une méthode dans le mapper
2) Les types des paramètres et le type de retour doivent
être compatible
3) Les exceptions jetées doivent être compatibles
mapper = (Person person) -> person.getAge() ;
La façon du JDK 8
D’autres lambdas
On peut écrire d’autres lambdas facilement :
mapper = (Person person) -> person.getAge() ; // mapper filter = (int age) -> age > 20 ; // filter reducer = (int i1, int i2) -> i1 + i2 ; // reducer
Et la plupart du temps, le compilateur reconnaît ceci :
Le type des paramètres peut être omis
mapper = person -> person.getAge() ; // mapper filter = age -> age > 20 ; // filter reducer = (i1, i2) -> i1 + i2 ; // reducer
D’autres lambdas
Une remarque sur la réduction
Comment cela fonctionne-t-il réellement ?
Réduction
2 exemples :
Attention :
le résultat est toujours reproductible en série
il ne l’est en général pas en parallèle
Reducer r1 = (i1, i2) -> i1 + i2 ; // Ok Reducer r2 = (i1, i2) -> i1*i1 + i2*i2 ; // Oooops
Pour le moment
Une expression lambda est une autre façon
d’écrire des instances de classes anonymes
Il y a d’autres syntaxes
On peut écrire :
Mais on peut aussi écrire :
mapper = person -> person.getAge() ;
mapper = Person::getAge ; // méthode non statique
Il y a d’autres syntaxes
On peut écrire :
Ou encore :
sum = (i1, i2) -> i1 + i2 ; sum = Integer::sum ; // méthode statique, nouvelle !
max = (i1, i2) -> i1 > i2 ? i1 : i2 ; max = Integer::max ; // méthode statique, nouvelle !
Il y a d’autres syntaxes
Encore un autre exemple :
toLower = String::toLowerCase ;
// !!!! NON NON NON !!!! toLowerFR = String::toLowerCase(Locale.FRANCE) ;
Plus loin sur les lambdas
Questions :
Comment modéliser une expression lambda ?
Questions :
Comment modéliser une expression lambda ?
Puis-je mettre une expression lambda dans une variable ?
Questions :
Comment modéliser une expression lambda ?
Puis-je mettre une expression lambda dans une variable ?
Un lambda est-il un objet ?
Modélisation
Un lambda = instance d’une « interface fonctionnelle »
@FunctionalInterface public interface Consumer<T> { public void accept(T t) ; }
Modélisation
Un lambda = instance d’une « interface fonctionnelle »
- ne possède qu’une unique méthode
@FunctionalInterface public interface Consumer<T> { public void accept(T t) ; }
Modélisation
Un lambda = instance d’une « interface fonctionnelle »
- ne possède qu’une unique méthode
- peut être annotée par @FunctionalInterface (optionnel)
@FunctionalInterface public interface Consumer<T> { public void accept(T t) ; }
Mettre un lambda dans une variable
Exemple d’un consommateur :
Consumer<String> c = new Consumer<String>() { @Override public void accept(String s) { System.out.println(s) ; } } ;
Consumer<String> c = s -> System.out.println(s) ;
Donc :
Mettre un lambda dans une variable
Exemple d’un consommateur :
Consumer<String> c = new Consumer<String>() { @Override public void accept(String s) { System.out.println(s) ; } } ;
Consumer<String> c = s -> System.out.println(s) ;
Question : qu’est-ce que s ?
Donc :
Mettre un lambda dans une variable
Exemple d’un consommateur :
Consumer<String> c = new Consumer<String>() { @Override public void accept(String s) { System.out.println(s) ; } } ;
Consumer<String> c = s -> System.out.println(s) ;
Réponse : le
compilateur infère qu’il
s’agit d’un String Donc :
Mettre un lambda dans une variable
Question : peut-on écrire ce code ?
int i = ... ; Consumer<String> c = new Consumer<String>() { @Override public void accept(String s) { System.out.println("i = " + i) ; } } ;
Mettre un lambda dans une variable
Question : peut-on écrire ce code ?
JDK 7 : i doit être final
int i = ... ; Consumer<String> c = new Consumer<String>() { @Override public void accept(String s) { System.out.println("i = " + i) ; } } ;
Mettre un lambda dans une variable
Question : peut-on écrire ce code ?
JDK 8 : ce code compile
int i = ... ; Consumer<String> c = new Consumer<String>() { @Override public void accept(String s) { System.out.println("i = " + i) ; } } ;
Mettre un lambda dans une variable
En revanche, ce code …
… ne compile pas
int i = ... ; Consumer<String> c = new Consumer<String>() { @Override public void accept(String s) { System.out.println("i = " + i) ; } } ; i = i + 1 ;
Mettre un lambda dans une variable
Raison : le compilateur infère que i est effectivement final
final int i = ... ; Consumer<String> c = new Consumer<String>() { @Override public void accept(String s) { System.out.println("i = " + i) ; } } ; i = i + 1 ; // on ne peut pas modifier i
Note au sujet de this
JDK 7 : this est l’instance anonyme
Appeler accept() affiche donc « je suis dans c »
Consumer<String> c = new Consumer<String>() { @Override public void accept(String s) { System.out.println("this = " + this) ; } public String toString() { return "je suis dans c" ; } } ;
Note au sujet de this
JDK 8 : idem, accept() affiche « je suis dans c »
Mais …
Consumer<String> c = new Consumer<String>() { @Override public void accept(String s) { System.out.println("this = " + this) ; } public String toString() { return "je suis dans c" ; } } ;
Note au sujet de this
Un consommateur peut aussi s’écrire comme ça :
Dans ce cas, this est l’instance englobante
Consumer<String> c = s -> System.out.println(this) ;
Questions :
Quel modèle pour un lambda ?
réponse : une interface fonctionnelle
Puis-je mettre un lambda dans une variable ?
réponse : oui
Un lambda est-il un objet ?
Un lambda est-il un objet ?
Petit jeu des 7 erreurs (avec une seule erreur)
Consumer<String> c = new Consumer<String>() { @Override public void accept(String s) { System.out.println(s) ; } } ;
Consumer<String> c = s -> System.out.println(s) ;
Un lambda est-il un objet ?
Petit jeu des 7 erreurs (avec une seule erreur)
Consumer<String> c = new Consumer<String>() { @Override public void accept(String s) { System.out.println(s) ; } } ;
Consumer<String> c = s -> System.out.println(s) ;
Questions :
Quel modèle pour un lambda ?
réponse : une interface fonctionnelle
Puis-je mettre un lambda dans une variable ?
réponse : oui
Un lambda est-il un objet ?
réponse : non
Des lambdas à profusion :
Java.util.functions
Java.util.functions
C’est dans ce package que sont les interfaces
fonctionnelles
Il y en a 43
Java.util.functions
Supplier
Consumer / BiConsumer
Function / BiFunction (UnaryOperator / BinaryOperator)
Predicate / BiPredicate
En plus des versions construites sur les types primitifs
Supplier
Un supplier fournit un objet
public interface Supplier<T> { T get() ; }
Consumer
Un consommateur consomme un objet
public interface Consumer<T> { void accept(T t) ; }
Consumer<String> c1 = s -> System.out.println(s) ; Consumer<String> c2 = ... ; Consumer<String> c3 = c1.andThen(c2) ; persons.stream().forEach(c3) ;
BiConsumer
Prend deux arguments au lieu d’un
Peuvent être chaînés
Versions types primitifs :
- ObjIntConsumer
- ObjLongConsumer
- ObjDoubleConsumer
ObjIntConsumer prend un objet et un int en arguments
Function
Une fonction prend un objet et retourne un autre objet
Les fonctions peuvent être chaînées et / ou composées
BiFunction prend deux arguments au lieu d’un
UnaryOperator et BinaryOperator opèrent sur un seul type
public interface Function<T, R> { R apply(T t) ; }
Predicate
Un Predicate prend un objet et retourne un booléen
Il peut être inversé, et composé avec des AND ou OR
public interface Predicate<T> { boolean test(T t) ; }
BiPredicate
Un BiPredicate prend deux objets et retourne un booléen
Version pour les types booléens
public interface BiPredicate<T, U> { boolean test(T t, U u) ; }
Retour sur
map / filter / reduce
La façon JDK 7
Comment implémenter le pattern map / filter / reduce
sur List<Person> ?
La façon JDK 7
Comment implémenter le pattern map / filter / reduce
sur List<Person> ?
La façon classique est d’itérer sur les éléments et
d’appliquer le pattern
La façon JDK 7
Comment implémenter le pattern map / filter / reduce
sur List<Person> ?
La façon classique est d’itérer sur les éléments et
d’appliquer le pattern
On peut pour cela créer une méthode helper
Mapping d’une liste
Mapping en JDK 8 avec des lambdas
List<Person> persons = new ArrayList<>() ; List<Integer> ages = Lists.map( persons, person -> person.getAge() ) ;
Mapping d’une liste
Mapping en JDK 8 avec des lambdas
List<Person> persons = new ArrayList<>() ; List<Integer> ages = Lists.map( persons, Person::getAge ) ;
Pattern complet
Le pattern va ressembler à ça :
// pattern map / filter / reduce List<Person> persons = ... ; List<Integer> ages = Lists.map( persons, p -> p.getAge()) ; List<Integer> agesGT20 = Lists.filter(ages, a -> a > 20) ; int sum = Lists.reduce(agesGT20, (i1, i2) -> i1 + i2) ;
Pattern complet
Le pattern va ressembler à ça :
L’idée est de pousser un lambda vers l’implémentation, et
de la laisser itérer en interne
// pattern map / filter / reduce List<Person> persons = ... ; List<Integer> ages = Lists.map( persons, p -> p.getAge()) ; List<Integer> agesGT20 = Lists.filter(ages, a -> a > 20) ; int sum = Lists.reduce(agesGT20, (i1, i2) -> i1 + i2) ;
Pattern complet
Le pattern va ressembler à ça :
Pour : l’API peut être optimisée sans
que l’on ait à toucher au code, super !
// pattern map / filter / reduce List<Person> persons = ... ; List<Integer> ages = Lists.map( persons, p -> p.getAge()) ; List<Integer> agesGT20 = Lists.filter(ages, a -> a > 20) ; int sum = Lists.reduce(agesGT20, (i1, i2) -> i1 + i2) ;
Pattern complet
Le pattern va ressembler à ça :
Pour : l’API peut être optimisée sans
que l’on ait à toucher au code, super !
Contre…
// pattern map / filter / reduce List<Person> persons = ... ; List<Integer> ages = Lists.map( persons, p -> p.getAge()) ; List<Integer> agesGT20 = Lists.filter(ages, a -> a > 20) ; int sum = Lists.reduce(agesGT20, (i1, i2) -> i1 + i2) ;
Pattern complet
1) Supposons que persons soit vraiment GRANDE
// pattern map / filter / reduce List<Person> persons = ... ; List<Integer> ages = Lists.map( persons, p -> p.getAge()) ; List<Integer> agesGT20 = Lists.filter(ages, a -> a > 20) ; int sum = Lists.reduce(agesGT20, (i1, i2) -> i1 + i2) ;
Pattern complet
1) Supposons que persons soit vraiment GRANDE
2 duplications : ages & agesGT20
// pattern map / filter / reduce List<Person> persons = ... ; List<Integer> ages = Lists.map( persons, p -> p.getAge()) ; List<Integer> agesGT20 = Lists.filter(ages, a -> a > 20) ; int sum = Lists.reduce(agesGT20, (i1, i2) -> i1 + i2) ;
Pattern complet
1) Supposons que persons soit vraiment GRANDE
2 duplications : ages & agesGT20
Que fait-on de ces listes ? On les envoie au GC !
// pattern map / filter / reduce List<Person> persons = ... ; List<Integer> ages = Lists.map( persons, p -> p.getAge()) ; List<Integer> agesGT20 = Lists.filter(ages, a -> a > 20) ; int sum = Lists.reduce(agesGT20, (i1, i2) -> i1 + i2) ;
Pattern complet
2) Supposons que les algorithmes de calcul du map / filter /
reduce soient déjà optimisés
// pattern map / filter / reduce List<Person> persons = ... ; List<Integer> ages = Lists.map( persons, p -> p.getAge()) ; List<Integer> agesGT20 = Lists.filter(ages, a -> a > 20) ; int sum = Lists.reduce(agesGT20, (i1, i2) -> i1 + i2) ;
Pattern complet
2) Supposons que les algorithmes de calcul du map / filter /
reduce soient déjà optimisés
… mais on doit encore gagner du temps !
// pattern map / filter / reduce List<Person> persons = ... ; List<Integer> ages = Lists.map( persons, p -> p.getAge()) ; List<Integer> agesGT20 = Lists.filter(ages, a -> a > 20) ; int sum = Lists.reduce(agesGT20, (i1, i2) -> i1 + i2) ;
Pattern complet
2) Supposons que les algorithmes de calcul du map / filter /
reduce soient déjà optimisés
Seule solution : paralléliser !
// pattern map / filter / reduce List<Person> persons = ... ; List<Integer> ages = Lists.map( persons, p -> p.getAge()) ; List<Integer> agesGT20 = Lists.filter(ages, a -> a > 20) ; int sum = Lists.reduce(agesGT20, (i1, i2) -> i1 + i2) ;
Pattern complet
2) Supposons que l’on veuille paralléliser…
Il faut mieux que ages et agesGT20 soient des collections
concurrentes !
// pattern map / filter / reduce List<Person> persons = ... ; List<Integer> ages = Lists.map( persons, p -> p.getAge()) ; List<Integer> agesGT20 = Lists.filter(ages, a -> a > 20) ; int sum = Lists.reduce(agesGT20, (i1, i2) -> i1 + i2) ;
Pattern complet
3) Supposons que nous aillons un reducer : allMatch
allMatch est vrai si tous les noms sont plus courts que 20
caractères
Voyons une implémentation possible de allMatch()
// pattern map / filter / reduce List<Person> persons = ... ; List<String> names = Lists.map(persons, p -> p.getName()) ; boolean allMatch = Lists.allMatch(names, n -> n.length() < 20) ;
Écriture de allMatch()
Voici une implémentation basique de allMatch()
public static <T> boolean allMatch( List<? extends T> list, Filter<T> filter) { for (T t : list) { if (!filter.filter(t)) { return false ; } } return true ; }
Écriture de allMatch()
Voici une implémentation basique de allMatch()
public static <T> boolean allMatch( List<? extends T> list, Filter<T> filter) { for (T t : list) { if (!filter.filter(t)) { return false ; } } return true ; }
Pas besoin de parcourir
toute la liste !
Écriture de allMatch()
Voici une implémentation basique de allMatch()
public static <T> boolean allMatch( List<? extends T> list, Filter<T> filter) { for (T t : list) { if (!filter.filter(t)) { return false ; } } return true ; }
Sauf que…
Pattern complet
Quand on applique la réduction allMatch()…
… names a déjà été évaluée !
// pattern map / filter / reduce List<Person> persons = ... ; List<String> names = Lists.map(persons, p -> p.getName()) ; boolean allMatch = Lists.allMatch(names, n.length() < 20) ;
Pattern complet
Quand on applique la réduction allMatch()…
… names a déjà été évaluée !
On a perdu une belle optimisation…
// pattern map / filter / reduce List<Person> persons = ... ; List<String> names = Lists.map(persons, p -> p.getName()) ; boolean allMatch = Lists.allMatch(names, n.length() < 20) ;
Pattern complet
Quand on applique la réduction allMatch()…
… names a déjà été évaluée !
Il aurait fallu appliquer le mapping de façon lazy
// pattern map / filter / reduce List<Person> persons = ... ; List<String> names = Lists.map(persons, p -> p.getName()) ; boolean allMatch = Lists.allMatch(names, n.length() < 20) ;
Conclusion
Pour : 1
Contre : 3 (au moins)
// pattern map / filter / reduce List<Person> persons = ... ; List<Integer> ages = Lists.map( persons, p -> p.getAge()) ; List<Integer> agesGT20 = Lists.filter(ages, a -> a > 20) ; int sum = Lists.reduce(agesGT20, (i1, i2) -> i1 + i2) ;
// pattern map / filter / reduce List<Person> persons = ... ; List<Integer> ages = Lists.map( persons, p -> p.getAge()) ; List<Integer> agesGT20 = Lists.filter(ages, a -> a > 20) ; int sum = Lists.reduce(agesGT20, (i1, i2) -> i1 + i2) ;
Conclusion
Pour : 1
Contre : 3 (au moins)
Conclusion
Et il en va de même pour celui-ci :
// pattern map / filter / reduce List<Person> persons = ... ; List<Integer> ages = persons.map(p -> p.getAge()) ; List<Integer> agesGT20 = ages.filter(a -> a > 20) ; int sum = agesGT20.reduce(ages, (i1, i2) -> i1 + i2) ;
Conclusion (again)
On a besoin d’un nouveau concept pour traiter les listes de
grande taille de façon efficace
Conclusion (again)
On a besoin d’un nouveau concept pour traiter les listes de
grande taille de façon efficace
Le framework Collection n’apporte pas de solution
satisfaisante
Conclusion (again)
On a besoin d’un nouveau concept pour traiter les listes de
grande taille de façon efficace
Le framework Collection n’apporte pas de solution
satisfaisante
On a besoin de quelque chose d’autre !
Quel pattern choisir ?
Introduction
Implémenter le map / filter / reduce sur Collection aurait
mené à ceci :
Et même si cette approche n’est pas viable, c’est une
façon agréable d’écrire les choses
// map / filter / reduce pattern sur Collection int sum = persons .map(p -> p.getAge()) .filter(a -> a > 20) .reduce(0, (a1, a2) -> a1 + a2) ;
Introduction
Conservons le même genre de pattern, en ajoutant un
appel intermédiaire
// map / filter / reduce pattern sur Collection int sum = persons.stream() .map(p -> p.getAge()) .filter(a -> a > 20) .reduce(0, (a1, a2) -> a1 + a2) ;
Introduction
Collection.stream() retourne un Stream : une nouvelle
interface
Nouvelle interface = on a les mains libres !
// map / filter / reduce pattern sur Collection int sum = persons.stream() .map(p -> p.getAge()) .filter(a -> a > 20) .reduce(0, (a1, a2) -> a1 + a2) ;
Nouvelle interface Collection
Donc on a besoin d’une nouvelle méthode sur Collection
public interface Collection<E> { // nos bonnes vieilles méthodes Stream<E> stream() ; }
Nouvelle interface Collection
Problème : ArrayList ne compile plus…
Une solution doit être trouvée !
public interface Collection<E> { // nos bonnes vieilles méthodes Stream<E> stream() ; }
Interfaces
Problème : ArrayList ne compile plus…
Une solution doit être trouvée !
Une solution qui ne nécessite pas de modifier ou de
recompiler toutes les implémentations existantes de
Collection !
Nouvelles interfaces
Interfaces Java 8
Problème : ArrayList ne compile plus…
Une solution doit être trouvée !
Une solution qui ne nécessite pas de modifier ou de
recompiler toutes les implémentations existantes de
Collection !
Problème : ajouter des méthodes à une interface sans
toucher aux implémentations…
Problème : ajouter des méthodes à une interface sans
toucher aux implémentations…
Solution : changer la façon dont les interfaces fonctionnent
en Java !
Interfaces Java 8
Interfaces Java 8
ArrayList a besoin de l’implémentation de stream()…
public interface Collection<E> { // nos bonnes vieilles méthodes Stream<E> stream() ; }
Interfaces Java 8
Solution : mettons-les dans l’interface !
public interface Collection<E> { // nos bonnes vieilles méthodes default Stream<E> stream() { return ... ; } }
Interfaces Java 8
Introduisons les default methods en Java
Les méthodes par défaut permettent de faire évoluer de
vieilles interfaces
public interface Collection<E> { // nos bonnes vieilles méthodes default Stream<E> stream() { return ... ; } }
Méthodes par défaut
Cela amène-t-il l’héritage multiple en Java ?
Méthodes par défaut
Cela amène-t-il l’héritage multiple en Java ?
Oui, mais on l’a déjà
Méthodes par défaut
Cela amène-t-il l’héritage mutliple en Java ?
Oui, mais on l’a déjà
public class String implements Serializable, Comparable<String>, CharSequence { // ... }
Méthodes par défaut
Ce que l’on a en Java est l’héritage multiple de type
Méthodes par défaut
Ce que l’on a en Java est l’héritage multiple de type
Java 8 amène l’héritage multiple d’implémentation
Méthodes par défaut
Ce que l’on a en Java est l’héritage multiple de type
Java 8 amène l’héritage multiple d’implémentation
Ce que l’on a pas, c’est l’héritage multiple d’état
Méthodes par défaut
Ce que l’on a en Java est l’héritage multiple de type
Java 8 amène l’héritage multiple d’implémentation
Ce que l’on a pas, c’est l’héritage multiple d’état
… et d’ailleurs, on n’en veut pas !
Méthodes par défaut
Peut-on avoir des conflits ?
Méthodes par défaut
Peut-on avoir des conflits ?
Hélas oui…
Méthodes par défaut
Peut-on avoir des conflits ?
Hélas oui…
Il nous faut donc des règles pour les gérer
Méthodes par défaut
public class C implements A, B { // ... }
Méthodes par défaut
public class C implements A, B { // ... }
public interface A { default String a() { ... } }
public interface B { default String a() { ... } }
Méthodes par défaut
Exemple #0
La classe gagne !
L’implémentation efface la méthode par défaut
public class C implements A, B { public String a() { ... } }
public interface A { default String a() { ... } }
public interface B { default String a() { ... } }
Méthodes par défaut
Exemple #1
public class C implements A, B { // ... }
public interface A { default String a() { ... } }
public interface B { default String a() { ... } }
Méthodes par défaut
Exemple #1
Erreur de compilation :
class C inherits unrelated defaults for a() from types A and B
public class C implements A, B { // ... }
public interface A { default String a() { ... } }
public interface B { default String a() { ... } }
Méthodes par défaut
Exemple #2
A est plus spécifique que B : A.a() a la priorité
public class C implements A, B { // ... }
public interface A extends B { default String a() { ... } }
public interface B { default String a() { ... } }
Méthodes par défaut
Exemple #2
public class C implements A { // ... }
public interface A extends B { default String a() { ... } }
public interface B { default String a() { ... } }
public class D extends C implements B { // ... }
Méthodes par défaut
Exemple #2
A est toujours plus spécifique que B : A.a() gagne
public class C implements A { // ... }
public interface A extends B { default String a() { ... } }
public interface B { default String a() { ... } }
public class D extends C implements B { // ... }
Méthodes par défaut
Retour sur l’exemple #1
On peut aussi faire un appel explicite
public interface A { default String a() { ... } }
public interface B { default String a() { ... } }
public class C implements A, B { public String a() { return B.super.a() ; } }
2 règles simples
Pour gérer les conflits de l’héritage multiple d’implémentation
2 règles simples
Pour gérer les conflits de l’héritage multiple d’implémentation
1) La classe gagne
2 règles simples
Pour gérer les conflits de l’héritage multiple d’implémentation
1) La classe gagne
2) Les implémentations les plus spécifiques gagnent
Un exemple
On a tous écrit une implémentation d’Iterator :
public class MyIterator implements Iterator<E> { // du code métier super important public void remove() { throw new UnsupportedOperationException("Naaaaaaan !") ; } ; }
Un exemple
Grâce à Java 8 :
Plus besoin d’écrire ce code !
public interface Iterator<E> { default void remove() { throw new UnsupportedOperationException("remove") ; } ; }
Interfaces en Java 8
Donc on change le concept d’interfaces en Java 8
public class HelloWorld { // souvenir souvenir public static void main(String[] args) { System.out.println("Hello world!") ; } ; }
Interfaces en Java 8
Donc on change le concept d’interfaces en Java 8
Vraiment…
public interface HelloWorld { // souvenir souvenir public static void main(String[] args) { System.out.println("Hello world!") ; } ; }
Interfaces en Java 8
1) Méthodes par défaut, héritage multiple
d’implémentation
règles pour gérer les conflits, qui s’appliquent à la compilation
2) Des méthodes statiques dans les interfaces
Tout ceci va permettre de nouvelles manières
d’écrire les APIs
On en sommes-nous ?
1) On a une nouvelle syntaxe : les lambdas
2) On a un pattern à implémenter
3) On a des nouvelles interfaces qui permettent de
conserver la compatibilité ascendante
L’API Stream
Qu’est-ce qu’un Stream ?
D’un point de vue technique : une interface paramétrée
« un stream de String »
Qu’est-ce qu’un Stream ?
D’un point de vue technique : une interface paramétrée
D’autres interfaces pour les types primitifs :
« un stream de String »
IntStream, LongStream, DoubleStream
Une nouvelle notion
Cela ressemble à une collection, mais…
- Un Stream est construit sur une « source »
- Pas de donnée à la construction
- Pas de limite sur ce qu’une source peut produire
Une nouvelle notion
Un Stream peut être construit sur :
- Une collection ou un tableau
- Un itérateur
- Un source I/O
Certaines de ces sources sont « infinies »
Une nouvelle notion
1) Un stream ne porte pas de donnée
Il s’agit juste d’un objet sur lequel on peut déclarer des
opérations
Une nouvelle notion
1) Un stream ne porte pas de donnée
2) Un stream ne peut pas modifier sa source
Conséquence : il peut mener ses traitements en parallèle
Une nouvelle notion
1) Un stream ne porte pas de donnée
2) Un stream ne peut pas modifier sa source
3) Une source peut être infinie, ou non bornée
On a donc besoin de garantir que les calculs seront menés
en temps fini
Une nouvelle notion
1) Un stream ne porte pas de donnée
2) Un stream ne peut pas modifier sa source
3) Une source peut être infinie, ou non bornée
4) Un Stream traite ses données de façon lazy
On peut optimiser les opérations entre elles
On a besoin d’un mécanisme pour déclencher les
traitements
Un Stream est-il une Collection ?
La réponse est non
Un Stream est-il une Collection ?
La réponse est non
Une collection permet d’itérer sur ses éléments
Un Stream est-il une Collection ?
La réponse est non
Une collection permet d’itérer sur ses éléments
Pas un Stream
Un Stream est-il une Collection ?
La réponse est non
Une collection ne permet pas d’appliquer un lambda sur
ses éléments
Un Stream est-il une Collection ?
La réponse est non
Une collection ne permet pas d’appliquer un lambda sur
ses éléments
Un Stream le peut
Comment construire un Stream ?
De nombreuses façons de faire…
1) À partir d’une collection :
Collection<String> collection = ... ; Stream<String> stream = collection.stream() ;
Comment construire un Stream ?
De nombreuses façons de faire…
1) À partir d’une collection :
2) À partir d’un tableau :
Stream<String> stream2 = Arrays.stream(new String [] {"one", "two", "three"}) ;
Comment construire un Stream ?
De nombreuses façons de faire…
1) À partir d’une collection :
2) À partir d’un tableau :
3) À partir des méthodes factory de Stream
Stream<String> stream1 = Stream.of("one", "two", "three") ;
Comment construire un Stream ?
Encore quelques patterns :
Stream.empty() ; // Stream vide Stream.of(T t) ; // un seul élément Stream.generate(Supplier<T> s) ; Stream.iterate(T seed, UnaryOperator<T> f) ;
Comment construire un Stream ?
Encore d’autres façons :
string.chars() ; // retourne un IntStream lineNumberReader.lines() ; // retourne un Stream<String> random.ints() ; // retourne un IntStream
Un premier exemple
Retour sur notre map / filter / reduce
// map / filter / reduce pattern on collections int sum = persons.stream() .map(p -> p.getAge()) .filter(a -> a > 20) .reduce(0, (a1, a2) -> a1 + a2) ;
Construction d’un
Stream sur une List
Un premier exemple
Retour sur notre map / filter / reduce
// map / filter / reduce pattern on collections int sum = persons.stream() .map(p -> p.getAge()) .filter(a -> a > 20) .reduce(0, (a1, a2) -> a1 + a2) ;
Déclarations
Un premier exemple
Retour sur notre map / filter / reduce
// map / filter / reduce pattern on collections int sum = persons.stream() .map(p -> p.getAge()) .filter(a -> a > 20) .reduce(0, (a1, a2) -> a1 + a2) ;
On lance le
calcul
Un premier exemple
Retour sur notre map / filter / reduce
// map / filter / reduce pattern on collections int sum = persons.stream() .map(p -> p.getAge()) .filter(a -> a > 20) .reduce(0, (a1, a2) -> a1 + a2) ;
Valeur par défaut, dans le
cas d’un stream vide
Deux types d’opérations
On peut donc déclarer des opérations sur un Stream
Deux types d’opérations :
1) Les opérations intermédiaires
Exemple : map, filter
Deux types d’opérations
On peut donc déclarer des opérations sur un Stream
Deux types d’opérations :
1) Les opérations intermédiaires
Exemple : map, filter
2) Les opérations terminales, qui déclenchent le traitement
Exemple : reduce
Un Stream possède un état
Les implémentations de Stream possèdent un état :
- SIZED = le cardinal du Stream est connu
- ORDERED = l’ordre du Stream est important (List)
- DISTINCT = pas de doublon dans le Stream (Set)
- SORTED = les Stream est trié (SortedSet)
Un Stream possède un état
Certaines opérations changent cet état
- le filtrage annule SIZED
- le mapping annule DISTINCT et SORTED
Un Stream possède un état
Et cela permet d’optimiser !
- un HashSet ne peut pas avoir de doublons…
Un Stream possède un état
Et cela permet d’optimiser !
- un HashSet ne peut pas avoir de doublons…
- donc distinct() ne fait rien (NOP) pour un stream
construit sur un HashSet
Un Stream possède un état
Et cela permet d’optimiser !
- un HashSet ne peut pas avoir de doublons…
- donc distinct() ne fait rien (NOP) pour un stream
construit sur un HashSet
- un TreeSet est trié, donc…
Un Stream possède un état
Et cela permet d’optimiser !
- un HashSet ne peut pas avoir de doublons…
- donc distinct() ne fait rien (NOP) pour un stream
construit sur un HashSet
- un TreeSet est trié, donc…
- sort() ne fait rien pour un stream construit sur un
TreeSet
Un 2ème exemple
Trions une liste de chaîne de caractères
List<String> strings = new ArrayList<>() ; // on remplit la liste // et plus loin dans le code on a ça : List<String> result = strings.stream() .sorted() .collect(Collector.toList()) ;
Un 2ème exemple
Trions une liste de chaîne de caractères
// un jour un petit malin change l’implémentation SortedSet<String> strings = new TreeSet<>() ; // on remplit la liste // et plus loin dans le code on a ça : List<String> result = strings.stream() .sorted() .collect(Collector.toList()) ;
Un 2ème exemple
Trions une liste de chaîne de caractères
Dans ce cas le code de tri n’est plus exécuté !
// un jour un petit malin change l’implémentation SortedSet<String> strings = new TreeSet<>() ; // on remplit la liste // et plus loin dans le code on a ça : List<String> result = strings.stream() .sorted() .collect(Collector.toList()) ;
Opérations stateless / statefull
Opérations Stateless / statefull
Certaines opérations sont stateless :
Cela signifie que l’on n’a pas besoin de plus d’information
que ce qui est contenu dans p
persons.stream().map(p -> p.getAge()) ;
Opérations stateless / statefull
Opérations Stateless / statefull
Certaines opérations sont stateless :
Pas le cas de toutes les opérations
persons.stream().map(p -> p.getAge()) ;
stream.limit(10_000_000) ; // select les premiers 10M éléments
Exemple
Trions un tableau de String
Random rand = new Random() ; String [] strings = new String[10_000_000] ; for (int i = 0 ; i < strings.length ; i++) { strings[i] = Long.toHexString(rand.nextLong()) ; }
Exemple
Trions un tableau de String
Soooo Java 7…
Random rand = new Random() ; String [] strings = new String[10_000_000] ; for (int i = 0 ; i < strings.length ; i++) { strings[i] = Long.toHexString(rand.nextLong()) ; }
Exemple
Trions un tableau de String
Meilleur ?
Random rand = new Random() ; Stream<String> stream = Stream.generate( () -> Long.toHexString(rand.nextLong()) ) ;
Exemple
Trions un tableau de String
Meilleur !
// Random rand = new Random() ; Stream<String> stream = Stream.generate( () -> Long.toHexString(ThreadLocalRandom.current().nextLong()) ) ;
Exemple
Trions un tableau de String
// other way Stream<String> stream = ThreadLocalRandom .current() .longs() // returns a LongStream .mapToObj(l -> Long.toHexString(l)) ;
Exemple
Trions un tableau de String
// other way Stream<String> stream = ThreadLocalRandom .current() .longs() .mapToObj(Long::toHexString) ;
Exemple
Trions un tableau de String
// other way Stream<String> stream = ThreadLocalRandom .current() .longs() .mapToObj(Long::toHexString) .limit(10_000_000) .sorted() ;
T = 4 ms
Exemple
Trions un tableau de String
// other way Stream<String> stream = ThreadLocalRandom .current() .longs() .mapToObj(Long::toHexString) .limit(10_000_000) .sorted() ; Object [] sorted = stream.toArray() ;
Exemple
Trions un tableau de String
// other way Stream<String> stream = ThreadLocalRandom .current() .longs() .mapToObj(Long::toHexString) .limit(10_000_000) .sorted() ; Object [] sorted = stream.toArray() ;
T = 14 s
Example
Trions un tableau de String
// other way Stream<String> stream = ThreadLocalRandom .current() .longs() .mapToObj(Long::toHexString) .limit(10_000_000) .sorted() ; Object [] sorted = stream.toArray() ;
T = 14 s
Opérations
intermédiares !
Au final
1) Il y a des opérations intermédiaires, et des opérations
terminales
2) Seule une opération terminale déclenche les
traitements
Au final
1) Il y a des opérations intermédiaires, et des opérations
terminales
2) Seule une opération terminale déclenche les
traitements
3) Une seule opération terminale est autorisée
4) Un Stream ne peut traité qu’une seule fois
Si besoin, un autre Stream doit être construit
Parallel Streams
Optimisation
La première optimisation (après l’exécution lazy) est le
parallélisme
Fork / join permet la programmation parallèle depuis le
JDK 7
Écrire du code parallèle reste complexe, et ne mène pas
toujours à de meilleures performances
Optimisation
La première optimisation (après l’exécution lazy) est le
parallélisme
Fork / join permet la programmation parallèle depuis le
JDK 7
Écrire du code parallèle reste complexe, et ne mène pas
toujours à de meilleures performances
Utiliser l’API Stream est plus simple et plus sûr
Construction d’un Stream parallèle
Deux patterns
1) Appeler parallelStream() au lieu de stream()
2) Appeler parallel() sur un stream existant
Stream<String> s = strings.parallelStream() ;
Stream<String> s = strings.stream().parallel() ;
Peut-on choisir le nombre cœurs ?
La réponse est non
Peut-on choisir le nombre cœurs ?
La réponse est non
Mais… en a-t-on vraiment besoin ?
Paralléliser est-il aussi simple ?
En fait oui !
Paralléliser est-il aussi simple ?
En fait oui !
… et non…
Paralléliser est-il aussi simple ?
Exemple 1 :
« retourne les premiers 10M éléments »
persons.stream().limit(10_000_000) ;
Paralléliser est-il aussi simple ?
Exemple 1 :
« retourne les premiers 10M éléments »
persons.parallelStream().limit(10_000_000) ;
Paralléliser est-il aussi simple ?
Exemple 1 :
« retourne les premiers 10M éléments »
Comment peut-on tracer que les éléments sont les
premiers sur un CPU multicœur ?
persons.parallelStream().limit(10_000_000) ;
Paralléliser est-il aussi simple ?
Exemple 1 : performances
Code 1
List<Long> list = new ArrayList<>(10_000_100) ; for (int i = 0 ; i < 10_000_000 ; i++) { list1.add(ThreadLocalRandom.current().nextLong()) ; }
Paralléliser est-il aussi simple ?
Exemple 1 : performances
Code 2
Stream<Long> stream = Stream.generate(() -> ThreadLocalRandom.current().nextLong()) ; List<Long> list1 = stream.limit(10_000_000).collect(Collectors.toList()) ;
Paralléliser est-il aussi simple ?
Exemple 1 : performances
Code 3
Stream<Long> stream = ThreadLocalRandom.current().longs(10_000_000).mapToObj(Long::new) ; List<Long> list = stream.collect(Collectors.toList()) ;
Paralléliser est-il aussi simple ?
Exemple 1 : performances
Série Parallèle
Code 1 (for) 270 ms
Code 2 (limit) 310 ms
Code 3 (longs) 250 ms
Paralléliser est-il aussi simple ?
Exemple 1 : performances
Série Parallèle
Code 1 (for) 270 ms
Code 2 (limit) 310 ms 500 ms
Code 3 (longs) 250 ms 320 ms
Paralléliser est-il aussi simple ?
Exemple 2 :
« retourne un Stream composé des éléments du premier
Stream, puis des éléments du second »
Stream s3 = Stream.concat(stream1, stream2) ;
Parallélisme
Le parallélisme implique des calculs supplémentaires la
plupart du temps
Des opérations mal configurées vont entraîner des calculs
inutiles, qui vont affaiblir les performances globales
Exemple : un stream ORDERED est plus complexe à
traiter
Réductions
Qu’est-ce qu’une réduction ?
2 types de réduction :
1) La réduction
« A reduction operation (also called a fold) takes a
sequence of input elements and combines them
into a single summary result by repeated
application of a combining operation »
Qu’est-ce qu’une réduction ?
2 types de réduction :
2) La réduction mutable
« A mutable reduction operation accumulates
input elements into a mutable result container,
such as a Collection or StringBuilder, as it
processes the elements in the stream »
Une réduction simple
La somme des âges
// map / filter / reduce pattern on collections int sum = persons.stream() .map(p -> p.getAge()) .filter(a -> a > 20) .reduce(0, (a1, a2) -> a1 + a2) ;
Une réduction simple
Ca serait bien de pouvoir écrire :
Mais sum() n’est pas définie sur Stream<T>
Que voudrait dire « additionner des personnes » ?
// map / filter / reduce pattern on collections int sum = persons.stream() .map(p -> p.getAge()) .filter(a -> a > 20) .sum() ;
Une réduction simple
Ca serait bien de pouvoir écrire :
Mais sum() n’est pas définie sur Stream<T>
Mais il y a une méthode sum() sur IntStream !
// map / filter / reduce pattern on collections int sum = persons.stream() .map(p -> p.getAge()) .filter(a -> a > 20) .sum() ;
Une réduction simple
2ème version :
Résultat pour une liste vide : 0
// map / filter / reduce pattern on collections int sum = persons.stream() .map(Person::getAge) .filter(a -> a > 20) .mapToInt(Integer::intValue) .sum() ;
Une réduction simple
2ème version (encore meilleure) :
Résultat pour une liste vide : 0
// map / filter / reduce pattern on collections int sum = persons.stream() .mapToInt(Person::getAge) .filter(a -> a > 20) // .mapToInt(Integer::intValue) .sum() ;
Une réduction simple
Qu’en est-il de min() et de max() ?
Quelle valeur par défaut pour max() ?
// map / filter / reduce pattern on collections ....... = persons.stream() .mapToInt(Person::getAge) .filter(a -> a > 20) .max() ;
Problème des valeurs par défaut
La notion de « valeur par défaut » est plus complexe qu’il y
paraît…
Problème des valeurs par défaut
La notion de « valeur par défaut » est plus complexe qu’il y
paraît…
1) La « valeur par défaut » est la réduction de l’ensemble
vide
Problème des valeurs par défaut
La notion de « valeur par défaut » est plus complexe qu’il y
paraît…
1) La « valeur par défaut » est la réduction de l’ensemble
vide
2) Mais aussi l’élément neutre de la réduction
Problème des valeurs par défaut
La notion de « valeur par défaut » est plus complexe qu’il y
paraît…
1) La « valeur par défaut » est la réduction de l’ensemble
vide
2) Mais aussi l’élément neutre de la réduction
Problème des valeurs par défaut
Problème : max() and min() n’ont pas d’élément neutre
ie : un élément e pour lequel max(e, a) = a
Problème des valeurs par défaut
Problème : max() and min() n’ont pas d’élément neutre
ie : un élément e pour lequel max(e, a) = a
Ca ne peut pas être 0 : max(-1, 0) = 0
−∞ n’est pas un entier
Problème des valeurs par défaut
Donc quelle est la valeur par défaut de max() et min() ?
Problème des valeurs par défaut
Donc quelle est la valeur par défaut de max() et min() ?
Réponse : il n’y a pas de valeur par défaut pour max() et
pour min()
Problème des valeurs par défaut
Donc quel type choisir pour la méthode max() ?
// map / filter / reduce pattern on collections ....... = persons.stream() .mapToInt(Person::getAge) .filter(a -> a > 20) .max() ;
Problème des valeurs par défaut
Donc quel type choisir pour la méthode max() ?
Si c’est int, la valeur par défaut sera 0…
// map / filter / reduce pattern on collections ....... = persons.stream() .mapToInt(Person::getAge) .filter(a -> a > 20) .max() ;
Optionals
Sans valeur par défaut, il faut trouver autre chose…
// map / filter / reduce pattern on collections OptionalInt optionalMax = persons.stream() .mapToInt(Person::getAge) .filter(a -> a > 20) .max() ;
« il peut ne pas y avoir
de résultat »
Optionals
Que peut-on faire avec OptionalInt ?
1er pattern : tester s’il contient une valeur
OptionalInt optionalMax = ... ; int max ; if (optionalMax.isPresent()) { max = optionalMax.get() ; } else { max = ... ; // décider d’une « valeur par défaut » }
Optionals
Que peut-on faire avec OptionalInt ?
2ème pattern : lire la valeur ou jeter une exception
OptionalInt optionalMax = ... ; // throws NoSuchElementException if no held value int max = optionalMax.getAsInt() ;
Optionals
Que peut-on faire avec OptionalInt ?
3éme pattern : lire la valeur / retourner une valeur par défaut
OptionalInt optionalMax = ... ; // get 0 if no held value int max = optionalMax.orElse(0) ;
Optionals
Que peut-on faire avec OptionalInt ?
4ème pattern : lire la valeur ou jeter une exception
OptionalInt optionalMax = ... ; // exceptionSupplier will supply an exception, if no held value int max = optionalMax.orElseThrow(exceptionSupplier) ;
Available Optionals
Optional<T>
OptionalInt, OptionalLong, OptionalDouble
Réductions disponibles
Sur Stream<T> :
- reduce()
- count(), min(), max()
- anyMatch(), allMatch(), noneMatch()
- findFirst(), findAny()
- toArray()
- forEach(), forEachOrdered()
Réductions disponibles
Sur IntStream, LongStream, DoubleStream :
- average()
- summaryStatistics()
Mutable reductions : exemple 1
Utilisation d’une classe helper : Collectors
ArrayList<String> strings = stream .map(Object::toString) .collect(Collectors.toList()) ;
Mutable reductions : exemple 2
Concaténation de String avec un helper
String names = persons .stream() .map(Person::getName) .collect(Collectors.joining()) ;
Mutable reductions
Une réduction mutable dépend :
- d’un container : Collection or StringBuilder
- d’un moyen d’ajouter un élément au container
- d’un moyen de fusionner deux containers (utilisé dans
les opérations parallèles)
Mutable reductions
La forme générale a donc besoin de 3 objets :
- un supplier : construit une instance du container
Supplier<ArrayList<String>> supplier = () -> new ArrayList<String>() ;
Mutable reductions
La forme générale a donc besoin de 3 objets :
- un supplier : construit une instance du container
- un accumulateur : ajoute un élément au container
BiConsumer<ArrayList<String>, ? super String> accumulator = (suppliedList, s) -> suppliedList.add(s) ;
Mutable reductions
La forme générale a donc besoin de 3 objets :
- un supplier : construit une instance du container
- un accumulateur : ajoute un élément au container
- un combiner : fusionne deux containers
BiConsumer<ArrayList<String>, ArrayList<String>> combiner = (supplied1, supplied2) -> supplied1.addAll(supplied2) ;
Mutable reductions : exemple 1
D’où le code :
ArrayList<String> strings = stream .map(Object::toString) .collect( () -> new ArrayList<String>(), // the supplier (suppliedList, s) -> suppliedList.add(s), // the accumulator (supplied1, supplied2) -> supplied1.addAll(supplied2) // the combiner ) ;
Mutable reductions : exemple 1
D’où le code :
ArrayList<String> strings = stream .map(Object::toString) .collect( ArrayList::new, // the supplier ArrayList::add, // the accumulator ArrayList::addAll // the combiner ) ;
Collectors
La classe Collectors
Une boite à outils (37 méthodes) pour la plupart des types
de réduction
- counting, minBy, maxBy
- summing, averaging, summarizing
- joining
- toList, toSet
Et
- mapping, groupingBy, partionningBy
La classe Collectors
Average, Sum, Count
persons .stream() .collect(Collectors.averagingDouble(Person::getAge)) ;
persons .stream() .collect(Collectors.counting()) ;
La classe Collectors
Concaténation des nom dans une String
String names = persons .stream() .map(Person::getName) .collect(Collectors.joining(", ")) ;
La classe Collectors
Accumulation dans un Set
Set<Person> setOfPersons = persons .stream() .collect( Collectors.toSet()) ;
La classe Collectors
Accumulation dans une collection passée en paramètre
TreeSet<Person> treeSetOfPersons = persons .stream() .collect( Collectors.toCollection(TreeSet::new)) ;
La classe Collectors
Calculer un max avec un comparateur
Bonus : une API Comparator
Optional<Person> optionalPerson = persons .stream() .collect( Collectors.maxBy( Comparator.comparing(Person::getAge)) ;
Construction de comparateurs
Nouvelle API pour construire des comparateurs
Comparator<Person> comp = Comparator.comparing(Person::getLastName) .thenComparing(Person::getFirstName) .thenComparing(Person:getAge) ;
Classe Collectors : mapping
La méthode mapping() prend 2 paramètres
- une fonction, qui mappe les éléments du Stream
- un collecteur, appelé « downstream », appliqué aux
valeurs mappées
Classe Collectors : mapping
Accumuler les noms des personnes dans un Set
Set<String> set = persons .stream() .collect( Collectors.mapping( Person::getLastName, Collectors.toSet())) ;
Classe Collectors : mapping
Mapper le stream, accumulation dans une collection
TreeSet<String> set = persons .stream() .collect( Collectors.mapping( Person::getLastName, Collectors.toCollection(TreeSet::new)) ) ;
Classe Collectors : groupingBy
« Grouping by » construit des tables de hachage
- méthode de construction des clés
- par défaut les éléments sont rangés dans une liste
- on peut spécifier un downstream (collector)
Classe Collectors : groupingBy
Des personnes par âge
Map<Integer, List<Person>> map = persons .stream() .collect( Collectors.groupingBy(Person::getAge)) ;
Classe Collectors : groupingBy
… rangées dans des Set
Map<Integer, Set<Person>> map = persons .stream() .collect( Collectors.groupingBy( Person::getAge, Collectors.toSet() // le downstream ) ;
Classe Collectors : groupingBy
… on ne garde que les noms
Map<Integer, Set<String>> map = persons .stream() .collect( Collectors.groupingBy( Person::getAge, Collectors.mapping( // Person::getLastName, // the downstream Collectors.toSet() // ) ) ;
Classe Collectors : groupingBy
… les noms rangés dans des TreeSet
Map<Integer, TreeSet<String>> map = persons .stream() .collect( Collectors.groupingBy( Person::getAge, Collectors.mapping( Person::getLastName, Collectors.toCollection(TreeSet::new) ) ) ;
Classe Collectors : groupingBy
… etc… etc… etc…
TreeMap<Integer, TreeSet<String>> map = persons .stream() .collect( Collectors.groupingBy( Person::getAge, TreeMap::new, Collectors.mapping( Person::getLastName, Collectors.toCollection(TreeSet::new) ) ) ;
Classe Collectors : groupingBy
Exemple : création d’un histogramme des âges
Donne le nombre de personnes par âge
Map<Integer, Long> map = persons .stream() .collect( Collectors.groupingBy(Person::getAge, Collectors.counting()) ) ;
Classe Collectors : partionningBy
Crée une Map<Boolean, …> à partir d’un prédicat
- la table a deux clés : TRUE et FALSE
- et on peut y ajouter un downstream
Classe Collectors : partionningBy
Crée une Map<Boolean, …> avec un prédicat
map.get(TRUE) retourne la liste des personnes de plus de
20 ans
Map<Boolean, List<Person>> map = persons .stream() .collect( Collectors.partitioningBy(p -> p.getAge() > 20) ) ;
Classe Collectors : partionningBy
On peut définir d’autres traitements
Map<Boolean, TreeSet<String>> map = persons .stream() .collect( Collectors.partitioningBy( p -> p.getAge() > 20, Collectors.mapping( Person::getLastName, Collectors.toCollection(TreeSet::new)) ) ) ) ;
Classe Collectors : collectingAndThen
Collecte les données avec un downstream
Applique enfin une fonction appelée « finisher »
Indispensable pour retourner des collections immutables
Classe Collectors : collectingAndThen
Dans ce cas « Map::entrySet » est un finisher
Set<Map.Entry<Integer, List<Person>>> set = persons .stream() .collect( Collectors.collectingAndThen( Collectors.groupingBy( Person::getAge), // downstream, construit une map Map::entrySet // finisher, appliqué à la map ) ;
Quelques exemples réels
1er exemple
Optional<Entry<Integer, Long>> opt = movies.stream().parallel() .collect( Collectors.collectingAndThen( Collectors.groupingBy( movie -> movie.releaseYear(), Collectors.counting() ), Map::entrySet ) ) .stream() .max(Map.Entry.comparingByValue()) ;
1er exemple
Optional<Entry<Integer, Long>> opt = movies.stream().parallel() .collect( Collectors.collectingAndThen( Collectors.groupingBy( movie -> movie.releaseYear(), Collectors.counting() ), Map::entrySet ) ) .stream() .max(Map.Entry.comparingByValue()) ;
Un stream de movies
1er exemple
Optional<Entry<Integer, Long>> opt = movies.stream().parallel() .collect( Collectors.collectingAndThen( Collectors.groupingBy( movie -> movie.releaseYear(), Collectors.counting() ), Map::entrySet ) ) .stream() .max(Map.Entry.comparingByValue()) ;
Construction d’une map année / # de films
1er exemple
Optional<Entry<Integer, Long>> opt = movies.stream().parallel() .collect( Collectors.collectingAndThen( Collectors.groupingBy( movie -> movie.releaseYear(), Collectors.counting() ), Map::entrySet ) ) .stream() .max(Map.Entry.comparingByValue()) ;
Construction de l’EntrySet
1er exemple
Optional<Entry<Integer, Long>> opt = movies.stream().parallel() .collect( Collectors.collectingAndThen( Collectors.groupingBy( movie -> movie.releaseYear(), Collectors.counting() ), Map::entrySet ) ) .stream() .max(Map.Entry.comparingByValue()) ;
Et déterminer la plus grande valeur
1er exemple
Optional<Entry<Integer, Long>> opt = movies.stream().parallel() .collect( Collectors.collectingAndThen( Collectors.groupingBy( movie -> movie.releaseYear(), Collectors.counting() ), Map::entrySet ) ) .stream() .max(Map.Entry.comparingByValue()) ;
Retourne l’année qui a vu le plus grand nombre de films
Remarques sur parallel()
Traitement parallèle
Deux façons de le faire :
- créer un stream parallèle d’entrée
- Appeler parallel() sur un stream donné, on peut aussi
appeler sequential()
Quand une opération terminale est appelée, l’ensemble
des opérations est exécuté en parallèle ou pas, en fonction
du mode du stream
Les choses qui ne marchent pas
Certaines opérations ne donnent pas des résultats
reproductibles en parallèle, ex : findAny()
On ne doit pas modifier la source durant le traitement, si on
le fait les résultats ne sont pas prévisibles, même si la
source est une collection concurrente
Les choses qui marchent pas, mais…
Le traitement parallèle doit être le moins contraint possible
pour être efficace
- ORDERED est une contrainte coûteuse
- collect() doit utiliser des structures concurrentes
Conclusion
Conclusion
Pourquoi les lambdas ont-ils été introduits dans Java 8 ?
Conclusion
Pourquoi les lambdas ont-ils été introduits dans Java 8 ?
Réponse : parce que c’est à la mode !
Conclusion
Pourquoi les lambdas ont-ils été introduits dans Java 8 ?
Réponse : parce que c’est à la mode !
Conclusion
Pourquoi les lambdas ont-ils été introduits dans Java 8 ?
Réponse : parce que c’est à la mode !
Parce que le code écrit est plus compact !
Plus c’est court, plus c’est bon !
Un exemple de code compact
#include "stdio.h" main() { int b=0,c=0,q=60,_=q;for(float i=-20,o,O=0,l=0,j,p;j=O*O,p=l*l, (!_--|(j+p>4)?fputc(b?q+(_/3):10,(i+=!b,p=j=O=l=0,c++,stdout)), _=q:l=2*O*l+i/20,O=j-p+o),b=c%q,c<2400;o=-2+b*.05) ; }
http://www.codeproject.com/Articles/2228/Obfuscating-your-Mandelbrot-code
Un exemple de code compact
#include "stdio.h" main() { int b=0,c=0,q=60,_=q;for(float i=-20,o,O=0,l=0,j,p;j=O*O,p=l*l, (!_--|(j+p>4)?fputc(b?q+(_/3):10,(i+=!b,p=j=O=l=0,c++,stdout)), _=q:l=2*O*l+i/20,O=j-p+o),b=c%q,c<2400;o=-2+b*.05) ; }
http://www.codeproject.com/Articles/2228/Obfuscating-your-Mandelbrot-code
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++ +++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++ ++ +++++++++++ +++++++++++++++++++++++++++++++++++ +++++++ ++++++++++++++++++++++++++++++++++++ +++++++ +++++++++++++++++++++++++++++++++++ ++++++++ ++++++++++++++++++++++++++++++++++ +++++++ +++++++++++++++++++++++++++++++++ +++++ +++++++++++++++++++++++++++++++++ ++++++ +++++++++++++++++++++++ + +++++ ++++++ +++++++++++++++++++++++ ++ ++++++ ++++++++++++++++++++++ + ++++++ ++++++++++++++++++++++ + ++++++ ++++++++++++++++++++ + + +++++++ ++++++ ++++++++ ++++++++++++++++++++ + + +++++++ ++++++++++++++++++++++ + ++++++ ++++++++++++++++++++++ + ++++++ +++++++++++++++++++++++ ++ ++++++ +++++++++++++++++++++++ + +++++ ++++++ +++++++++++++++++++++++++++++++++ ++++++ +++++++++++++++++++++++++++++++++ +++++ ++++++++++++++++++++++++++++++++++ +++++++ +++++++++++++++++++++++++++++++++++ ++++++++ ++++++++++++++++++++++++++++++++++++ +++++++ +++++++++++++++++++++++++++++++++++ +++++++ ++++++++++++++++++++++++++++++++++++ ++ +++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++ +++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Plus c’est court, plus c’est bon !
Conclusion
Pourquoi les lambdas ont-ils été introduits dans Java 8 ?
Réponse : parce que c’est à la mode !
Parce que le code écrit est plus compact !
Conclusion
Pourquoi les lambdas ont-ils été introduits dans Java 8 ?
Parce que les lambdas autorisent des nouveaux patterns
qui permettent de paralléliser les traitements simplement et
de façon sûre
- dont les applications ont besoin
- dont concepteurs d’API ont besoin
Conclusion
Java 8 arrive, il s’agit de la mise à jour la plus importante
depuis 15 ans que Java existe
Conclusion
Java 8 arrive, il s’agit de la mise à jour la plus importante
depuis 15 ans que Java existe
Migrer vers Java 8 va nécessiter du travail pour nous les
développeurs
- auto-formation
- changement de nos habitudes de travail
Conclusion
Java 8 arrive, il s’agit de la mise à jour la plus importante
depuis 15 ans que Java existe
Migrer vers Java 8 va nécessiter du travail pour nous les
développeurs
- auto-formation
- changement de nos habitudes de travail
- convaincre nos boss…
Conclusion
Java 8 arrive, il s’agit de la mise à jour la plus importante
depuis 15 ans que Java existe
Téléchargeons la version développeur sur openjdk.net
Date de sortie : 18 mars 2014
Conclusion
Java 8 arrive, il s’agit de la mise à jour la plus importante
depuis 15 ans que Java existe
« Java is a blue collar language. It’s not PhD thesis
material but a language for a job » – James Gosling, 1997
Conclusion
Java 8 arrive, il s’agit de la mise à jour la plus importante
depuis 15 ans que Java existe
« Language features are not a goal unto themselves;
language features are enablers, encouraging or
discouraging certain styles and idioms » – Brian Goetz,
2013
Conclusion
Java 8 arrive, il s’agit de la mise à jour la plus importante
depuis 15 ans que Java existe
La bonne nouvelle :
Java est toujours Java !