Skip to content

API Stream

Programmation fonctionnelle

Comme vous le savez, Java est un langage de programmation multi-paradigme. Vous avez pu traiter la programmation orientée objet, la programmation procédurale et nous allons désormais découvrir la programmation fonctionnelle.

Cette approche a été renforcée dans Java 8 avec l'introduction d'une nouvelle amélioration syntaxique puissante sous la forme d'expressions lambda.

Lambda

Une expression lambda est une fonction anonyme que nous pouvons traiter de la même manière que n'importe quel autre objet. Cela signifie qu'elle peut être utilisée de manière flexible, telle que la passer en tant qu'argument de méthode ou encore en valeur de retour.

L'interface fonctionnelle est un exemple qui permet de bien comprendre le principe de lambda. Voici un exemple de BiFunction :

public class Main {
    public static void main(String[] args) {
        // Les lambda sont ce que vous observez avec la notation fléchée
        // Addition
        // L'instance du lambda est stockée dans une variable
        BiFunction<Double, Double, Double> addition = (a, b) -> { 
            return a + b; 
        };
        System.out.println("Addition : " + addition.apply(5.0, 3.0));

        // Multiplication
        BiFunction<Double, Double, Double> multiplication = (a, b) -> a * b;
        System.out.println("Multiplication : " + multiplication.apply(4.0, 2.5));
    }
}

Voici quelques points à savoir sur les lambda :

  • Il n'est pas obligatoire de mettre les parenthèses s'il n'y a qu'un seul paramètre.
  • Vous pouvez omettre le bloc de code après la flèche s'il n'y a qu'une seule instruction return (cf. ligne 12).

Le plus souvent, les lambda iront de pair avec un autre concept Java apparu dans la même version : les streams. Les streams sont une extension naturelle des lambdas, offrant une approche de code plus lisible et maintenable pour manipuler des collections de données.

Streams

Qu'est-ce qu'un Stream ?

Un Stream est un objet qui encapsule un flux de données sur lequel on peut effectuer des opérations. Son utilisation est très répandue, voire privilégiée pour beaucoup de cas d'utilisation.

Il existe deux types d'opérations que l'on peut effectuer sur des streams :

  • les opérations terminales : on récupère le résultat à l'issue de l'opération
  • les opérations intermédiaires : opérations intermédiaires qui modifient le flux

Concrètement, là où l'on aurait utilisé une boucle for sur une collection pour modifier ses éléments, nous allons désormais préférer utiliser des streams et appliquer des fonctions de modification sur les différents éléments de la collection.

Toutes les collections qui étendent l'API Collection proposent une méthode stream() qui permet de récupérer un objet Stream sur lequel appliquer des opérations.

Opérations sur les Streams

Opérations terminales :

  • reduce(identity, accumulator, combiner) : combine les éléments d'une collection en utilisant une valeur initiale, une opération d'accumulation et une opération de combinaison
  • forEach(element) : itère sur la collection avec element comme valeur courante
  • collect : permet de transformer le stream en une valeur (List, Double, ...)
  • min/max(comparator) : renvoie un Optional qui contient la valeur min/max de la collection trouvée en appliquant comparator à tous les éléments deux à deux
  • anyMatch(predicate) : évalue le fait qu'au moins un élément de la collection valide le predicate
  • findFirst : renvoie un Optional du premier élément du Stream
  • findAny : renvoie un Optional d'un élément du Stream

Opérations intermédiaires :

  • filter(predicate) : renvoie une sous collection des éléments qui valident le predicate
  • map(mapping) : renvoie une collection où l'on a appliqué mapping à tous les éléments
  • distinct : renvoie un Stream qui ne contient pas de doublons
  • sorted : trie les éléments du Stream
  • limit(n) : limite le Stream à n éléments

Exemple

Étant donné la collection suivante :

List<String> chars = List.of("20", "35", "", "14", " ", "51");
On veut écrire un stream qui renvoie la somme des tailles des chaînes de caractères présentes dans la liste.

chars.stream()
         .filter(c -> !c.isBlank())
         .mapToInt(c -> c.length())
         .sum()
Ce stream enlève les valeurs vides et calcule la somme des tailles des chaînes de caractères présentes dans la liste.

Requêtage de notre application

Maintenant que vous avez appréhendé les Streams, il est l'heure d'ajouter des fonctionnalités de requêtage à notre application.

Nous vous proposons un jeu de données à ajouter avant de commencer cette partie.

À vous de jouer

Il faudra, pour chaque fonctionnalité, coder une méthode dans le service associé à la fonctionnalité. Quasiment toutes ces méthodes prendront un paramatre List<Travel>.

Voici les différentes fonctionnalités :

  • Notre agence doit pouvoir exposer la liste de tous les voyages disponibles actuellement (donc pour lesquels il reste encore des places)
    • Dans un second temps, nous voulons pouvoir paramétrer cette liste de voyages disponibles par une localisation. Exemple : tous les voyages disponibles à destination de Tokyo
  • Nos utilisateurs aimeraient faire une recherche détaillée sur les voyages que propose l'agence dans une certaine fourchette de prix.
  • Nos utilisateurs aimeraient avoir le voyage le plus proche de la date actuelle.

Optionnel

Si vous avez bien progressé et qu'il vous reste du temps, continuez à implémenter ces fonctionnalités :

  • L'agence doit pouvoir lister la liste des n premiers voyages triés par ordre croissant de prix.
  • Elle devra pouvoir également lister tous les voyages disponibles avec une promotion en cours.
  • Nous voulons que l'agence puisse lister tous les voyages auxquels le User Jean-Michel est inscrit (Il faudrait pouvoir filtrer sur les user qui match la condition).

  • Il y a un nouveau CEO dans l'entreprise. Il veut pouvoir évaluer la rentabilité de notre agence et il souhaiterait obtenir la moyenne du prix des voyages proposés par l'agence.

Essayez votre code dans le Main

À vous de jouer

Vous pouvez maintenant tester votre service dans le main.

...
List<Travel> findAvailableTravels (List<Travel> travels){
    ...
}
...
...
// Ma liste de voyages
List<Travel> travels = new LinkedList();
travels.add(new Travel(0, "To New York", Airport.PARIS_CHARLES_DE_GAULLE, Airport.NEW_YORK_JFK, 
                       Instant.now().plus(Duration.ofDays(2)), 
                       Instant.now().plus(Duration.ofDays(15)), "NewYork", 6, 3500));
travels.add(new Travel(0, "Paris is next", Airport.TOKYO_HANEDA, Airport.PARIS_ORLY, 
                       Instant.now().minus(Duration.ofDays(2)),
                       Instant.now().plus(Duration.ofDays(5)),"Paris", 10, 2900));

// instancier mon service
TravelService travelService = new TravelService();

// récupérer que les voyages disponible
List<Travel> availableTravels = findAvailableTravels(travels);
...

Récapitulatif

Excellent, nous avons clôturé la partie sur les streams et vous avez pu créer vos programmes de recherche.

Check

.
└── service
│   └── TravelService.java
└── Main.java`

N'oubliez pas de Commit votre travail !

Conclusion

On est arrivé à la fin de cette première partie de la formation. À demain pour de nouvelles aventures !