Java 8 Non ce n’est pas que les streams
Tags: java, streams, programming
Published: 2021-11-24 13:05:11

## Introduction
Java 8 est sortie en 2014. À ce jour, les nombreuses nouveautés que cette version a apportées ne semblent toujours pas connues de la grande majorité des développeurs.

Nous aimerions tenter ici d'éclaircir ce que sont les lambdas et les streams. D'après ce que nous entendons lors de nombreuses qualifications techniques, le sujet reste nébuleux.

Nous essaierons aussi, modestement, d'y ajouter un petit retour d'expérience sur leur utilisation ...


## Inner classes to lambdas

Plongeons tout de suite dans les lambdas.

Pour nous servir d'exemple nous utiliserons l'interface `org.springframework.jdbc.core.RowMapper` :

```java
public interface RowMapper<T> {
  T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
```

Les classes internes allègent le code en nous permettant de ne pas créer des classes à tour de bras pour finalement ne redéfinir ou n'implémenter qu'un nombre réduit de méthodes.

Seulement ces classes internes restent très verbeuses : dans les classes internes de l'exemple ci-dessous, combien de lignes nous donnent réellement de l'information  ? De combien de lignes devons-nous faire abstraction pour nous concentrer sur l'essentiel ?

```java
public List<DataRecord> getDataRecords(String jobId){
  return jdbcTemplate.query("select * from DATARECORD ", 
    new RowMapper<DataRecord>(){ ⓵
      @Override
      public DataRecord mapRow(ResultSet rs, int rowNum) 
             throws SQLException { ⓶
        return new DataRecord(
          rs.getString("job_id"), 
          rs.getString("analysis_name"));
      }
  });
}
```
⓵ Déclaration et utilisation de l'inner classe
⓶ Implémentation de la méthode.

Dans l'exemple du `RowMapper`, utilisé pour extraire le retour d'une requête sql jdbc, l'information que l'on retire de la définition de la classe interne est :
> L'instance de la classe `DataRecord` est créée en exploitant les colonnes `job_id` et `analysis_name`.

Le reste n'est que syntaxe et bruit pour le lecteur.
Et c'est ce que proposent de simplifier les expressions lambdas, introduites en java 8.

Le raisonnement est le suivant :

* Le `RowMapper` n'a qu'une unique méthode à redéfinir.
On pourrait donc se passer de préciser la méthode redéfinie lors de l'implémentation.
* Le type du paramètre d'entrée de la méthode peut se déduire de la définition de la méthode abstraite.
* Le type paramétré du `RowMapper` se déduit du type de l'objet retourné par la méthode mapRow.

Les expressions lambdas permettent d'appliquer ces simplifications.


### Mais c'est quoi une lambda ?
Les expressions lambdas sont un sucre syntaxique simplifiant l'implémentation de classe/interface.

Elles permettent de définir des fonctions sans les nommer. Elles peuvent être manipulées et exécutées dans un programme sans avoir un nommage figé.

Les lambdas s'écrivent de la façon suivante :

```
(Type1 param1, Type2 param2, .., TypeN paramN) -> { traitement }
```


### Utilisons-les pour mieux les comprendre

Appliquons les simplifications énoncées ci-dessus à notre exemple :

```java
public List<DataRecord> getRecords(String jobId) {
  return jdbcTemplate.query("select * from DATARECORD ",
    (ResultSet rs, int rowNum) -> {
      return new DataRecord(
        rs.getString("job_id"),
        rs.getString("analysis_name"));
    });
}
```

Il ne reste que l'essentiel : la définition du corps de la méthode en fonction des paramètres d'entrée.

La donnée essentielle s'écrit maintenant:

```java
(ResultSet rs, int rowNum) -> {
  return new DataRecord(
     rs.getString("job_id"), 
     rs.getString("analysis_name")
  );
}
```

où `rs` est le résultat de la requête, `rowNum` est le numéro de la ligne en cours.

Dans notre exemple, le compilateur comprend de lui-même qu'on définit un `RowMapper` via l'implémentation de son unique méthode.

Notons que dans une expression lambda :

* Si le type des paramètres peut être inféré, il peut être omis.
* S'il y a un et un seul paramètre, les parenthèses peuvent être omises.
* S'il n'y a qu'une seule instruction, les accolades autour du traitement, peuvent être omises. Dans ce cas, le mot clé `return` et le point-virgule `;` de fin d'instruction peuvent eux aussi être omis.
* Le nom des paramètres est indépendant de celui défini dans la méthode implémentée.

Si on applique toutes ces règles on peut encore simplifier la lambda utilisée pour définir le `RowMapper` :

```java
(ResultSet rs, int rowNum) -> {
  return new DataRecord(
    rs.getString("job_id"), 
    rs.getString("analysis_name")
  );
}
```

puis

```java
(resulSet, index) -> {
  return new DataRecord(
    resulSet.getString("job_id"), 
    resulSet.getString("analysis_name")
  );
}
```

puis

```java
(rs, rowNum) -> new DataRecord(
  rs.getString("job_id"), 
  rs.getString("analysis_name")))
```

 
> :warning: S'il est vrai que les lambdas peuvent améliorer la lisibilité du code. Ce n'est pas toujours le cas. 
> En effet une lambda de 40 lignes posée comme une verrue au milieu de votre code ne facilitera la lecture du code pour personne.
> Par ailleurs, il est strictement interdit de laisser sortir une _checked exception_ d'une lambda. Si vous avez l'habitude  d'en utiliser, c'est le moment ou jamais d'arrêter avec cette  hérésie.
![Shame !](https://media.giphy.com/media/vX9WcCiWwUF7G/giphy.gif)



## Consumer, supplier, Function and many others

Les interfaces qui peuvent être implémentées sous la forme de lambdas ne possèdent qu'une unique méthode non implémentée. Elles sont appelées interfaces fonctionnelles et peuvent être annotées `@FunctionalInterface`. 
Cette annotation permet au compilateur de vérifier que l'interface est bien fonctionnelle, mais elle n'est en aucun cas obligatoire.


La classe `Comparator<T>` est un exemple d'interface fonctionnelle commune aux développeurs : sa méthode `compare(param1,param2)` renvoie le résultat de la comparaison entre deux objets de type `T`. 
Elle existait certes avant java 8, mais cette dernière version l'a dotée de l'annotation `@FunctionalInterface` pour la désigner comme telle.


Nous avons jusqu'ici utilisé des expressions lambdas pour implémenter des interfaces  courantes comme `RowMapper`, `Comparator` ou `Runnable`. 

Mais les lambdas sont plus largement utilisées pour définir des méthodes à l'endroit même où elles sont utilisées, sans porter d'intérêt particulier à l'interface fonctionnelle sous-jacente. 

Il est possible de vouloir déclarer de telles méthodes lorsqu'elles ne sont utilisées qu'une fois par exemple, ou encore pour passer un traitement en paramètre d'un autre traitement.

La méthode que l'on souhaite définir via une lambda doit ici encore implémenter une interface fonctionnelle. 

Java 8 fournit une bibliothèque d'interfaces fonctionnelles standard appelée `java.util.function`, qui permet au développeur d'avoir accès aux interfaces fonctionnelles les plus communes sans les définir lui-même.



### L'essentiel

Les interfaces du package `java.util.function` (naissance avec Java 8) sont des fonctions génériques définissant les cas d'usages les plus courants:

* Une _function_ prendra un ou plusieurs paramètres en entrée et retournera un résultat.
* Un _supplier_ ne prendra pas de paramètre d'entrée et fournira un résultat.
* Un _consumer_ prendra un ou plusieurs paramètres d'entrée et ne renverra aucun résultat.
* Un _operator_ prendra un ou plusieurs paramètres du même type et fournira un retour de ce type.
* Un _predicate_ prendra un ou plusieurs paramètres en entrée et retournera un booléen.

### Plus en détail

#### Des basiques ...

##### `Function<T,R>` 

Elle désigne une fonction prenant un paramètre d'entrée de type `T` et retournant un objet de type `R`.

```java
Function<Integer, String> getNumber = item -> "Number " + 
item;
```

Elle s'applique en utilisant apply :

```
>> getNumber.apply(1); \\ renvoie Number 1
```

Elle se décline en :
* `UnaryOperator<T>` : fonction dont les types d’entrée et de retour sont identiques. C’est donc une `Function<T,T>`.
* `BiFunction<T,U,R>` : fonction qui prend en entrée deux paramètres, le premier de type `T` et le second de type `U`.
* `BinaryOperator<T>` : fonction qui prend en entrée deux paramètres de type `T`. C’est donc une `BiFunction<T,T,T>`. Par exemple :
```java
BinaryOperator<Integer> add = (a,b) -> a+b;
```

##### `Consumer<T>`
Il désigne une fonction prenant un paramètre d'entrée de type `T` et de type de retour `void`.

```java
Consumer<Integer> display = entier -> System.out.println("Number " + entier);
```

Elle s'applique en utilisant apply :

```
>> display.apply(1); \\ affiche Number 1
```

Il se décline également en `BiConsumer<T,U>` : prend en entrée deux paramètres, le premier de type `T` et le second de type `U`.

##### `Supplier<R>`
Il désigne une fonction ne prenant aucun paramètre d'entrée et retournant un objet de type `R`.

```java
Supplier<Double> randomFrom0To100 = () -> Math.random() * 100;
```

Elle s'applique en utilisant get :

```
>> randomFrom0To100.get(); \\ affiche un double aléatoire en 0 et 100
```

##### `Predicate<T>`
Il désigne une fonction prenant un paramètre d'entrée de type `T` et renvoyant un booléen.

Exemple :

```java
Predicate<String> isNull = (str) -> str == null;
```

Elle s'applique en utilisant test :

```
>> isNull.test("HelloJava8"); // renvoie faux
```

##### Les types primitifs

Les fonctions présentées ci-dessous ne permettent pas de manipuler des types primitifs. Des fonctions spécifiques existent pour ceux-ci :
* Paramètre d'entrée de type primitif :
  * `IntFunction<R>`, `IntConsumer` dont le type du paramètre d'entrée est `int`.
  * Et les autres dérivés sur le même modèle : `DoubleFunction<R>`, `DoubleConsumer`, `DoubleUnaryOperator`, `LongFunction<R>`, etc.
* Retour de type primitif :
  * `IntSupplier` dont le type de retour est `int`, `DoubleSupplier` dont le type de retour est double, etc.
  * `ToIntFunction<T>` dont le type de retour est `int`, et sur le même modèle : `ToIntBiFunction<T,U>`, `ToLongFunction<T>`, `ToLongBiFunction<T,U>`, `ToDoubleFunction<T>`, etc.

#### ... Aux composées

##### Composition de prédicats

La composition de prédicats permet de créer un prédicat par la combinaison logique de plusieurs prédicats.

Prenons l'exemple suivant :

```java
Predicate<Sock> isRed;
Predicate<Sock> isBlue;
Predicate<Sock> isHoled;
```

Pour déterminer si une chaussette est rouge ou bleue et non trouée, on définit le prédicat ci-dessous :

```java
Predicate<Sock> isRedOrBlueWithoutHoles = 
  sock -> (isRed.test(sock) || 
           isBlue.test(sock)) 
          && !isHoled.test(sock);
```

L'interface `Predicate` propose des méthodes permettant une réécriture concise et naturelle de ce prédicat :

```java
Predicate<Sock> isRedWithoutHoles = 
  isRed
  .or(isBlue)
  .and(not(isHoled));
```

##### Composition de fonctions

En mathématiques, la composition consiste à créer une fonction par l'application d'une fonction au résultat d'une autre fonction. Par exemple :

```
f(x) = x + 1
g(x) = x²
f(g(x)) = (x²) + 1
g(f(x)) = (x + 1)²
```
où f(g(x)) et g(f(x)) sont des compositions de f et g.

En utilisant les fonctions du package `java.util.function`, on peut créer ces mêmes fonctions composées :

```java
Function<Integer,Integer> f = x -> x + 1;
Function<Integer,Integer> g = x -> x^2;
Function<Integer,Integer> composition1 = x -> 
  f.apply(
    g.apply(x)
  ); // f(g(x))
Function<Integer,Integer> composition2 = x -> 
  g.apply(
    f.apply(x)
  ); // g(f(x))
```

Vous conviendrez aisément que ces `apply` successifs ne sont pas d'une lisibilité à toute épreuve. Pour clarifier ces compositions, Java 8 propose pour les _function_ une interface plus commode :

```java
Function<Integer,Integer> composition1 = f.compose(g); ⓵
Function<Integer,Integer> composition2 = f.andThen(g); ⓶
```

La différence entre `compose` et `andThen` réside dans l'ordre d'évaluation des fonction :

⓵ équivalent à `g.andThen(f)`
⓶ équivalent à `g.compose(f)`


## Streams

L'API `Stream<T>` a été introduite par Java 8. Le `Stream` est un objet java qui permet de définir via une API une série de traitements à réaliser sur une collection ou tableau d'objets. 

Cette API utilise massivement lambdas et références de méthodes (que nous verrons plus tard). En revanche, aucune structure de boucle n'est nécessaire pour appliquer ces opérations.


Avec les `Streams`, la programmation java peut devenir déclarative au lieu d'être impérative : déclarer ce que doit faire un traitement et non comment il doit le faire. 

Dans ce cadre déclaratif, les fonctions sont manipulées comme n'importe quel objet java pour être passées en paramètre ou retournées.


Prenons un exemple :

```java
String[] tableau = {"toto", "titi", "tata", "toto", ""};
long count = Stream.of(tableau) ⓵
            .filter(item -> item.isBlank()) ⓶
            .distinct() ⓷
            .count(); ⓸
```
⓵ Création d'un `Stream` à partir du tableau.
⓶ Filtrage des éléments vide.
⓷ Suppression des doublons.
⓸ Décompte des éléments restants.

Ce programme ne déclare que les opérations à effectuer. À aucun moment la façon d'exécuter ces traitements n'est définie : pas de boucle, pas de variable locale pour stocker les résultats temporaires, etc. Cela crée une topologie.

Le `Stream` est maître des algorithmes à utiliser, de l'optimisation et même de l'ordonnancement des opérations. La documentation de chacune des méthodes du `Stream` précise les garanties qu'elles offrent, charge au développeur de les prendre en compte.

Stream signifie flux : c'est à dire qu'une fois qu'un `Stream` a été utilisé pour appliquer une succession d'opérations et a permis d'obtenir un résultat, il n'est plus réutilisable.

L'exemple suivant ne fonctionnera donc pas :

```java
String[] tableau = {"toto", "titi", "tata", "toto", ""};
Stream<String> stream = Stream.of(tableau);

long count = stream
            .filter(item -> item.isBlank())
            .distinct()
            .count();

long totosNumber = stream
        .filter(str -> "toto".equalsIgnoreCase(str))
        .count(); ⓵
```
⓵ :bomb: `Stream` déjà épuisé !

### Créer un Stream : générateurs de flux
Toute séquence d'éléments peut être transformée en `Stream` :

- Un tableau :

```java
String[] helloWorld = {"Hello", "stream", "world", "!"};
Stream<String> helloStream = Arrays.stream(helloWorld);
Stream<String> otherHelloStream = Stream.of(helloWorld);
```

- Une Collection :

```java
List<String> helloWorld = Arrays.asList("Hello", "stream", "world", "!");
Stream<String> helloStream = helloWorld.stream();
```

- Une suite numérique :

```java
IntStream zeroToHundred = IntStream.range(0, 100);
DoubleStream squaresOfTwo = DoubleStream
  .iterate(2, i -> i < 1000000, i -> i * 2);
```

* Un autre flux :

```java
Stream<String> fewWords = Stream.<String>builder()
        .add("words")
        .add("to")
        .add("add")
        .build();
Stream<String> filesLines = Files
  .lines(Path.of("/c/file-sample.txt"));
Stream<String> linesStartingWithAddedWords = 
  Stream.concat(fewWords, filesLines);
```

* Ou même rien du tout :

```java
Stream<Object> empty = Stream.empty();
```

### Appliquer des opérations : méthodes intermédiaires

Les méthodes intermédiaires sont l'ensemble des opérations applicables à un `Stream` et qui renvoient un `Stream`.

Puisqu'elles renvoient un `Stream`, elles peuvent être chaînées pour appliquer plusieurs méthodes intermédiaires successivement.

Ces méthodes utilisent une approche builder, c'est-à-dire que leur invocation permet de créer le pipeline de traitement qui sera _ou pas_ invoqué dans le futur.

> :warning: Les méthodes intermédiaires ne déclenchent aucune exécution. Elles ne font que configurer une future utilisation.

* `distinct` : ne conserve que les éléments non égaux du `Stream` initial.

```java
Stream<Character> letters = Stream.of('a', 'b', 'j', 'z', 'b');
Stream<Character> distinctLetters = letters.distinct(); // contains only 'a', 'b', 'j', 'z'
```

* `sorted` : Elle prend en paramètre un `Comparator` et trie les éléments selon leur ordre naturel. Il est possible de préciser l'ordre dans lequel les éléments doivent être triés en fournissant un Comparator en entrée.

```java
Stream<Character> letters = Stream.of('a', 'b', 'j', 'z', 'b');
letters.sorted();
// equivalent à
letters.sorted((someLetter, someOtherLetter) -> someLetter.compareTo(someOtherLetter));
// equivalent à
letters.sorted(Comparator.naturalOrder());
```
* `limit` / `skip` : `limit(x)` tronque le `Stream` à x éléments, tandis que `skip(x)` retire du `Stream` les x premiers éléments.

* `filter` : Prend en paramètre un `Predicate` et retire du `Stream` tous les éléments ne le respectant pas.

```java
Stream<String> namesContainingToto = Stream.of("Pierre-Toto", "Jean-Toto", null, "Tutu", "Toto")
        .filter(item -> Objects.nonNull(item))
        .filter(item -> item.contains("Toto"));
```

* `map` : Prend en paramètre une `Function` et l'applique sur chacun des éléments du `Stream`. Permet de passer d'un Stream<K> à un Stream<V>.

```java
PairOfSocks[] socks = {
        new PairOfSocks("blanc", 38),
        new PairOfSocks("bordeaux", 42),
        new PairOfSocks("bleu", 39)
};

Stream<PairOfSocks> socksStream = Arrays.stream(socks);
Stream<Integer> socksSizes = Arrays.stream(socks).map(pair -> pair.size);
Stream<String> socksColors = Arrays.stream(socks).map(pair -> pair.color);
Stream<PairOfSocks> biggerSocks = Arrays.stream(socks).map(pair -> new PairOfSocks(pair.color, pair.size + 1));
```

> `mapToInt`, `mapToDouble` et `mapToLong` sont des spécialisations de map qui imposent que le `Stream` résultant de l'application du map soit respectivement un `IntStream`, `DoubleStream`, `LongStream`. Ces `Streams` permettent de manipuler les types primitifs `int`, `double` et `long`.

* `flatMap` : transforme chaque élément du `Stream` en un autre `Stream` via la fonction passée en paramètre et retourne la concaténation de chacun des `Streams` obtenus.

```java
Stream<String> names = Stream.of("Pierre-Toto", "Jean-Toto", "Tutu", "Toto");
Stream<String> letters = names.flatMap(name -> Arrays.stream(name.split(""))); // splits into letters
```

Dans l'exemple ci-dessus, `letters` est un `Stream` composé de chacun des caractères présents dans chacun des noms de `names`. Il équivaut à :

```java
Stream<String> names = Stream.of("P","i","e","r","r","e","-","T","o","t","o", "J","e","a","n","-","T","o","t","o", "T","u","t","u", "T","o","t","o");
```

Le `Stream` d'origine n'a donc pas forcément la même cardinalité que le `Stream` résultant du `flatmap`.

* `peek` : Il permet de "jeter un coup d'oeil" sur les éléments du `Stream`, sans les transformer, et sans être final non plus. Dans la grande majorité des cas, si vous apercevez un `peek`, fuyez. La doc indique clairement qu'il s'agit  d'une méthode dédiée au debug. Par ailleurs le `peek` ne fonctionne que si une opération intermédiaire ou l'opération terminal a besoin de parcourir l'intégralité des éléments.

```java
List<String> l = Arrays.asList("A", "B", "C", "D");
     long count = l.stream().peek(item -> System.out.println(item)).count();⓵
```
⓵ Le `count` n'a pas besoin de parcourir la liste donc pas de parcours de liste, et donc le peek n'est pas déclenché.


### Obtenir un résultat : méthodes terminales

Les méthodes terminales sont les méthodes de l'API `Stream` qui ne renvoient pas un `Stream` : elles terminent donc les enchaînements d'opérations sur un `Stream`.

> :warning: Les méthodes terminales déclenchent l'exécution des traitements configurés via les méthodes intermédiaires, provoquant ainsi l'épuisement du `Stream`.


#### Dénombrer

* `count` :
  * dénombre les éléments présents dans le `Stream`.
  * type de retour : `long`

```java
PairOfSocks[] socks = {
        new PairOfSocks("blanc", 38),
        new PairOfSocks("bordeaux", 42),
        new PairOfSocks("bleu", 39)
};

long socksUnderSize40 = Stream.of(socks)
                            .filter(sock -> sock.size < 40)
                            .count(); // returns 2
```

#### Réduire

Les méthodes de réduction permettent de passer d'un `Stream` à un unique résultat.

* `allMatch` :
  * renvoie vrai si tous les éléments du `Stream` respectent le prédicat fourni en paramètre. Faux sinon.
  *  type de retour : `boolean`
* `anyMatch` :
  * renvoie vrai si au moins un élément du `Stream` respecte le prédicat fourni en paramètre. Faux sinon.
  * type de retour : `boolean`

```java
PairOfSocks[] socks = {
        new PairOfSocks("blanc", 38),
        new PairOfSocks("bordeaux", 42),
        new PairOfSocks("bleu", 39)
};

boolean existsSockSizeOver38 = Stream
     .of(socks)
     .anyMatch(sock -> sock.size > 38); // returns true
```

* `noneMatch` :
  * renvoie vrai si aucun élément du `Stream` ne respecte le prédicat fourni en paramètre. Faux sinon.
  * type de retour : `boolean`
* `reduce` :
  * renvoie un objet qui résulte de l'accumulation de tous les éléments du `Stream` via le BiOperator donné en entrée d
  *  type de retour : `boolean`

```java
PairOfSocks[] socks = {
        new PairOfSocks("blanc", 38),
        new PairOfSocks("bordeaux", 42),
        new PairOfSocks("bleu", 39)
};

PairOfSocks neutralSock = new PairOfSocks("gris", 40);
BinaryOperator<PairOfSocks> makePatchworkSocks = (someSocks, someOtherSocks) -> new PairOfSocks(someSocks.color + "," + someOtherSocks.color, someSocks.size);

PairOfSocks patchworkSocks = Arrays.stream(socks)
                                .reduce(neutralSock, makePatchworkSocks); // returns a PairOfSocks("gris,blanc,bordeaux,bleu", 40)
```

* `collect` : rassemble tous les éléments du `Stream` dans un nouvel objet, tel qu'une List, une Map ou une String par exemple.
    * prend en paramètre un collecteur de type `Collector`. `Collectors` propose plusieurs implémentations usuelles telles que tolist().

```java
PairOfSocks[] socks = {
        new PairOfSocks("blanc", 38),
        new PairOfSocks("bordeaux", 42),
        new PairOfSocks("bleu", 39)
};

List<String> availableColors = Stream.of(socks)
   .map(sock -> sock.color) // only keep colors
   .collect(Collectors.toList()); // make a list of them

Map<String,Integer> availableSizesByColor = Stream.of(socks)                                                  
   .collect(Collectors.toMap(
       sock -> sock.color, // take sock colors as keys
       sock -> sock.size // take sock sizes as values
   )); // Only works if there are no color duplicates
```

#### Rechercher

Les méthodes de recherche d'éléments renvoient des `Optional<T>`. Un `Optional` encapsule un objet java (ou du vide) et fournit une API qui permet mettre en place des traitements ne dépendants pas de la nullité de l'objet, sans mettre en place de structure conditionnelle.

Pour un stream de type `Stream<T>` :

* `findAny` :
  * renvoie n'importe quel élément du `Stream`.
  * type de retour : `Optional<T>`
* `findFirst`:
  * renvoie le premier élément du `Stream`
  * type de retour : `Optional<T>`
* `max`:
  * renvoie l'élément le plus grand du `Stream`, selon le comparateur passé en paramètre
  * type de retour : `Optional<T>`
* `min`:
  * renvoie l'élément le plus petit du `Stream`, selon le comparateur passé en paramètre
  * type de retour : `Optional<T>`

### Découpage

Rappelons que les méthodes intermédiaires ne font effectivement aucun traitement. Si l'on doit effectuer de nombreuses transformations, il peut être  plus lisible de scinder le code :

```java
Stream<PairOfSocks> socksStream = socks.stream();

Stream<PairOfSocks> usedSocksStream = socksStream
        .filter(item -> item.used);

Stream<PairOfSocks> smallUsedSocksStream = usedSocksStream
        .filter(item -> item.size < 35);

Map<Integer, List<PairOfSocks>> pairOfSocksBySize =  smallUsedSocksStream
        .collect(Collectors.groupingBy(item -> item.size));
```

## Oui mais alors on a perdu les noms ?

Comme chacun le sait, après le choix entre espace et tabulation, un nommage correct reste souvent un des meilleurs moyens d'avoir du code lisible.

Or a priori les merveilleuses lambdas nous ont fait perdre nos noms !

### Référence de méthode

Imaginons que nous ayons une liste de chaînes de caractères myList de type List<String>, dont nous souhaitons afficher chacun des éléments sur la sortie standard.
L'implémentation naïve serait la suivante :

```java
for (String element : myList) {
    System.out.println(element);
}
```

Mais maintenant que nous connaissons les lambdas, passons à une version plus concise :

```java
myList.forEach(element -> System.out.println(element));
// pour chaque élément que l'on appelera "element" de myList,
// appliquer la méthode System.out.println avec comme paramètre d'entrée "element"
```

La méthode forEach applique à chaque élément de la liste le `Consumer<String>` fourni en entrée : fonction qui prend une `String` en entrée et ne renvoie rien.

On pourrait amplement se satisfaire de cette version. Mais poussons encore légèrement le curseur de la concision. 
En effet, le `forEach` itère sur une simple liste de chaînes de caractères : le `Consumer` prendra forcément un élément de cette liste en paramètre d'entrée. Seule la méthode à appliquer aux éléments nous donne de l'information :

```java
myList.forEach(System.out::println); //  à chaque élément de myList, appliquer la méthode println issue de la classe System.out
```

On vient alors d'utiliser une référence de méthode. De manière générale, les références de méthodes s'effectuent ainsi :

```
 <nom de la classe ou de l'instance>::<nom de la méthode>
```

On peut les utiliser si notre lambda comporte pour seule instruction un appel de méthode et une seule variable.

```java
myList.forEach(item -> item.toString()); // équivaut à :
myList.forEach(String::toString);

myList.forEach(element -> System.out.println(element)); // équivaut à :
myList.forEach(System.out::println);
```

### Lambdas nommées

Imaginons désormais que nous souhaitions concaténer notre élément à une autre chaîne de caractères lors de l'affichage :
```java
myList.forEach(element -> System.out.println("This is one element of my list: " + element));
```
Il n'est pas possible de faire référence à la méthode _println_, puisque son paramètre d'entrée ne peut plus être implicite.

En revanche, si nous avons plusieurs listes sur lesquelles appliquer ce traitement, il est possible de mutualiser la déclaration de notre expression lambda. Il faudra pour cela, créer une variable correctement typée et l'initialiser avec une lambda.

```java
myList.forEach(element -> System.out.println("This is one element of my list: " + element));
theirList.forEach(element -> System.out.println("This is one element of my list: " + element));
```

peut devenir :

```java
Consumer<String> elementPrinter = element -> System.out.println("This is one element of my list: " + element);

myList.forEach(elementPrinter);
theirList.forEach(elementPrinter);
```

On a pu extraire l'expression lambda dans une variable de type `Consumer<String>` et ainsi la nommer et la réutiliser.

Une version qu'on voit moins souvent -et c'est bien dommage- nous permet d'utiliser les méthodes de sa propre classe.

Je peux écrire :

```java
public void printElements(List<String> myList, List<String> theirList) {
    myList.forEach(item -> this.printElement(item));
    theirList.forEach(item -> this.printElement(item));
}

private void printElement(String element) {
        System.out.println("This is one element of my list: " + element);
    }
```

Or comme on l'a vu, si la lambda contient pour seule instruction un appel de méthode, on peut utiliser les méthodes références :

```java
public void printElements(List<String> myList, List<String> theirList) {
    myList.forEach(this::printElement);
    theirList.forEach(this::printElement);
}

private void printElement(String element) {
    System.out.println("This is one element of my list: " + element);
}
```


## Cas réel

### Cas 1

Ainsi lors d'une revue, nous sommes tombés sur le code suivant :

```java
public Map<String, List<Record>> getMap(List<Record> records, boolean sortByAnalysis) {
    Map<String, List<Record>> groupedRecords = new HashMap<>(); ⓵
    for (Record record : records) { ⓶
        String keyword = sortByAnalysis ? record.getAnalysisName() : record.getJobId(); ⓷
        if (!groupedRecords.containsKey(keyword)) { ⓸
            groupedRecords.put(keyword, new ArrayList<>()); ⓹
        }
        groupedRecords.get(keyword).add(record); ⓺
    }
    return groupedRecords;
}
```
⓵ On instancie la `map` que l'on va retourner
⓶ On boucle sur la liste
⓷ On extraie la clef de regroupement
⓸ Si la `map` ne contient pas encore la clef
⓹ On l'ajoute avec  une liste vide
⓺ On ajoute l'élément à la liste présente à cette clef

En utilisant les apis à notre disposition, nous avons effectué la réécriture suivante :

```java
public Map<String, List<Record>> getMap(List<Record> records, boolean sortByAnalysis) {
    Function<Record, String> classifier = sortByAnalysis ? Record::getAnalysisName : Record::getJobId; ⓵
    return records.stream().collect(Collectors.groupingBy(classifier)); ⓶
}
```
⓵ On initialise le `classifier`
⓶ On regarde la plateforme travailler.


### Cas 2

Regardons un autre exemple de code également réel, dont le jargon métier a été modifié :

```java
public boolean hasRedSocks(Home home) {
    return home.getRooms().stream()
            .filter(room -> room.getName().equals("bedroom"))
            .findAny()
            .flatMap(room -> room.getFurnitures().stream()
                    .filter(furniture -> furniture.getName().equals("sock drawer"))
                    .findAny().flatMap(furniture -> furniture.getClothes().stream()
                            .filter(clothe -> clothe instanceof Sock)
                            .map(clothe -> ((Sock) clothe).getMaterial())
                            .filter(material -> material instanceof Cotton
                                    && ((Cotton) material).getColor().equals("red"))
                            .findAny()
                            .map(o -> true)))
            .orElse(false);
}
```

Alors, sceptique ? Pourtant tout cela nous semble très clair
!
En effet, l'api `Stream` utilisée à mauvais escient devient tout à fait indigeste. Considérez les trois réécritures suivantes :


#### Mieux avec lambda

```java
public boolean hasRedSock1(Home home) { ⓵
    return home.getRooms()
            .stream()
            .anyMatch(room -> isBedroom(room) && containsRedSock1(room));
}

private boolean containsRedSock1(Room room) {
    return room.getFurnitures()
            .stream()
            .anyMatch(furniture -> isSockDrawer(furniture) && containsRedSock1(furniture));
}

private boolean containsRedSock1(Furniture furniture) {
    return furniture.getClothes()
            .stream()
            .anyMatch(this::isARedSock);
}
```

#### Mieux sans lambda

```java
public boolean hasRedSock2(Home home) { ⓶
    for (Room room : home.getRooms()) {
        if (isBedroom(room) && containsRedSock2(room)) {
            return true;
        }
    }
    return false;
}

private boolean containsRedSock2(Room room) {
    for (Furniture furniture : room.getFurnitures()) {
        if (isSockDrawer(furniture) && containsRedSock2(furniture)) {
            return true;
        }
    }
    return false;
}

private boolean containsRedSock2(Furniture furniture) {
    for (Clothe clothe : furniture.getClothes()) {
        if (isARedSock(clothe)) {
            return true;
        }
    }
    return false;}
```

#### Mieux nommage

```java
public boolean hasRedSock3(Home home) { ⓷
    Stream<Room> bedRooms = home.getRooms().stream()
            .filter(this::isBedroom); // Retains only bedrooms
    Stream<Furniture> sockDrawers = bedRooms
            .map(Room::getFurnitures) // Gets the list of furnitures
            .flatMap(List::stream) // Turns them to stream
            .filter(this::isSockDrawer); // Filter on sock drawer
    Stream<Clothe> clothes = sockDrawers
            .map(Furniture::getClothes) // Get clothes
            .flatMap(List::stream);
    return clothes
            .anyMatch(this::isARedSock); // Has at least one red sock
}
```

#### Et le code commun aux réécritures :

```java
private boolean isBedroom(Room room) {
    return room.getName().equals("bedroom");
}

private boolean isSockDrawer(Furniture furniture) {
    return furniture.getName().equals("sock drawer");
}

private boolean isARedSock(Clothe clothe) {
    return clothe instanceof Sock &&
            ((Sock) clothe).getMaterial() instanceof Cotton
            && ((Cotton) ((Sock) clothe).getMaterial()).getColor().equals("red");
}
```

Les réécritures ⓵ et ⓷ utilisent l'api `Stream` tandis que la ⓶ utilise des boucles _for_ et conditions basiques du langage.

Bien que moins concise, la réécriture ⓶ n'en est pas moins lisible que les autres.

Le gain en lisibilité obtenu via ces réécritures ne découle que de la réorganisation et de l'extraction de la logique métier dans des méthodes simples, et non pas de l'utilisation ou non des fonctionnalités de Java 8.

Les nouveautés ne sont pas forcément mieux, ni forcément mauvaises. Et c'est la force d'un bon développeur que de savoir quand et où les utiliser.

## Conclusion

Avec c'est article qui ne fait que gratter la surface, nous espérons avant tout donner des clefs pour comprendre les mécanismes à l'œuvre dans les streams, et les APIs fonctionnelles.

Nous pourrions encore explorer la programmation fonctionnelle (ou ce qu'il est possible de faire depuis Java8), nous pencher sur les exécutions de stream en parallèle, ou sur les optimisations faites par le compilateur java.

Mais avant tout ce qui nous intéresse ici, c'est de lever le voile sur la _magie noire_ et de vous donner envie d'aller creuser vous mêmes les merveilleuses arcanes suprêmes de la connaissance du java.

Bon voyage.

Avec la merveilleuse [Lucile Thienot](https://www.linkedin.com/in/lucile-thienot)