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

Query (fonctionnelle)


On cherche à réimplanter l'API des Stream (juste pour le fun), comme l'API est assez complexe, on ne réimplantera qu'une sous-partie. Pour cela, nous allons définir une interface Query et son implantation QueryImpl.

Exercice 1 - Maven

Pour ce TP, nous allons utiliser la même configuration Maven qu'habituellement.
         <project xmlns="http://maven.apache.org/POM/4.0.0"
                  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

           <modelVersion>4.0.0</modelVersion>
           <groupId>fr.uge.query</groupId>
           <artifactId>query</artifactId>
           <version>0.0.1-SNAPSHOT</version>

           <properties>
             <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
           </properties>

           <dependencies>
             <dependency>
               <groupId>org.junit.jupiter</groupId>
               <artifactId>junit-jupiter-api</artifactId>
               <version>5.10.0</version>
               <scope>test</scope>
             </dependency>
           </dependencies>

           <build>
             <plugins>
               <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
                 <version>3.11.0</version>
                 <configuration>
                   <release>21</release>
                   <compilerArgs>
                     <compilerArg>--enable-preview</compilerArg>
                   </compilerArgs>
                 </configuration>
               </plugin>

               <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-surefire-plugin</artifactId>
                 <version>3.1.2</version>
                 <configuration>
                   <argLine>--enable-preview</argLine>
                 </configuration>
               </plugin>
             </plugins>
           </build>
         </project>
       
Comme précédemment, créer un projet Maven, au niveau du premier écran, cocher create simple project puis passer à l'écran suivant en indiquant Next. On peut remarquer que les preview features sont activés, ce qui nous aidera pour la dernière question du TP.
Pour ce TP, le groupId est fr.uge.query , l'artefactId est query et la version est 0.0.1-SNAPSHOT. Puis cliquer sur Finish.

Exercice 2 - Query

Une Query est une réimplantation simple d'une partie de l'API des Stream. C'est une interface que l'on crée à partir d'une liste ou d'un iterable, qui agit comme une vue et qui possède les traditionnelles méthodes filter, map et reduce. Comme pour l'API des Stream, ces méthodes n'effectuent pas le calcul tant qu'une opération terminale comme toString, toList ou reduce n'est pas exécutée.
L'interface Query possède une seule implantation QueryImpl qui sera définie en tant que classe interne de l'interface Query. La classe QueryImpl est la seule implantation possible de Query.
Il existe deux méthodes pour créer une Query, la méthode fromList et la méthode fromIterable. La méthode fromList, en plus de prendre en paramètre une liste, prend comme second paramètre une fonction qui indique si chaque élément de la liste doit ou non être présent lors d'un calcul sur la Query (en utilisant un Optional).
Par exemple, si l'on veut créer une Query de noms à partir d'une liste d'animaux de compagnie (Pet) qui peuvent posséder un nom, on va écrire le code suivant :
       record Pet(Optional<String> name) { }

       List<Pet> pets = List.of(new Pet(Optional.of("Scooby"), new Pet(Optional.empty())));
       Query<String> query = Query.fromList(pets, Pet::name);
       System.out.println(query);  // Scooby
     
Le record Pet indique si le nom de l'animal existe ou non grâce à un Optional. La Query est créé en indiquant que seul les noms des animaux de la liste qui ont un nom (ceux dont l'Optional est présent) sont à prendre en compte.
La méthode fromIterable permet de créer une Query directement depuis un Iterable, par exemple
       List<Integer> list = List.of(1, 2, 4, 8);
       Query<Integer> query = Query.fromIterable(list);
     

L'interface Query possède les méthodes :
  • toList qui calcule les éléments d'une Query et les ajoute à une liste (non-modifiable une fois renvoyée),
  • toStream qui renvoie la liste sous-forme de Stream, dans ce cas les calculs ne sont faits que lorsque qu'une opération terminale est appelée sur le Stream,
  • toLazyList qui renvoie une liste non-modifiable dont la taille et les éléments sont calculés uniquement si on demande cette information (lazy veut dire paresseux en Anglais),
  • et les méthodes classiques filter, map et reduce.

Des tests unitaires correspondant à l'implantation sont ici : QueryTest.java.

  1. On souhaite écrire une interface Query ainsi qu'une classe QueryImpl qui est une classe interne de l'interface Query et qui va contenir l'implantation de l'interface. Cette classe doit être la seule implantation possible.
    Ce n'est pas très beau comme design, mais cela fait un seul fichier ce qui est plus pratique pour la correction.
    L'interface Query doit posséder une méthode fromList qui permet de créer une Query comme expliqué ci-dessus. De plus, il doit être possible d'afficher les éléments d'une Query avec la méthode toString() qui effectue le calcul des éléments et les affiche. L'affichage contient tous les éléments présents (ceux pour qui la fonction prise en second paramètre renvoie un élément présent) séparés par le symbole " |> ".
    Attention : il ne faut pas faire le calcul des éléments (savoir si ils sont présent ou non) à la création du Query, mais uniquement lorsque l'affichage est demandé.
    Écrire le fichier Query.java avec l'interface et la classe d'implantation.
    Vérifier que les tests unitaires "Q1" passent.

  2. On souhaite ajouter une méthode toList à l'interface Query dont le but est de renvoyer dans une liste non-modifiable les éléments présents.
    Écrire la méthode toList.
    Vérifier que les tests unitaires "Q2" passent.

  3. On souhaite maintenant ajouter une méthode toStream qui renvoie un Stream des éléments présents dans une Query.
    Note : ici, on ne vous demande pas de créer un Spliterator, il existe déjà une méthode stream() sur l'interface List.
    Écrire la méthode toStream.
    Vérifier que les tests unitaires "Q3" passent.

  4. On souhaite ajouter une méthode toLazyList qui renvoie une liste non-modifiable dont les éléments sont calculés et mis dans un cache (une liste modifiable) lorsque l'on a besoin de les connaître. Attention, on ne doit remplir le cache qu'avec les éléments dont on a besoin.
    Pour parcourir la liste de QueryImpl, on va utiliser un itérateur.
    L'algorithme de get(index) en français est le suivant :
            Tant que l'index est supérieur ou égal au nombre d'éléments du cache et qu'il reste des éléments dans l'itérateur
               on calcule l'Optional correspondant l'élément de l'itérateur
               on stocke la valeur de l'Optional dans le cache si elle existe
    
            Puis, on renvoie l'élément du cache à l'index index
           

    Note : il existe une classe java.util.AbstractList qui peut vous servir de base pour implanter la liste paresseuse demandée.
    Attention : vous veillerez à ne pas demander plusieurs fois si un même élément est présent, une seule fois devrait suffire.
    Écrire la méthode toLazyList.
    Vérifier que les tests unitaires "Q4" passent.

  5. On souhaite pouvoir créer une Query en utilisant une nouvelle méthode fromIterable qui prend un Iterable en paramètre. Dans ce cas, tous les éléments de l'Iterable sont considérés comme présents.
    Note : une java.util.List est un Iterable et Iterable possède une méthode spliterator().
    Écrire la méthode fromIterable et modifier le code des méthodes existantes si nécessaire.
    Vérifier que les tests unitaires "Q5" passent.
    Note: il existe une méthode spliterator() sur l'interface Iterable.

  6. On souhaite écrire une méthode filter qui permet de sélectionner uniquement les éléments pour lesquels un appel à la fonction prise en paramètre de filter renvoie vrai.
    Écrire la méthode filter.
    Vérifier que les tests unitaires "Q6" passent.

  7. On souhaite écrire une méthode map qui renvoie une Query telle que chaque élément est obtenu en appelant la fonction prise en paramètre de la méthode map sur un élément d'une Query d'origine.
    Écrire la méthode map.
    Vérifier que les tests unitaires "Q7" passent.

  8. Enfin, on souhaite écrire une méthode reduce sur une Query qui marche de la même façon que la méthode reduce à trois paramètres sur un Stream et sachant que comme notre Query n'a pas d'implantation parallel, le troisième paramètre est superflu.
    Écrire la méthode reduce.
    Vérifier que les tests unitaires "Q8" passent.
    Note: quelle early preview feature peut-on utiliser ici ?