:: Enseignements :: Master :: M1 :: 2024-2025 :: Java Avancé ::
[LOGO]

L'API des gatherers


Ce TD a pour but de découvrir et d'utiliser l'API des Gatherers pour implanter plusieurs opérations intermédiaires sur les Streams.
Cela permet aussi de réviser les lambdas, les classes internes, les types paramétrés, les wildcards et la capture de type paramétré.

Exercice 1 - Maven

Comme pour les TPs précédents, nous allons utiliser Maven comme outil de build. Comme les gatherers sont en preview en Java 23, il faut pas oublier en --enable-preview.

      
Comme précédemment, créer un projet Maven en cochant create simple project au niveau du premier écran, puis passer à l'écran suivant en indiquant Next.
Pour ce TP, le groupId est fr.uge.gatherer , l'artefactId est gatherer et la version est 0.0.1-SNAPSHOT. Pour finir, cliquer sur Finish.

Exercice 2 - GathererDemo

L'exercice consiste à écrire des Gathererers de plus en plus compliqués sous forme de méthodes statiques dans la classe GathererDemo.

Les tests unitaires sont dans la classe GathererDemoTest.java.

  1. On souhaite écrire une méthode filterIntegers qui permet de filtrer un Stream d'Integer, pour garder uniquement que les entiers qui sont vrais pour la fonction prise en paramètre.
    var list = List.of(1, 2, 3, 7, 10);
    var result = list.stream().gather(GathererDemo.filterIntegers(x -> x % 2 == 0)).toList();
        

    Quelle est l'interface fonctionnelle dans java.util.fonction qui correspond à la fonction prise en paramètre de filterIntegers ?
    Quels sont les types des 3 paramètres du Gatherer que la méthode filterIntegers va renvoyer.
    Le Gatherer a-t-il besoin d'un état ?
    Quelle version de la méthode Gatherer.ofSequential() allons-nous utilisez ici ?
    Écrire la méthode filterIntegers.
    Vérifier que les tests marqués "Q1" passent.

  2. On veut maintenant écrire une méthode takeWhileIntegers qui renvoie un Gatherer qui fonctionne comme la méthode Stream.takeWile mais qui ne fonctionne que sur un Stream d'Integer.
    Comme pour la question précédente, quelle est l'interface fonctionnelle prise en paramètre de takeWhileIntegers ? Quel est le type de retour de takeWhileIntegers ? Quelle est la version de Gatherer.ofSequential() que allons nous utiliser ?
    Écrire la méthode takeWhileIntegers.
    Vérifier que les tests marqués "Q2" passent.
    Rappel : takeWhile() fait passer les éléments du Stream tant que la fonction prise en paramètre renvoie vrai.

  3. On souhaite écrire une méthode takeWhile qui, contrairement à la méthode précédente, fonctionne sur n'importe quel type de Stream.
    Quelle est l'interface fonctionnelle prise en paramètre de takeWhile ? Quel est le type de retour de takeWhile ? Quelle est la version de Gatherer.ofSequential() que allons nous utiliser ?
    Écrire la méthode takeWhile.
    Vérifier que les tests marqués "Q3" passent.

  4. On veut maintenant écrire une méthode indexed() qui renvoie un Gatherer qui ajoute à chaque élément un index correspondant à sa position dans le Stream (en commençant à 0).
    Par exemple
    var list = List.of("foo", "bar");
    var result = list.stream().gather(GathererDemo.indexed()).toList();
    System.out.println(result);  // [Indexed[element=foo, index=0], Indexed[element=bar, index=1]]
        

    Pour cela, nous allons créer, dans un premier temps, un record Indexed avec comme composants un élément (element) et un index (index.
    Écrire le record Indexed à l'intérieur de GathererDemo).
    Expliquer pourquoi le Gatherer renvoyé par indexed() à besoin d'un état. Quel est le type de retour de indexed ? Quelle est la version de Gatherer.ofSequential() que allons nous utiliser ?
    Écrire la méthode indexed.
    Vérifier que les tests marqués "Q4" passent.

  5. En fait, l'API que nous avons écrite n'est pas terrible car on force les utilisateurs à utiliser le record Indexed et pas leur propre structure de données.
    On se propose de créer une nouvelle version de indexed qui prend en paramètre une fonction qui prend en paramètre un élément et un index et renvoie un objet. Ainsi, l'utilisateur peut indiquer l'objet qu'il souhaite envoyer.
    Par exemple, si l'on veut que indexed(function) se comporte comme indexed, on va écrire le code suivant
    var list = List.of("foo", "bar");
    var result = list.stream().gather(GathererDemo.indexed(GathererDemo.Indexed::new)).toList();
    System.out.println(result);  // [Indexed[element=foo, index=0], Indexed[element=bar, index=1]]
        

    Écrire la nouvelle version de la méthode indexed.
    Vérifier que les tests marqués "Q5" passent.

  6. On souhaite écrire une méthode squashTwoIntegers(function) qui renvoie un Gatherer qui prend deux entiers consécutifs et les remplace par un seul entier, la valeur de l'entier étant déterminée par la fonction prise en paramètre.
    Voici un exemple d'utilisation :
    var list = List.of(1, 2, 3, 4);
    var result = list.stream().gather(GathererDemo.squashTwoIntegers(Integer::sum)).toList();
    System.out.println(result);  // [3, 7]
        

    Sachant que l'on veut planter si le nombre d'éléments dans le Stream n'est pas pair, quelle variation de ofSequential() doit-on utiliser ?
    Écrire la méthode squashTwoIntegers.
    Vérifier que les tests marqués "Q6" passent.
    Note: on veut grouper les éléments par 2, une liste n'est pas nécessaire pour cela !

  7. On souhaite généraliser la méthode squashTwoIntegers à n'importe quel type. Pour cela, on va écrire la méthode squashTwo(function) qui appelle la fonction prise en paramètre avec deux éléments de même type et renvoie un élement qui peut être d'un type différent.
    Comme précédemment, on plantera si le nombre d'éléments n'est pas pair.
    Écrire la méthode squashTwo.
    Vérifier que les tests marqués "Q7" passent.
    Attention : squashTwo(function) doit aussi fonctionner s'il y a des nulls.

  8. On veut généraliser squashTwo en permettant de résumer pas uniquement deux valeurs, mais n valeurs. Pour cela, on souhaite écrire la méthode windowSquash(size, function) qui prend les éléments pour groupe de taille size, les met dans une liste et appelle la fonction qui pour la liste va renvoyer la valeur correspondante.
    Quelle doit être la signature de la fonction function ? Quelle est l'interface fonctionnelle correspondante ? Doit-on utiliser un int ou un long pour size ? Quel est le type de retour de windowSquash ? Que doit-on faire si le nombre d'éléments du Stream n'est pas un multiple de size ? Quelle variante de Gatherer.ofSequential() allons nous utiliser ?
    Écrire la méthode windowSquash.
    Vérifier que les tests marqués "Q8" passent.

  9. Enfin, on souhaite écrire une méthode windowFixed(size, collector) qui prend une taille et un collecteur et groupe size éléments dans la structure de données gérée par le collecteur.
    Par exemple, si on veut collecter les éléments deux à deux pour les mettre dans une liste, on va écrire
    var list = List.of(1, 2, 3, 4);
    var result = list.stream().gather(GathererDemo.windowFixed(2, Collectors.toList())).toList();
    System.out.println(result);  // [[1, 2], [3, 4]]
        

    Écrire la méthode windowFixed(size, collector).
    Vérifier que les tests marqués "Q9" passent.

  10. Si vous ne l'avez pas déjà fait, en fait, on a besoin uniquement de deux types paramétrés pour implanter la méthode windowFixed... En effet, le type du milieu du Collector peut être déclaré comme "?" (Le A de <E, A, T>).
    Modifier votre code pour que la méthode windowFixed n'utilise que deux types paramétrés.
    Vérifier que les tests marqués "Q10" passent.