I.
JAVA 8
LES FONDAMENTAUX DU LANGAGE
Attia Mehdi
Table des matières I. Lambda expressions (JSR 335) ....................................................................................................... 1
I.1 Lambda Target Type ....................................................................................................................... 3
I.2 Lambda Scopes ................................................................................................................................ 4
I.3 Ce que Lambda ne peut pas faire .................................................................................................... 5
II.
Interfaces fonctionnelles .................................................................................................................. 6
II.1 Méthodes statiques et les méthodes par défaut. ....................................................................... 7
II.2 Référence de méthodes et de constructeurs ............................................................................. 8
II.3
Les interfaces fonctionnelles les plus utilisées ...................................................................... 10
III. Streams .......................................................................................................................................... 14
III.1 Obtention d’un stream ....................................................................................................... 15
III.2 Opérations sur les stream ................................................................................................... 16
IV. Date/Time API .............................................................................................................................. 31
V. Repeating annotations ................................................................................................................... 35
II. Lambda expressions (JSR 335)
Dans les versions antérieures du Java, il n'existe que deux types de références : des
références vers des valeurs primitives, et des références vers des instances d'objets. Dans
d'autres langages (Groovy, Scala, Haskell...), il est également possible d'établir des références
vers des closures, c'est-à-dire des blocs de code anonymes. Une référence de ce type peut alors,
comme toutes les autres, être utilisée en tant que champ d'une classe ou en paramètre d'une
méthode. Sous la pression des langages "alternatifs" et de la communauté Java, Oracle s'est
enfin décidé à intégrer les closures dans le langage.
Dans sa forme la plus simple, une expression lambda pourrait être représentée comme une
liste des paramètres séparés par des virgules, le symbole de → et le corps.
Lambdas expression est considérée comme la plus grosse nouveauté de Java 8, c’est une
forme de méthode, plus compacte qu’une méthode standard, pour laquelle le nom est implicite
et les paramètres peuvent être omis, tout comme leur type ou une valeur de retour. C’est une
manière simplifiée d'écrire l'implémentation d'une interface qui n'a qu'une seule méthode
abstraite (SAM).
Exemple 1 : Trier une liste de chaines de caractères
Au lieu de créer des objets anonymes toute la journée, Java 8 est livré avec une syntaxe
beaucoup plus courte et plus flexible, les expressions lambdas :
Attia Mehdi – Java 8 les fondamentaux du langage
1
Comme vous pouvez le voir le code est beaucoup plus court et plus facile à lire, et il
peut être encore plus court puisque il est possible d’omettre les types de paramètres et la valeur
de retour.
Remarque : Le type de retour d’une expression lambda est celui de la méthode abstraite de
l’interface fonctionnelle associée à cette expression.
Exemple 2 : Les expressions lambda dans les applications graphiques (GUI)
Pour traiter les événements dans une interface utilisateur graphique (GUI), telles que les
actions de clavier et les actions de la souris, vous créez généralement des gestionnaires
d'événements, ce qui implique généralement la mise en œuvre d'une interface particulière.
Souvent, les interfaces de gestionnaire d'événement sont des interfaces fonctionnelles; ont
tendance à avoir une seule méthode.
Voici une classe anonyme qui définit une action de la souris en Java FX :
L'invocation de méthode "btn.setOnAction" spécifie ce qui se passe lorsque vous
sélectionnez le bouton représenté par l'objet de "btn". Cette méthode nécessite un objet de type
"EventHandler<ActionEvent>". L’interface "EventHandler<ActionEvent>" contient une
seule méthode abstraite, "void handle(T event)" (Functional interface), donc il est possible
d’utiliser l'expression lambda mis en évidence ci-dessous pour la remplacer.
Attia Mehdi – Java 8 les fondamentaux du langage
2
I.1 Lambda Target Type
Dans d’autres langages, le type de l'expression lambda serait une fonction; mais dans
Java, les expressions lambda sont représentées comme des objets, donc ils doivent avoir un type
d'objet, particulier connu sous le nom d'une interface fonctionnelle. Ce qu'on appelle le type
de cible "target type".
Puisque une interface fonctionnelle ne peut avoir qu'une seule méthode abstraite, les
types des paramètres de l'expression Lambda doivent correspondre aux paramètres de cette
méthode, et le type du corps de lambda doit correspondre au type de retour de la méthode. En
outre, toutes les exceptions émises dans le corps lambda doivent être autorisés par la clause
"throws" de cette méthode dans l'interface fonctionnelle.
Considérons
les
deux
interfaces
fonctionnelles
suivantes
(java.lang.Runnable
et java.util.concurrent.Callable<V>):
La méthode "Runnable.run" n’a pas une valeur de retour contrairement à la méthode
"CallableV>.call"
Supposons qu’une méthode nomée "invoke" sera surchargée comme suit :
Quelle méthode sera appelée par l’instruction suivante :
Attia Mehdi – Java 8 les fondamentaux du langage
3
La méthode "invoke (Callable <T>)" sera invoquée parce que cette méthode renvoie
une valeur. Dans ce cas, le type de l'expression lambda " () -> done" est Callable <T>.
I.2 Lambda Scopes
Accéder à des variables externes à partir des expressions lambda est très similaire aux objets
anonymes. Il est possible d’accéder aux variables locaux finals (finals) ainsi que les variables
d'instance (instance fields) et les variables statiques.
Accéder à des variables locales
Nous pouvons lire des variables locales finales à partir de la portée externe des expressions
lambda.
Mais contrairement aux objets anonymes la variable "num" peut être déclarée non final. Ce
code est également valable:
Cependant la variable "num" doit être implicitement finale. Le code suivant ne compile pas:
Attia Mehdi – Java 8 les fondamentaux du langage
4
Accéder aux fields et aux variables statiques
Contrairement aux variables locales nous avons l'accès en lecture et écriture aux variables
d'instance et aux variables statiques de l'intérieur des expressions lambda.
I.3 Ce que Lambda ne peut pas faire
Il y a quelques caractéristiques que lambdas ne fournissent pas. Elles ont été considérées
pour Java 8, mais n’ont pas été incluses, pour plus de simplicité et en raison de contraintes de
temps.
Accès aux variables locales non finales
Comme il est mentionné dans la section (I.2) si une nouvelle valeur est affectée à une
variable, elle ne peut pas être utilisée dans une expression lambda. Le mot-clé "final" n’est pas
nécessaire, mais la variable doit être "effectivement finale» (voir plus haut). Ce code ne compile
pas:
La transparence d’exception
Si une exception contrôlée peut être lancée à l'intérieur d'une expression lambda, l'interface
fonctionnelle doit également déclarer que cette exception contrôlée peut être lancée aussi.
L'exception n’est pas propagée à la méthode contenant l’expression lambda. Ce code ne
compile pas:
Attia Mehdi – Java 8 les fondamentaux du langage
5
Flux de contrôle (break, return)
Il n y a pas un moyen de sortir d’une boucler et retourner une valeur comme résultat de la
méthode qui contient l’expression lambda, par exemple :
III.
Interfaces fonctionnelles
Une interface fonctionnelle est une interface disposant une unique méthode abstraite. Le but est
de définir la signature d’une méthode qui pourra être utilisée pour passer en paramètre :
Une référence vers une méthode statique
Une référence vers une méthode d’instance
Une référence vers un constructeur
Une expression lambda.
Même si l'annotation @FunctionalInterface n’est pas obligatoire, elle a un rôle important
permettant au compilateur de forcer l’interface afin qu’elle ne contienne qu’une seule méthode
abstraite. Si nous essayons de définir plus d'une seule méthode abstraite dans une interface
annotée avec @FunctionalInterface, le compilateur affichera une erreur.
Attia Mehdi – Java 8 les fondamentaux du langage
6
Une interface ne peut pas fournir une implémentation par défaut de l'une des méthodes de
la classe Object. En particulier, cela signifie qu'il n’est pas possible de fournir une
implémentation pour les méthodes "equals", "hashCode", ou "toString" à partir d'une
interface.
II.1 Méthodes statiques et les méthodes par défaut.
Parmi les nouveautés apportées par Java 8, on en trouve deux qui concernent les interfaces:
les méthodes statiques et les méthodes par défaut. Les méthodes statiques définies sur les
interfaces fonctionnent exactement de la même façon que celles portées par les classes, il n'y a
donc pas grand-chose à en dire. En revanche, les méthodes par défaut risquent de modifier assez
profondément notre façon de concevoir nos API.
En Java 7 et antérieur, une méthode déclarée dans une interface ne fournit pas
d'implémentation. Ce n'est qu'une signature, un contrat auquel chaque classe dérivée doit se
conformer en fournissant une implémentation propre. Mais il arrive que plusieurs classes
similaires souhaitent partager une même implémentation de l'interface. Dans ce cas, deux
stratégies sont possibles :
- Factoriser le code commun dans une classe abstraite, mais il n'est pas toujours possible
de modifier la hiérarchie des classes
Publicité
- Extraire le code commun dans une classe utilitaire, sous forme de méthode statique (ex:
Collections.sort()).
On conviendra qu'aucune des deux n'est réellement satisfaisante. Heureusement, Java 8 nous
offre maintenant une troisième possibilité en proposant une solution plus propre permettant aux
méthodes déclarées dans les interfaces d'avoir une implémentation, il s’agit bien des méthodes
par défaut. Une méthode par défaut est une méthode d'instance définie dans une interface et
commence par le mot-clé "default", elle contient également un corps de code. Chaque classe
qui implémente l'interface hérite ses méthodes par défaut et peut les redéfinir.
Prenons un coup d'œil sur exemple ci-dessous.
Attia Mehdi – Java 8 les fondamentaux du langage
7
L’interface Formule définit la méthode abstraite "calculer" la méthode par défaut "sqrt".
Classes concrètes n’ont qu'à implémenté la méthode abstraite calculer.
L’interface formule est implémentée en tant qu'objet anonyme. Le code est assez verbeux:
6 lignes de code pour une simple calcule de "sqrt (a * 100)". Dans les prochaines sections
(II.3, III) nous présentons un moyen plus agréable d’implémentation d’interfaces qui possèdent
une seule méthode abstraite (Single Abstract Method interface) en Java 8.
II.2 Référence de méthodes et de constructeurs
Une référence de méthode ou de constructeur est utilisée pour définir une méthode ou un
constructeur en tant qu’implémentation de la méthode abstraite d’une interface fonctionnelle, à
l’aide du nouvel opérateur « :: ».
Tous constructeurs et méthodes peuvent être utilisés comme référence, à condition qu’il n’y
ait aucune ambigüité. Lorsque plusieurs méthodes ont le même nom, ou lorsqu’une classe a
plus d’un constructeur, la méthode ou le constructeur approprié est sélectionné en fonction de
la méthode abstraite de l’interface fonctionnelle à laquelle fait référence l’expression.
Attia Mehdi – Java 8 les fondamentaux du langage
8
Exemples :
Voyons comment le mot-clé « :: » fonctionne pour les constructeurs. Nous définissons d'abord une classe avec différents constructeurs:
Ensuite, nous spécifions une interface "PersonneFactory" pour créer de nouvelles personnes:
Attia Mehdi – Java 8 les fondamentaux du langage
9
Nous créons une référence au constructeur personne via "Person::new". Le compilateur Java
choisit automatiquement le bon constructeur en faisant correspondre la signature de
"PersonFactory.create".
Le tableau suivant présente quatre types de références de méthode.
Type de référence de
méthode
Syntaxe
Exemple
Equivalence en Lambda
Référence d’une méthode
className::staticMetho
statique
dName
Référence d’une méthode
d’instance
objectName::instanceM
ethodName
Référence d’une méthode
d’instance arbitraire
className::instanceMet
hodName
String::valueOf
x -> String.valueOf(x)
s::toString
() -> s.toString()
Object::toString
x -> x.toString()
Référence du constructeur
className::new
ArrayList::new
LinkedList<Employee>::new
() -> new ArrayList<>() () -> new LinkedList<Employee>()
Les Références des méthodes est un moyen facile pour accéder aux méthodes existantes et les
utiliser d'une manière fonctionnelle.
II.3 Les interfaces fonctionnelles les plus utilisées
Le JDK 8 apporte par défaut plusieurs interfaces fonctionnelles, D’anciennes interfaces de
l’API ont été migrées telles que "Comparable" et "Runnable".
Nous citons ci-dessous les interfaces les plus utilisées :
Attia Mehdi – Java 8 les fondamentaux du langage
10
Function
Une interface "Function" prend un argument et retourne un résultat dont la méthode
fonctionnelle est "apply(Object)".
@FunctionalInterface
T - le type d'entrée de la fonction
public interface Function<T,R>
R - le type du résultat de la fonction
L’interface "Function" contient des méthodes par défaut (compose, andthen) qui peuvent être
utilisées pour chaîner plusieurs fonctions ensemble.
Supplier
L’interface "Supplier" produise un résultat d'un type donnée. Contrairement à
l’interface "Function", "Supplier" n’accepte pas les arguments. La méthode abstraite de cette
interface est "get()".
@FunctionalInterface public interface Supplier<T>
T - Le type du résultat fournit par ce Supplier
Consumer
L’interface "Consumer" représente une opération qui accepte un seul argument et ne
renvoie aucun résultat. La méthode abstraite de cette interface est "accept(Object)".
Attia Mehdi – Java 8 les fondamentaux du langage
11
@FunctionalInterface public interface Consumer<T>
Predicate
T - le type d'entrée de la fonction
L’interface "Predicate" représente une opération qui accepte un seul argument et
renvoie un résultat de type "Boolean". La méthode abstraite de cette interface est
"test(Object)".
@FunctionalInterface public interface Predicate<T>
T - le type d'entrée de la fonction
L’interface "Predicate" contient quelques méthodes non abstraites (par défaut) telles que :
"and (Predicate<? super T> other)", "isEqual (Object targetRef)", "negate()" (Predicate<? super T> other)".
,"or
Comparator
L’interface "Comparator" est bien connue dans les anciennes versions du Java. C’est une
fonction de comparaison, qui impose un ordre total sur certains objets de collection via sa
méthode abstraite "compare(T o1, T o2)".
@FunctionalInterface public interface Comparator<T>
T - le type d'objets qui peuvent être comparés par ce comparateur.
Java 8 ajoute d’autres méthodes par défaut à l'interface "Comparator" telles que
"reversed()" et "thenComparing(Comparator<? super T> other)".
Attia Mehdi – Java 8 les fondamentaux du langage
12
UnaryOperator
Représente une opération sur un opérande unique qui produit un résultat du même type que
son opérande. C’est une spécialisation de l’interface "Function" dans le cas où l'opérande et
le résultat sont du même type. L’interface abstraite de cette interface est "apply(Object)".
@FunctionalInterface public interface UnaryOperator<T> extends Function<T,T>
T - le type de l'opérande et du résultat de l'opération
BiFunction
Représente une fonction qui accepte deux arguments et produit un résultat. C’est la
spécialisation de deux arités de l’interface "Function". La méthode abstraite de cette interface
est "apply(T t, U u)".
@FunctionalInterface public interface BiFunction<T,U,R>
T - le type du premier argument
de la fonction
U
-
le
type du deuxième
argument de la fonction
R - le type du résultat de la
fonction
Attia Mehdi – Java 8 les fondamentaux du langage
13
BinaryOperator
Représente une opération à deux opérandes de même type, qui produise un résultat du même
type que les opérandes. C’est une spécialisation de l’interface "BiFunction" dans le cas où les
opérandes et le résultat sont tous du même type.
@FunctionalInterface public interface BinaryOperator<T> extends BiFunction<T, T, T>
T - le type des opérandes et du résultat de l'opération
On a cité les interfaces fonctionnelles les plus utilisées en java 8, mais il existe plusieurs
d’autres tels que "BiPredicate<T, U>", "BiConsumer<T, U>…"
IV. Streams
Java 8 arrive avec une toute nouvelle API Stream qui utilise les Lambda. Cette nouvelle
API offre la possibilité de simplifier l’écriture, d’améliorer la performance ainsi d’augmenter
la lisibilité d’un certain nombre de code.
Un "Stream<T>" décrit une séquence d’éléments (de type T) supportant des opérations
agrégées en séquence ou en parallèle.
Publicité
En voici un exemple :
Attia Mehdi – Java 8 les fondamentaux du langage
14
On récupère un "Stream<Player>" à partir d’une collection que l’on filtre en ne prenant
que les joueurs dont le nom est "lionel". On crée ensuite un "IntStream" (stream de int) des
salaires de ces joueurs filtrés que l’on additionne. En bref, on a calculé le salaire total des
joueurs dont le nom est "lionel".
Il est important de ne pas voir les "streams" comme une alternative aux collections mais
plutôt comme un moyen de traitement associé :
Un "stream" n’est pas un moyen de stockage, il ne fait que transmettre des données
provenant d’une source (collection, tableau, générateur de données, canal d’E/S, …) au
travers d’une file d’opérations.
Un "stream" est fonctionnel par nature, une opération sur un "stream" produit un
résultat mais ne modifie jamais sa source. Un filtrage, par exemple, crée un nouveau
"stream" avec les éléments filtrés, il ne supprime rien de la source.
Un "stream" a un mode d’exécution paresseuse. Les opérations d’un "stream" sont de
deux types : intermédiaires (elles renvoient un "stream" comme le filtrage par exemple)
ou terminales (comme la somme vue plus haut). Les opérations intermédiaires sont
toujours implémentées de manière paresseuse, afin de profiter au maximum d’une
éventuelle optimisation.
Un "stream" est un consommateur, ses éléments ne peuvent être visités qu’une seule
fois pendant sa durée de vie, un nouveau "stream" doit donc être créé pour parcourir à
nouveau la source.
III.1 Obtention d’un stream
Il existe plusieurs façons de créer un "stream". La plus simple consiste à appeler la méthode
"stream()" ou "parallelStream()" sur une collection, de plus un certain nombre de méthodes
ont été ajoutées aux classes déjà existantes.
Attia Mehdi – Java 8 les fondamentaux du langage
15
Un "stream" s’obtient de la manière suivante :
Depuis une Collection via la méthode "stream()" ou "parallelStream()"
Depuis un tableau via "Arrays.stream(Object[])".
Depuis
des méthodes
statiques
de
l’interface
"Stream<T>
comme
"Stream.of(Object[])",
"IntStream.range(int,int)"
ou
"Stream.iterate(Object,
UnaryOperator) ".
Lors de la lecture d’un fichier via "BufferedReader.lines()".
III.2 Opérations sur les stream
L’intérêt principal d’un "stream" réside dans la possibilité d’effectuer plusieurs opérations
plus ou moins complexes en les enchaînant en une seule instruction. Cet enchaînement, appelé
pipeline, est composé de plusieurs opérations intermédiaires et d’une seule opération terminale.
Opérations intermédiaires
- Du même type :
Chaque opération
intermédiaire
retourne un nouveau "stream"
(permettant ainsi
l’enchaînement des opérations).
Soit "stringCollection" une liste des String définie comme suit :
- Stream<T> filter(Predicate<T>) : retourne un Stream<T> dont les éléments
correspondent au test du prédicat.
Attia Mehdi – Java 8 les fondamentaux du langage
16
- Stream<T> sorted() : retourne un Stream<T> trié selon l’ordre naturel des éléments.
Gardez en tête que "sorted" créée seulement une vue de Stream triée sans manipuler l’ordre
de la collection "stringCollection".
NB : Si T n’implémente pas l’interface Comparable, une "ClassCastException" est levée. Pour
les "streams" ordonnés, le tri est stable, dans le cas contraire, rien n’est garanti.
- Stream<T> sorted(Comparator<T> comparator)
:
identique à
la méthode
précédente mais en précisant le comparateur.
- Stream<T> distinct() : retourne un Stream<T> dont les éléments sont tous distincts.
NB : T doit obligatoirement implémenter la méthode "hashCode()".
- Stream<T> limit(long maxSize) : retourne un Stream<T> limité à la taille donnée.
- Stream<T> peek(Consumer<T> action) : retourne un Stream<T> avec les mêmes
éléments que la source mais dont l’action spécifiée est exécutée lors de leur
consommation.
Cette méthode est très pratique pour débugger pendant la visite d’un pipeline de
plusieurs opérations.
Attia Mehdi – Java 8 les fondamentaux du langage
17
- Stream<T> skip(long n) : retourne un Stream<T> sans les n premiers éléments.
NB : Si le "stream" contient moins de n éléments, un "stream" vide est renvoyé.
- De type différent
Des opérations intermédiaires plus complexes peuvent « changer » le type du Stream :
- <R> Stream<R> map(Function<T, R> mapper) : retourne un Stream<R> dont
chaque élément est une obtenu à partir d’un élément de la source (via le mapper qui est
une fonction de transformation d’un objet de type T en objet de type R).
- R> Stream<R> flatMap(Function<T, Stream<R>> mapper) : retourne un
Stream<R> contenant tous les éléments résultants de l’application du mapper appliqué
à chaque élément de la source. En bref, on applique une transformation 1-n à chaque
élément puis on concatène le tout.
Nous avons déjà appris comment transformer les objets d'un "stream" à un autre type
d'objets en utilisant l'opération "map". "map" est un peu limité parce que chaque objet
transformé exactement à un autre objet. Mais que faire si nous voulons transformer un objet en
plusieurs autres? C’est là que "flatMap" vient à la rescousse.
"flatMap" transforme chaque élément du "stream" à un "stream" d'autres objets. Donc, chaque
objet sera transformé en zéro, un ou plusieurs autres objets sauvegardés par des "streams". Le
Attia Mehdi – Java 8 les fondamentaux du langage
18
contenu de ces "streams" sera par la suite placé dans le "stream" renvoyé par l'opération de
"flatMap".
Avant de voir en action "flatMap" nous avons besoin d'une hiérarchie de type approprié:
Ensuite, nous utilisons nos connaissances sur les "stream" d'instancier un couple d'objets:
Maintenant, nous avons une liste de trois "foos" constituées chacune de trois "bars".
"FlatMap" accepte une fonction qui doit retourner un "stream" d'objets. Donc, afin de
résoudre les objets bar de chaque "foo", nous passons la fonction appropriée:
Attia Mehdi – Java 8 les fondamentaux du langage
19
Comme vous pouvez le voir, nous avons transformé un "stream" de trois objets "foo" à un
"stream" de neuf objets "bar". Enfin, l'exemple ci-dessus peut être simplifié en un seul pipeline
d'opérations de "stream" :
NB : Les mêmes opérations complexes existent sur et vers les "streams" de type (IntStream,
LongStream et DoubleStream) telles que mapToInt, flatMapToInt, etc.
Opérations terminales
L’opération terminale d’un pipeline est l’opération finale, celle qui déclenche l’exécution
du pipeline d’opérations (et donc consomme le flux d’entrée), produit (éventuellement) un
résultat puis ferme le "stream".
- Booléennes
Les opérations booléennes "allMatch", "anyMatch" et "noneMatch", qui prennent toutes
les trois un "Predicate<T>" en paramètre, sont vraies lorsque, respectivement, tous les
éléments, au moins un ou aucun satisfont le prédicat donné.
Attia Mehdi – Java 8 les fondamentaux du langage
20
- Numériques
Seules 2 opérations numériques existent :
long count() : retourne le nombre d’éléments du "stream".
type sum() : retourne la somme des éléments du "stream" (type étant int, double ou
long), cette méthode n’existe que sur les "streams" typés.
- Simples
Certaines opérations renvoient un "Optional<T>" c’est-à-dire un objet qui peut être soit
(null), soit un élément du "stream":
"Optional<T>" est un conteneur d’objet permettant de gérer différemment la nullité d’une
valeur. La classe "Optional" pourrait être manipulée via plusieurs méthodes. La méthode
Attia Mehdi – Java 8 les fondamentaux du langage
21
"isPresent" nous renseigne sur l’existence d’une valeur dont l’accès est donné par "get".
De plus, il est possible d’’ajouter une valeur par défaut ou de lever une exception via les
méthodes "orElse" ou "orElseThrow").
Optional<T> findAny() : retourne un élément (n’importe lequel) du "stream".
Optional<T> findFirst() : retourne le premier élément du "stream".
Optional<T> max
(Comparator<T>comparator) et Optional<T> min
(Comparator<T>comparator) retournent respectivement le maximum et le
minimum des éléments du "stream" en accord avec le comparateur donné.
- Complexes
La plupart des exemples de code de cette section utilisent la liste suivante des personnes pour
Publicité
la démonstration.
Collect
"Collect" est une opération terminale extrêmement utile pour transformer les éléments
d’un "stream" à un autre type de résultat, par exemple une "List", "Set" ou "Map". "Collect"
accepte un "collector" qui se compose de quatre opérations différentes: un "supplier", un
"accumulator", un "combiner" et un "finisher". Cela semble super-compliqué au premier
abord, mais l’atout de Java 8 c’est qu’il prend en charge diverses collectors intégrés via la classe
Collectors. Donc, pour les opérations les plus courantes vous n’avez pas à mettre en œuvre un
"Collector" par vous-même.
Commençons par un cas d'utilisation très courant:
Attia Mehdi – Java 8 les fondamentaux du langage
22
Comme vous pouvez le voir, c’est très simple à construire une liste à partir des éléments
d'un "stream". Si vous avez besoin d'un Set au lieu de la liste - il suffit d'utiliser
"Collectors.toSet()".
L'exemple suivant regroupe toutes les personnes selon l'âge:
"Collectors" sont extrêmement polyvalent. Vous pouvez également créer des
agrégations sur les éléments du "stream", par exemple déterminer l'âge moyen de toutes les
personnes:
Si vous êtes intéressés par des statistiques plus complètes, vous pouvez utiliser les
méthodes
"summarizing" de
la
classe
collectors
tels que
"summarizingInt",
Attia Mehdi – Java 8 les fondamentaux du langage
23
"summarizingLong" et "summarizingDouble". Donc, nous pouvons tout simplement
déterminer le min, max, somme, nombre et moyennes des âges des personnes.
L’exemple suivant ajoute les noms des personnes à une chaine de caractère.
La méthode "joining" accepte un séparateur et optionnellement un préfixe et un suffixe.
Afin de transformer les éléments du "stream" en une "Map", nous devons préciser comment
les clés et les valeurs devraient être mappées. Gardez à l'esprit que les clés mappées doivent
être uniques, sinon une exception de type "IllegalStateException" est levée. Vous pouvez
éventuellement passer une fonction de fusion comme un paramètre supplémentaire pour
contourner l'exception:
Attia Mehdi – Java 8 les fondamentaux du langage
24
Maintenant que nous connaissons les principales méthodes de la classe Collectors, nous
allons essayer de construire notre propre "Collector". Nous voulons transformer toutes les
personnes du "stream" en une seule chaîne constituée de tous les noms en lettres majuscules
séparées par le caractère "|". Pour y arriver, nous créons un nouveau "collector" via
"Collector.of()". Nous devons passer les quatre paramètres d'un "collector": un "supplier", un
"accumulator", un "combiner" and un "finisher".
Puisque les String en Java sont immuables, nous avons besoin d'une classe comme
"StringJoiner" pour les construire avec un "collector". Le supplier construit initialement un
"StringJoiner" avec le délimiteur approprié. "L’accumulator" est utilisé pour ajouter chaque
nom en majuscule à la "StringJoiner". Le "combiner" sait comment fusionner deux
"StringJoiner" en une seule. Dans la dernière étape le "finisher" construit la chaîne désirée à
partir de la "StringJoiner".
- Reduce
L'opération de réduction combine tous les éléments du "stream" en un seul résultat. Java 8
prend en charge trois types différents pour réduire les méthodes. Le premier réduit un "stream"
d'éléments a exactement un élément du "stream". Voyons comment nous pouvons utiliser cette
méthode pour déterminer la personne la plus âgée:
Attia Mehdi – Java 8 les fondamentaux du langage
25
Dans l’exemple ci-dessus, la méthode "reduce" accepte un "BinaryOperator" pour
comparer les âges des personnes afin de retourner la personne avec l'âge maximum. La
deuxième méthode de réduire accepte à la fois une valeur d'identité "identity" et un
accumulateur "BinaryOperator". Cette méthode peut être utilisée pour construire une nouvelle
personne avec les noms et les âges d'agrégées toutes les autres personnes dans le "stream":
La troisième méthode de réduire accepte trois paramètres: une valeur d'identité
"identity", un accumulateur "BiFunction" et une fonction de combinaison de type
"BinaryOperator". Comme le type de valeurs d'identité n’est pas limité au type de personne,
nous pouvons utiliser cette réduction pour déterminer la somme des âges de toutes les
personnes:
Comme vous pouvez voir le résultat est 107, mais qu’est ce qui se passe exactement en
background? Etendons le code ci-dessus par un affichage de débogage:
Attia Mehdi – Java 8 les fondamentaux du langage
26
Comme vous pouvez voir la fonction de l'accumulateur "accumulator" fait tout le
travail. Elle a d'abord être appelée avec la valeur d'identité initiale 0 et la première personne
Max. Dans les trois prochaines étapes la somme augmente avec l'âge de la dernière personne
jusqu'à atteindre le total de 76. La combinaison "combiner" ne sera jamais appelé? L'exécution
du même "stream" en parallèle va lever le secret:
L'exécution de ce "stream" en parallèles implique un comportement d’exécution
complétement différent. Maintenant,
la combinaison est en fait appelée. Lorsque
Attia Mehdi – Java 8 les fondamentaux du langage
27
l'accumulateur est appelé en parallèle, le combinateur est nécessaire pour additionner les valeurs
accumulées séparés.
- Order
Maintenant que nous avons appris comment créer et travailler avec différents types de
"stream", nous allons plonger plus profondément dans la façon avec laquelle les opérations du
"stream" sont traités sous le capot.
Une caractéristique importante des opérations intermédiaires est la paresse ("laziness").
Regardez cet exemple où une opération terminale est manquante:
On exécutant ce bout de code, rien ne sera affiché à la console. C’est parce que les
opérations intermédiaires ne seront exécutés que lorsque une opération terminale est présente.
Étendons l'exemple ci-dessus par l'opération terminale "forEach":
Attia Mehdi – Java 8 les fondamentaux du langage
28
L'ordre du résultat peut être surprenant. Une approche naïve consiste à exécuter les
opérations horizontalement, l’une après l'autre sur tous les éléments du "stream". Mais au lieu
que chaque élément se déplace le long de la chaîne verticalement, le premier élément String
"d2" passe de la méthode "filtre" à la méthode "forEach", et ce n’est qu’à ce moment-là que
le deuxième élément "A2" peut être traité. Ce comportement peut réduire le nombre réel des
opérations effectuées sur chaque élément, comme nous le voyons dans l'exemple suivant:
L'opération "AnyMatch" renvoie vrai dès que le prédicat s’applique à l'élément d'entrée
donné. Cela est vrai pour le second élément passé "A2". En raison de l'exécution verticale de la
chaîne de "stream", dans ce cas la méthode "map" sera exécutée seulement deux fois. Donc,
plutôt que de mapper tous les éléments du "stream", la méthode "map" sera appelé aussi peu
que possible.
Pourquoi l'ordre est important
L'exemple suivant est constitué de deux opérations intermédiaires "map" et "filtre" et
une opération terminale "forEach". Essayons de nouveau de voir de près la manière dont ces
opérations sont exécutées:
Attia Mehdi – Java 8 les fondamentaux du langage
29
Comme vous l'aurez deviné les opérations "map" et "filtre" sont appelées cinq fois dans la
collection alors que la méthode "forEach" est appelée une seule fois.
Nous pouvons réduire le nombre réel d'exécutions si nous changeons l'ordre des opérations, en
déplaçant "filtre" au début de la chaîne:
Maintenant, la méthode "map" est appelée une seule d’où les opérations du pipe-line
beaucoup plus rapide pour un nombre d'éléments d’élément plus grand. Gardez cela à l'esprit
lors de la composition d’une chaîne des méthodes complexes.
Étendons l'exemple ci-dessus par une opération supplémentaire de tri "sorted":
Le tri est un type spécial de fonctionnement intermédiaire. C’est une opération dite
"stateful" car pour trier une collection d'éléments vous devez maintenir l'état lors du tri.
Attia Mehdi – Java 8 les fondamentaux du langage
30
Tout d'abord, l'opération de tri est exécutée sur tous les éléments du "stream". En d'autres
termes "sorted" est exécutée horizontalement. Donc dans ce cas "sorted" est appelé huit fois
pour plusieurs combinaisons sur chaque élément du "stream".
Une fois de plus, nous pouvons optimiser les performances en réorganisant la chaîne:
Dans cet exemple "sorted" n’est jamais été appelé parce "filtre" réduit le nombre des
éléments à un seul élément. Donc la performance est considérablement améliorée notamment
pour les grand nombre d’éléments.
V. Date/Time API
Java 8 arrive avec une nouvelle API pour la gestion du temps, via le package "java.time".
Elle est inspirée de la librairie "Joda-Time". "Stephen Colebourne", créateur de "JodaTime",
a participé à l'élaboration de celle-ci. Son but est de combler les défauts des vieillissantes API
"Date" (JDK 1.0) et "Calendar" (JDK 1.1) en introduisant de nouveaux concepts.
Les exemples suivants couvrent les parties les plus importantes de cette nouvelle API.
Clock
Horloge permet d'accéder à la date et l'heure courante. Clocks ont un fuseau horaire et
peuvent être utilisés à la place du "System.currentTimeMillis()" pour récupérer les
Attia Mehdi – Java 8 les fondamentaux du langage
31
millisecondes actuelles. Un point instantané sur la ligne de temps est également représenté par
la classe "Instant". Instants peuvent être utilisés pour créer des objets du package
"java.util.Date".
Timezones
"Timezones" sont représentés par un "ZoneId". Ils peuvent être facilement accessibles
via des méthodes statiques. Ils définissent les décalages horaires ce qui e