Rossi Oddet

Blog d'un artisan développeur

Soirée JUG Nantes : Java 8 -> Lambdas, Streams Et Collectors (Partie 2)

La première partie de cet article est accessible ici.

La deuxième partie de cette soirée du JUG Nantes est consacrée à une présentation de Java 8 animée par José Paumard.

Avant de rentrer dans le coeur de la présentation de José, faisons un tour de ce que réserve Java 8.

Java 8 -> quoi de neuf ?

Java 8 est une évolution majeure du langage Java.

Les nouveautés peuvent être classées suivant plusieurs catégories pour les plus significatives :

  • Nouveaux Projets -> les nouvelles fonctionnalités assez structurantes pour être considérées comme des projets à part.
  • Machine Virtuelle -> des modifications du fonctionnement de la JVM et des travaux d’amélioration des performances.
  • Core -> nouveautés non structurantes du langage (ajout d’API, d’annotations, etc.).

La liste complète des nouveautés : ici.

Voici quelques nouveautés qui ont attiré mon attention.

Java 8 -> Nouveaux Projets

Deux projets :

  • Le projet Lambda qui ajoute les closures au langage Java. Il s’agit certainement de la fonctionnalité qui a le plus d’impact sur le langage. C’est le sujet principal de cette soirée.
  • Le projet Nashorn qui offre un moteur d’exécution Javascript sur la JVM.

Java 8 -> Machine Virtuelle

L’évolution la plus remarquable est la suppression de la Permanent Generation. Cette zone était utilisée par la JVM pour stocker les définitions des classes, méthodes, etc. Elle était bien distincte de la Heap qui contient les instances des objets créées.

Par défaut, la Permanent Generation a la taille maximale de 64MB. Il est possible de modifier cette valeur avec le paramètre -XX:MaxPermSize.

Si vous avez le plaisir de travailler sur application usine (nombreuses classes, nombreuses librairies), habituelle du milieu professionnel, vous avez peut-être déjà rencontré une erreur qui ressemble à cela :

1
2
3
java.lang.OutOfMemoryError: PermGen space
        at java.lang.ClassLoader.defineClass1(Native Method)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:620)

Avec Java 8, dites adieu à cette erreur et au paramétrage lié à la Permanent Generation.

Cette évolution de la machine virtuelle est un des résultats de la convergence entre la machine virtuelle propriétaire d’Oracle JRockit et celle de la communauté : HotSpot. En effet, JRockit est distribué avec le serveur d’application propriétaire d’Oracle WebLogic et il a la particularité de ne pas avoir de Permanent Generation. Un bon signe pour la communauté ?

Java 8 -> Core

Quelques améliorations sont apportées au coeur du JDK.

Une application JavaFX devient exécutable (JEP 153)

Avant Java 8, pour rendre une application JavaFX exécutable depuis un JAR, il fallait faire deux choses :

  • Créer une classe avec la méthode public static void main(String[] args) qui contient du code d’initialisation de l’application JavaFX.
  • Référencer cette classe dans l’attribut Main-Class du Manifest du JAR.

Avec Java 8, il est possible de créer un JAR exécutable JavaFX sans passer par une classe avec une méthode main.

Des annotations sur les types Java (JEP 104)

Java 8 permet d’écrire ce genre de chose :

1
2
3
4
5
6
7
8
9
10
MaClasse m = new @Interned MaClasse();

// ou encore
String str = (@NotNull String) element;

// ou encore 
public class MaClasse<T> implements @ReadOnly List<@ReadOnly T>

// ou encore
public void monitorTemperature() throws @Critical TemperatureException {...}

La documentation officielle en version béta : ici.

Répéter des annotations (JEP 120)

Avec Java 8, il est possible d’écrire :

1
2
3
@Alert(role="Manager")
@Alert(role="Administrator")
public class UnauthorizedAccessException extends SecurityException { ... }

Plus de détails, ici.

Accéder au nom des paramètres au runtime (JEP 118)

L’idée ici est de stocker dans le bytecode les noms des paramètres de méthodes ainsi que leurs types. Cette fonctionnalité pourrait apporter un vrai plus aux développeurs de librairies.

Par exemple, l’annotation @PathParam de JAX-RS pourrait être optionnelle avec un reconnaissance du nom du paramètre ;)

Une nouvelle API pour les dates et les heures (JEP 150)

Java 8 vient avec une nouvelle API pour les dates/heures qui se veut plus clean, fluent, immutable et extensible.

Un aperçu d’utilisation :

1
2
LocalDate aujourdhui = LocalDate.now();
LocalDate dans2ansMoins4jours = LocalDate.now().plusYears(2).minusDays(4);

Documentation officielle en version béta : ici.

Trie en parallèle des tableaux (JEP 103)

Des méthodes sont ajoutées à la classe java.util.Arrays :

1
Arrays.parallelSort(...);

Revenons au JUG !

Les nantais ont répondu présent pour découvrir Java 8 et pour José, c’est la session qui clôture son tour de Bretagne… Ou plutôt de l’ouest de la France pour les plus susceptibles ;)

José va faire un zoom sur les expressions Lambda et ses impacts sur le JDK 8.

Une introduction aux expressions Lambda

José va illustrer les expressions Lambda à l’aide du pattern Map - Filter - Reduce. Il va montrer une implémentation en Java 7 et une équivalence en Java 8 avec les expressions Lambda.

Illustration -> Approche impérative (avant Java 8) vs Approche fonctionnel (SQL)

Illustration -> Passage d’un code Java 7 à un code Java 8 avec une expression Lambda.

Syntaxes d’une expression Lambda

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mapper = (Person person) -> person.getAge();

mapper = person -> person.getAge(); // sans préciser le type

mapper = Person::getAge; // méthode non statique

sum = Integer::max; // méthode statique

mapper = (Person person) -> {
  System.out.println("Mapping " + person);
  return person.getAge();
}

reducer = (int i1, int i2) -> {
  return i1 + i2;
}

reducer = (int i1, int i2) -> i1 + i2;

reducer = (i1, i2) -> i1 + i2; // sans préciser les types

La notion d’interface fonctionnelle

Une expression Lambda peut être définie comme une instance d’une interface dite fonctionnelle. Une interface fonctionnelle est une interface avec une seule méthode abstraite.

L’annotation @FunctionalInterface peut être utilisée pour désigner une interface fonctionnelle. Elle est optionnelle et comme l’annotation @Override, elle ne sert qu’à apporter une sécurité supplémentaire à la compilation. Une interface annotée avec @FunctionalInterface ne compilera pas si elle possède plusieurs méthodes sans implémentation par défaut.

Un exemple d’utilisation dans le JDK 8.

1
2
3
4
5
6
package java.util.function;

@FunctionalInterface
public interface IntSupplier {
    int getAsInt();
}

Le nouveau package java.util.function

Il contient des interfaces fonctionnelles usuelles utilisées pour représenter des expressions Lambda en entrée et sortie de méthode.

Les interfaces Supplier, Consumer, BiConsumer, Function, BiFunction, Predicate, BiPredicate seront expliquées pendant la présentation.

Des méthodes implémentées dans les interfaces

Avec Java 8, il est possible d’exprimer une implémentation par défaut à une méthode d’une interface. C’est l’option qui a été choisie pour permettre d’ajouter des méthodes à des interfaces historiques du JDK sans modifier toutes les classes des implémentations.

Le mot clé default est utilisé pour définir une implémentation par défaut.

Exemple avec l’interface Collection :

1
2
3
4
5
6
7
8
public interface Collection<E> {
  
  ...

  default Stream<E> stream() {
      return ...;
  }
 }

Java 8 autorise aussi les méthodes statiques dans les interfaces.

1
2
3
4
5
6
public interface MonInterface {
  
  public static void main(String[] args) {
      System.out.println("Désormais, le Hello World en Java se fera avec une interface");
  }
}

L’API Stream

Un Stream est défini comme une interface paramétrée construite à partir d’une source (une collection, un tableau, une source I/O) qui permet d’appliquer une expression Lambda sur ses éléments.

Comment construire un Stream ?

1
2
3
4
5
6
7
8
9
10
11
12
13
// A partir d'une collection
Collection<String> collection = ...;
Stream<String> stream1 = collection.stream();

// A partir d'un tableau
Stream<String> stream2 = Arrays.stream(new String[]{"un", "deux", "trois"});

// A partir d'une factory de Stream 
// Stream.of, Stream.empty, Stream.generate, Stream.iterate, etc.
Stream<String> stream3 = Stream.of("un", "deux", "trois");

// A partir de quelques méthodes de classes du JDK mises à jour
IntStream stream4 = random.ints();

Un exemple d’utilisation ?

1
2
3
4
int sum = persons.stream()
  .map(p -> p.getAge())
  .filter(a -> a > 20)
  .reduce(0, (a1, a2) -> a1 + a2);

Deux types d’opérations sont applicables à un Stream : une opération dite intermédiaire et une dite terminale. Seule une opération dite terminale déclenche le traitement modélisé. Dans l’exemple précédent, map et filter sont des opérations intermédiaires et reduce une opération terminale.

Attention, un Stream ne peut être traité qu’une seule fois. Une fois une opération terminale exécutée, il est nécessaire de créer un nouveau Stream.

Illustration -> Un Stream est lazy (seule l’opération terminale déclenche le traitement) !

Stream parallèle

Il permet d’exécuter du code modélisé dans un Stream en parallèle.

Comment construire un Stream parallèle ?

1
2
3
4
5
// Appeler parallelStream() au lieu de stream()
Stream<String> s = strings.parallelStream();

// Appeler parallel() sur un stream existant
Stream<String> s = strings.stream().parallel();

Attention, un Stream parallèle ne signifie pas toujours plus performant. Le parallélisme entraine des opérations supplémentaires et ses performances sont dépendantes de la nature des opérations à exécuter. Il n’y a donc pas de recette magique de performance, la règle mesurer pour optimiser s’applique toujours.

Les Optionals

Certaines méthodes des classes du JDK 8 vont renvoyer des instances de la famille Optional : Optional, OptionalInt, OptionalLong, OptionalDouble.

Cette structure est utilisée pour modéliser une possible absence de résultat d’un traitement.

Que peut-on faire avec un Optional dans les mains ?

Un exemple avec OptionalInt :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
OptionalInt opt = ...;

// Tester s'il contient une valeur et récupérer la valeur
if(opt.isPresent()) {
  int valeur1 = opt.get();
} else {
  ...
}

// Lire la valeur ou lancer une exception NoSuchElementException s'il n'y a pas de valeur
int valeur2 = opt.getAsInt();

// Lire la valeur ou lancer une exception particulière s'il n'y a pas de valeur
int valeur3 = opt.orElseThrow(exceptionSupplier);

// Lire la valeur si elle existe sinon retourner une valeur par défaut
int valeur4 = opt.orElse(12);

Les réductions & La classe Collectors

L’API Stream donne accès à plusieurs réductions : reduce(), count(), min(), max(), findFirst(), etc.

La méthode collect applicable à un Stream permet d’appliquer des réductions complexes à partir d’un Collector. Sans rentrer dans les détails de la définition d’un Collector, la classe Collectors fournit un ensemble d’instance de Collector qui facilitent le travail du développeur.

Quelques exemples d’utilisation de la classe Collectors.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Transformer un Stream en List
List<Person> liste1 = persons.stream().collect(Collectors.toList());

// Transformer un Stream en Set
Set<String> liste2 = persons.stream().collect(Collectors.toSet());

// Transformer un Stream en TreeSet
TreeSet<String> liste3 = persons.stream().collect(Collectors.toSet(TreeSet::new));

// Concaténer les noms d'une liste de personnes
String names1 = persons.stream().map(Person::getName).collect(Collectors.joining());

// Concaténer les noms séparés par une virgule d'une liste de personnes
String names2 = persons.stream().map(Person::getName).collect(Collectors.joining(","));

// Compter le nombre de personnes
int nbPersons = persons.stream().collect(Collectors.counting());

// Moyenne des ages des personnes
double moyenneAge = persons.stream().collect(Collectors.averagingDouble(Person::getAge));

// Regroupement des personnes par age
Map<Integer, List<Person>> map = persons.stream().collect(Collectors.groupingBy(Person::getAge));

// Regroupement des personnes par age en utilisant un Set
Map<Integer, Set<Person>> map = persons.stream().collect(Collectors.groupingBy(Person::getAge,Collectors.toSet()));

// Répartir les données en 2 ensembles : true -> liste des personnes age > 20 et false -> le reste
Map<Boolean, List<Person>> map = persons.stream().collect(Collectors.partitionningBy(p -> p.getAge() > 20));

Nouvelle API pour construire des comparateurs

Avec Java 8, ça devient presqu’un plaisir de créer une instance de l’interface Comparator :

1
2
3
Comparator<Person> comp = Comparator.comparing(Person::getLastName)
                                  .thenComparing(Person::getFirstName)
                                  .thenComparing(Person::getAge);

Pourquoi des expressions Lambda ?

José explique que les expressions Lambda n’ont pas été introduites dans Java 8 parce que c’est à la mode ou parce que le code écrit est plus compact.

Les expressions Lambda apporte la possibilité d’appliquer de nouveaux patterns qui permettent de paralléliser simplement et de façon plus sûr des traitements.

Les slides

Vous y trouverez des détails que je n’ai pas abordé pour rester synthétique :

  • Des méthodes implémentées dans des interfaces entrainent la possibilité d’avoir un héritage multiple conflictuelle. Quelles sont les règles du compilateur ?
  • Des explications sur quelques classes du package java.util.function
  • Les différents états d’un Stream et leurs conséquences
  • La problématique des valeurs par défaut dans les réductions max et min
  • Une utilisation plus avancée de la classe Collectors
  • Des choses qui ne marchent pas avec le traitement parallèle

Alors, installer vous confortablement et prenez votre temps, il y a 330 slides ;)

Qu’est-ce que j’ai pensé de tout ça ?

Dans l’ensemble, j’ai trouvé cette présentation claire, progressive et surtout riche en contenu. Elle présente bien les expressions Lambda et ses impacts sur la future version du JDK.

Lambda Java 8, une révolution ?

La réponse est probablement oui. C’est la première fois :

  • qu’il est possible de déclarer des paramètres sans préciser leurs types (paramètres d’entrée d’une expression Lambda).
  • qu’une “méthode” (particulière certes) peut être passée en paramètre d’une autre.
  • que le langage fait une place aussi importante aux concepts de la programmation fonctionnelle.
  • qu’il y aura autant de changement dans le vocabulaire d’un développeur Java. Il parlera désormais avec des mots comme map, filter, reduce, Supplier, Function, BiFunction, Consumer, BiConsumer, etc. Y aura t-il une race d’intégriste qui va naitre dans la communauté Java comme les Scalafistes de Scala ? ;)

Lambda Java 8 et le debug ?

Je trouve que le langage Java a une qualité : une maintenance possible sur une grosse volumétrie de code. Même sur des applications dites legacy qui ont été développées de la pire des manières, j’ai toujours pu lancer l’application en debug, faire du pas à pas partout même dans les classes fournies par les librairies. Le côté impératif de la programmation fait clairement apparaître chaque étape du programme.

Avec Java 8, les applications vont de plus en plus ressembler à des enchainements de méthodes avec des expressions Lambda en paramètres. Ce code séduisant va probablement poser des difficultés au debuggage. Va t-il falloir mettre les expressions Lambda systématiquement sur plusieurs lignes pour permettre de faire du pas à pas ? Comment, pendant un debuggage, simuler l’exécution d’une opération sur un Stream sans avoir à en créer un nouveau ? Aurons-nous toujours cette maintenance qui peut être pénible mais toujours possible avec une application qui vieillit avec des expressions Lambda ? Peut-être tout simplement qu’il y aura moins de besoin d’utiliser un debugger ? A suivre ;)

Fini les boucles for pour les listes ?

Les boucles for servent souvent à itérer sur des listes d’éléments afin de les transformer (map), les filtrer (filter) et à faire des calculs (reduce). Les développeurs vont-ils privilégier l’utilisation des expressions Lambda ?

Lambda Java 8 vs les langages alternatifs ?

Les langages alternatifs (Scala, Groovy, etc.) de la JVM n’ont pas grand chose à envier aux expressions Lambda de Java 8, ils vont déjà beaucoup plus loin depuis plusieurs années. Ils vont cependant probablement continuer à jalouser la base énorme d’utilisateurs qui reste fidèle à Java Standard ;)

Java is a blue collar language. It’s not PhD thesis material but a language for a job.

James Gosling 1997

La date de sortie prévue de Java 8 est le 18/03/2014. Alors, impatient d’avoir Java 8 dans vos entreprises ?

Comments