<!--
Lambda
Stream api
optional
-->
> [!warning] Ce cours ne fera pas l'objet de l'évaluation
## Stream API
Depuis Java 8, le langage apporte un style de programmation fonctionnelle : l'API Stream.
Une syntaxe assez similaire à SQL : Flux de données -> transformations -> conversion
> [!warning] À ne pas confondre avec les stream des [[IO en Java]]
Elle propose une capacité de
- Passer des fonctions en arguments de méthodes
- Affecter des fonctions à des variables
- Un **pipelining** de traitements pouvant **assemblés** par étapes simples
- Une syntaxe **déclarative** plutôt que **impérative**
- Utiliser des références de fonctions
### Lambda
Les lambda sont des fonctions qu'on écrit à la volée.
On peut les composer entre elles.
```java
List<String> chasseurs = List.of(
"Dédé",
"Robert",
"Oriane",
"Gilberte",
"Robin",
"Marion");
Stream<String> streamDeChasseurs = chasseurs.stream();
String chasseursSeparesParVirgule =
streamDeChasseurs
.filter(c -> c.contains("a")) // ["Oriane", "Marion"]
.map(c -> c.toUpperCase()) // ["ORIANE", "MARION"]
.collect(Collectors.joining(", ")); // "ORIANE, MARION"
System.out.println(chasseursSeparesParVirgule);
// ORIANE, MARION
```
### Créer un stream
```java
// Créer un stream depuis une collection
Stream<String> stream = List.of("A", "B", "C").stream();
// Créer un stream depuis un tableau
Stream<String> stream = Arrays.stream(new String[]{"A", "B"});
// Créer un stream depuis varargs
Stream<String> stream = Stream.of("A", "B");
```
### filter()
Permet d'enlever des éléments d'un stream selon un critère
```java
// Ne garde que les chasseur qui ont "a" dans leur nom
stream.filter(chasseur -> chasseur.contains("a"))
```
### map()
Transformer un élément
```java
// transforme chaque chasseur en majuscule
stream.map(chasseur -> chasseur.toUpperString())
```
### reduce()
Pour réduire des résultats en les accumulant
```java
List<Integer> entiers = List.of(1, 3, 5);
{
int somme = 0;
for (Integer entier : entiers) {
somme = somme + entier;
}
}
{
int somme = entiers.stream()
.mapToInt(entier -> entier)
.sum();
}
{
int somme = entiers.stream()
.reduce(0, (integer, integer2) -> integer + integer2);
System.out.println("3 - " + somme);
}
{
int somme = entiers.stream()
.reduce(0, Integer::sum);
System.out.println("3 - " + somme);
}
```
### collect()
Pour convertir un stream à la fin de celui-ci.
Transformer une liste en chaine de caractères séparée par virgules
```java
List<String> chasseurs = List.of(
"Dédé",
"Robert",
"Oriane",
"Gilberte",
"Robin",
"Marion");
{
String chasseursSeparesParVirgule =
chasseurs.stream()
// Convertir le string en chaine de caractères en intercalant une virgule
.collect(Collectors.joining(", ")); // "ORIANE, MARION"
System.out.println(chasseursSeparesParVirgule);
// ORIANE, MARION
}
```
Transformer une liste en liste HTML
```java
{
String chasseursEnListeHtml =
chasseurs.stream()
.map(s -> "\t<li>"+s+"</li>") // <li>chasseur</li>
.collect(Collectors.joining("\n", "<ul>\n", "\n</ul>"));
System.out.println(chasseursEnListeHtml);
// <ul>
// <li>Dédé</li>
// <li>Robert</li>
// <li>Oriane</li>
// <li>Gilberte</li>
// <li>Robin</li>
// <li>Marion</li>
//</ul>
}
```
Transformer en liste
```java
List<String> listeDeChasseurs = chasseurs.stream()
.collect(Collectors.toList()); // ["ORIANE, MARION"]
// Equivalent
List<String> listeDeChasseurs = chasseurs.stream().toList();
```
#### `groupingBy()`
```java
Map<Region, Integer> sumByRegion = chasseurs
.stream()
.collect(Collectors.groupingBy(
Chasseur::region,
Collectors.summingInt(Chasseur::galinettesEues)));
System.out.println(sumByRegion);
// {BOURGOGNE=11, BOUCHONNOIS=3, ALSACE=7}
```
## Streams parallèles
```java
Stream<String> stream = chasseurs.parallelStream();
Stream<String> stream = chasseurs.stream().parallel();
```
## Référence de methode
```java
// Mettre les éléments du stream en majuscule
stream.map(String::toUpperString)
// Equivalent à
stream.map(chasseur -> chasseur.toUpperString())
```
### La Javadoc à la rescousse
> [!tip] La javadoc des Stream est très bien écrite et comporte des exemples de code
## Les outils automatiques à la rescousse
Des outils comme IntelliJ Idea proposent des actions automatiques pour passer d'un style à l'autre.
Ex.
Boucle for améliorée
```java
for (String chasseur : List.of("Bébert", "Dédé", "Roger")) {
System.out.println(chasseur);
}
```
Conversion en syntaxe de l'API stream
```java
List.of("Bébert", "Dédé", "Roger")
.forEach(chasseur -> System.out.println(chasseur));
```
Simplification avec une référence de fonction statique
```java
List.of("Bébert", "Dédé", "Roger")
.forEach(System.out::println);
// forEach expose 1 paramètre
// System.out::println accepte 1 paramètre
```
> [!example]- Refactor auto de boucle for vers foreach
![[idea-refactor-for-foreach.mp4]]
## Comparaison de styles : Somme des galinettes eues par région
Un style de programmation n'est pas forcément meilleur qu'un autre.
Il est à choisir selon les besoins.
### En style impératif
```java
var sumByRegion = new HashMap<Region, Integer>();
for (Chasseur chasseur : chasseurs) {
Region region = chasseur.region();
if (!sumByRegion.containsKey(region)) {
sumByRegion.put(region, 0);
}
int newSumForRegion = chasseur.galinettesEues() + sumByRegion.get(region);
sumByRegion.put(region, newSumForRegion);
}
System.out.println(sumByRegion);
// {ALSACE=7, BOURGOGNE=11, BOUCHONNOIS=3}
```
Simplification avec `Map::merge`
```java
var sumByRegion = new HashMap<Region, Integer>();
for (Chasseur chasseur : chasseurs) {
Region region = chasseur.region();
int galinettesEues = chasseur.galinettesEues;
sumByRegion.merge(region, galinettesEues, (a, b) -> a + b);
}
System.out.println(sumByRegion);
// {BOUCHONNOIS=3, ALSACE=7, BOURGOGNE=11}
```
Simplification avec `Integer::sum`
```java
var sumByRegion = new HashMap<Region, Integer>();
for (Chasseur chasseur : chasseurs) {
Region region = chasseur.region();
int galinettesEues = chasseur.galinettesEues;
sumByRegion.merge(region, galinettesEues, Integer::sum);
}
System.out.println(sumByRegion);
// {BOURGOGNE=11, BOUCHONNOIS=3, ALSACE=7}
```
### En style fonctionnel avec l’API Stream
```java
Map<Region, Integer> sumByRegion = chasseurs.stream()
.collect(Collectors.groupingBy(
Chasseur::region,
Collectors.summingInt(Chasseur::galinettesEues)));
System.out.println(sumByRegion);
// {BOURGOGNE=11, BOUCHONNOIS=3, ALSACE=7}
```
Amélioration de la lisibilité avec import statique des collectors
```java
var sumByRegion = chasseurs.stream()
.collect(
groupingBy(
Chasseur::region,
summingInt(Chasseur::galinettesEues)));
System.out.println(sumByRegion);
// {BOURGOGNE=11, BOUCHONNOIS=3, ALSACE=7}
```
Somme des galinettes eues par région
Pour aller plus loin : https://www.baeldung.com/java-groupingby-collector
## Optional
Si on ne vérifie pas qu'une valeur est `null` on peut avoir une `NullPointerException`.
```java
String texte = methodeQuiPeutRetournerNull();
// Si texte est null : NullPointerException
texte.toUpperCase();
```
Quand on veut éviter les problèmes de `NullPointerException` , on voit souvent ce genre de code :
```java
String texte = methodeQuiPeutRetournerNull();
if(texte!= null){
// Faire quelque-chose
}
```
> [!warning] Le compilateur ne nous oblige pas à le faire, et on peut donc oublier.
### Optional à la rescousse !
Pour **obliger l'appelant** de la méthode à vérifier si la valeur existe, on peut l'envelopper dans un type `Optional`.
Le compilateur nous **oblige à vérifier la présence** de la valeur pour pouvoir l'utiliser.
```java
Optional<String> texteOptionnel = methodeQuiRetourneOptional();
if(texteOptionnel.isPresent()){
String texte = texteOptionnel.get();
// Faire quelque-chose
}
```
Un `Optional`, C'est un peu comme une variable de Schrödinger
### Créer un `Optional`
#### `Optional.of()`
Si on sait que la valeur est non null
```java
String toto="J'existe";
Optional.of(toto);
```
#### `Optional.ofNullable()`
Si on n'est pas sûr que la valeur existe ou est `null`
```java
public void someMethdod(String toto){
Optional.ofNullable(toto);
}
```
#### `Optional.empty()`
Si on veut indiquer qu'il n'y a pas de valeur.
Ex. quand un programme peut avoir un premier argument optionnel
```java
static void main(String[] args) {
Optional<String> optionalFirstArg;
if(args.length>0){
optionalFirstArg = Optional.of(args[0]);
}else {
optionalFirstArg = Optional.empty();
}
}
```
### Vérifier si une valeur est présente
```java
Optional<String> optional = methodeQuiRetourneOptional();
if(optional.isPresent()){
// ...
}
```
```java
Optional<String> optional = methodeQuiRetourneOptional();
optional.ifPresent(texte -> {
// ...
});
```
### Récupérer la valeur
`optional.get()`
```java
Optional<String> optional = methodeQuiRetourneOptional();
if(optional.isPresent()){
String texte = optional.get();
IO.println(texte);
}
```
Valeur de la lambda dans `ifPresent()`
```java
Optional<String> optional = methodeQuiRetourneOptional();
optional.ifPresent(texte -> {
IO.println(texte);
});
```
### Faire un traitement si la valeur est présente
Valeur de la lambda dans `ifPresent()`
```java
Optional<String> optional = methodeQuiRetourneOptional();
optional.ifPresent(texte -> {
IO.println(texte);
});
// Alternative pour faire la même chose
// référence de fonction
optional.ifPresent(IO::println);
```
### Faire un traitement si present ou pas
Valeur de la lambda dans `ifPresent()`
```java
Optional<String> optional = methodeQuiRetourneOptional();
optional.ifPresentOrElse(
// then
texte -> System.out.println(texte),
// else
() -> System.err.println("Je n'ai pas trouvé de valeur")
);
```
### Chainage de lanmbdas
Optional permet d'utiliser des opérations de chainage similaires au stream.
Comme
- `filter()`
- `map()`
```java
String texte =
optional
// Si le texte contient "e"
.filter(texte -> texte.contains("e"))
// Si le texte est présent on met en majuscule
.map(texte -> texte.toUpperCase())
// Sinon valeur par défaut
.orElse("VALEUR PAR DEFAUT");
System.out.println("5 - " +texte);
```
### Cas d'utilisation de Optional
Quand on récupère **une donnée qui peut être absente** d'une base de données
```java
@Override
public Optional<RpgCharacterData> find(int id) {
try (PreparedStatement statement = connection.prepareStatement("""
SELECT id, name, hp, def, money, atk, heal, job
FROM rpg_character
WHERE id = ?
""")
) {
statement.setInt(1, id);
ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
return Optional.of(toRpgCharacterData(resultSet));
} else {
return Optional.empty();
}
} catch (SQLException e) {
throw new TechnicalException(DATABASE_ERROR, e);
}
}
```
Quand un programme peut avoir un premier argument optionnel
```java
static void main(String[] args) {
Optional<String> optionalFirstArg;
if(args.length>0){
optionalFirstArg = Optional.of(args[0]);
}else {
optionalFirstArg = Optional.empty();
}
}
```
### Préconisations
Ne pas stocker des optional dans des attributs.
> [!danger] A éviter
> ```java
> class Toto {
>
> // Pas bien
> Optional<String> op;
>
> Optional<String> getOp(){
> return this.op;
> }
> }
> ```
Seulement dans des retours de methodes
> [!success] Créer l'optional au retour de l'appel de méthode
> ```java
> class Toto {
>
> // Bien
> String op;
>
> Optional<String> getOp(){
> return Optional.ofNullable(this.op);
> }
> }
> ```
## Comparateurs
```java
Comparator<Person> byLastName = Comparator.comparing(Person::getLastName);
Comparator<Person> byLastName = Comparator.comparing(Person::getLastName)
.reversed();
```
Exemple plus complet
```java
import java.util.stream.Stream;
import static java.lang.Math.abs;
import static java.lang.Math.pow;
import static java.lang.Math.sqrt;
import static java.util.Comparator.comparing;
class Scratch {
public static void main(String[] args) {
// Comparator.comparingDouble(Segment2D::length)
Segment2D longestSegment = Stream.of(
new Segment2D(new Point(0, 0), new Point(0, 0)),
new Segment2D(new Point(0, 1), new Point(1, 0)),
new Segment2D(new Point(0, 2), new Point(2, 0)),
new Segment2D(new Point(0, 3), new Point(3, 0)),
new Segment2D(new Point(0, 4), new Point(4, 0)),
new Segment2D(new Point(0, 5), new Point(5, 0)),
new Segment2D(new Point(0, 6), new Point(6, 0)),
new Segment2D(new Point(0, 7), new Point(7, 0)),
new Segment2D(new Point(0, 8), new Point(8, 0)),
new Segment2D(new Point(0, 9), new Point(9, 0)),
new Segment2D(new Point(0, 10), new Point(10, 0)),
new Segment2D(new Point(1, 0), new Point(0, 0)),
new Segment2D(new Point(1, 1), new Point(1, 0)),
new Segment2D(new Point(1, 2), new Point(2, 0)),
new Segment2D(new Point(1, 3), new Point(3, 0)),
new Segment2D(new Point(1, 4), new Point(4, 0)),
new Segment2D(new Point(1, 5), new Point(5, 0)),
new Segment2D(new Point(1, 6), new Point(6, 0)),
new Segment2D(new Point(1, 7), new Point(7, 0)),
new Segment2D(new Point(1, 8), new Point(8, 0)),
new Segment2D(new Point(1, 9), new Point(9, 0)),
new Segment2D(new Point(1, 10), new Point(10, 0))
).max(comparing(Segment2D::length)).orElseThrow();
System.out.println("Longest segment : " + longestSegment);
}
record Segment2D(Point p1, Point p2) {
double length() {
// Length of segment in 2D plan using Pythagore theorem
// A (p1)
//
// C B (p2)
// AC = abs(p1.y - p2.y)
// BC = abs(p1.x - p2.x)
// AB^2 = AC^2 + BC^2
// AB = sqrt(AB^2)
double ac = abs(p1.y() - p2.y());
double bc = abs(p1.x() - p2.x());
return sqrt(pow(ac, 2) + pow(bc, 2));
}
}
record Point(int x, int y) {
}
}
```
<!--
## Comparator
## Runnable
## Predicate
## À suivre
[[Design Patterns]]
-->