<!-- 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]] -->