JDK 8, lambdas, streams, collectors - Bretagne Tour

Preview:

DESCRIPTION

L'API la plus utilisée du JDK est sans aucun doute l'API Collection. Brillamment conçue il y a un peu plus de 15 ans, elle est encore aujourd'hui au coeur de toutes les applications Java. En 2004, elle a subi son premier lifting, avec l'introduction des génériques. Cette mise à jour, bien qu'importante, n'a cependant pas modifié ses patterns d'utilisation. Avec l'introduction des lambdas en Java 8, l'API Collection est à nouveau réécrite, mais cette fois la situation est différente : ses patterns d'utilisation sont complètement changés. La première partie de cette conférence introduit les lambda expressions, comment les écrire, et ce qu'elle nous apportent en tant que développeurs. La deuxième partir présente en détail les nouveaux patterns introduits par les API Stream et Collector. Ces nouvelles API vont changer la façon dont nous allons pouvoir traiter les collections de grande taille, y compris en parallèle, avec un modèle de programmation très simple, et des patterns très puissants. Cette puissance sera montrée dans des exemples réels, qui monteront comment Java 8 va pouvoir nous aider à écrire simplement du code efficace et performant.

Citation preview

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 !

Recommended