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

Liste persistante (fonctionnelle)


Encapsulation, classe locale, implémentation paresseuse (lazy), types paramétrés, itérateur
Nous allons implanter l'interface fr.uge.seq.Seq qui représente une séquence (comme une liste) paresseuse non mutable d'éléments non null. Les éléments sont stockés en interne dans une liste non mutable.

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.seq</groupId>
          <artifactId>seq</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>
                </configuration>
              </plugin>

              <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.1.2</version>
              </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.
Pour ce TP, le groupId est fr.uge.seq , l'artefactId est seq et la version est 0.0.1-SNAPSHOT. Puis cliquer sur Finish.

Exercice 2 - Seq

L'interface fr.uge.seq.Seq est paramétrée par le type des éléments qu'elle contient.
Elle définit deux factory methods :
  • from(list) qui prend une liste en paramètre (une java.util.List) et créé un objet Seq contenant les éléments de la liste (dans le même ordre)
  • of(elements) qui prend des éléments séparés par des virgules en paramètre et créé un objet Seq contenant ces éléments (dans le même ordre)

ainsi que les opérations suivantes :
  • get qui permet d'obtenir le n-ième élément d'un Seq (avec une comnplexité en O(1), bien sûr) ;
  • size qui indique le nombre d'éléments dans la séquence (en O(1), toujours).
    Note : comme la séquence est non mutable, le nombre d'éléments de la séquence ne change pas.

Voici un exemple de création et d'utilisation d'un Seq.
      var seq = Seq.from(List.of(78, 56, 34, 23));
      System.out.println(seq.size());  // 4
      System.out.println(seq.get(2));  // 34
     

Les tests unitaires JUnit 5 correspondants à l'implantation sont ici : SeqTest.java.

  1. Dans un premier temps, on va définir une classe SeqImpl qui est une implantation de l'interface Seq dans le même package que Seq.
    Écrire le constructeur dans la classe SeqImpl ainsi que la méthode from(list) dans l'interface Seq sachant que, comme indiqué ci-dessus, SeqImpl contient une liste non mutable.
    Expliquer pourquoi le constructeur ne doit pas être public ?
    Puis déclarer les méthodes size et get() dans l'interface Seq et implanter ces méthodes dans la classe SeqImpl.
    Vérifier que les tests marqués "Q1" passent.
    Attention : la méthode from est une méthode publique qui prend un type paramétré en paramètre !

  2. On souhaite écrire une méthode d'affichage permettant d'afficher les valeurs d'un Seq séparées par des virgules (suivies d'un espace), l'ensemble des valeurs étant encadré par des chevrons ('<' et '>').
    Par exemple, avec le Seq créé précédemment, on obtient :
            System.out.println(seq);  // <78, 56, 34, 23>
          

    Modifier votre code en conséquence.
    Vérifier que les tests marqués "Q2" passent.

  3. On souhaite écrire une méthode map qui prend en paramètre une fonction à appliquer à chaque élément d'un Seq pour créer un nouveau Seq. On souhaite avoir une implantation paresseuse, c'est-à-dire une implantation qui ne fait pas de calcul si ce n'est pas nécessaire. Par exemple, tant que personne n'accède à un élément du nouveau Seq il n'est pas nécessaire d'appliquer la fonction. L'idée est de stoker les anciens éléments ainsi que la fonction et de l'appliquer seulement si c'est nécessaire.
    Bien sûr, cela va nous obliger à changer l'implantation déjà existante de SeqImpl car maintenant tous les Seq vont stocker une liste d'éléments ainsi qu'une fonction de transformation (de mapping).
    Exemple d'utilisation
             var seq2 = seq.map(String::valueOf); // String.valueOf() est pas appelée
             System.out.println(seq2.get(0));     // "78", String.valueOf a été appelée 1 fois
                                                  // car on demande explicitement la valeur
           

    Avant de se lancer dans l'implantation de map, quelle doit être sa signature ? Quel doit être le type des éléments de la liste ? Et le type de la fonction stockée ? Dans SeqImpl, ajouter un champ correspondant à la fonction prise en paramètre par map, sans pour l'instant écrire la méthode map. On appelle cela faire un refactoring, c'est-à-dire préparer la classe pour une fonctionnalité future.
    Vérifier que les tests des questions précédentes continuent de fonctionner.
    Déclarer la méthode map dans l'interface Seq et écrire le code de map dans l'implantation SeqImpl.
    Vérifier que les tests marqués "Q3" passent.
    Note : le code doit fonctionner si l'on appelle map deux fois successivement.

  4. On souhaite avoir une méthode findFirst qui renvoie le premier élément du Seq si celui-ci existe.
    Quel doit être le type de retour ?
    Déclarer la méthode findFirst dans l'interface et implanter celle-ci dans la classe SeqImpl.
    Vérifier que les tests marqués "Q4" passent.
    Rappel: il existe une méthode findFirst sur un Stream.

  5. On souhaite implanter la méthode stream() qui renvoie un Stream des éléments du Seq. Pour cela, on va commencer par implanter un Spliterator. Ici, on a deux façon d'implanter le Spliterator : soit on utilise le Spliterator de la liste sous-jacente, soit on utilise des indices. Expliquer dans quel cas on utilise l'un ou l'autre, sachant que nos données sont stockées dans une List.
    Ensuite, on peut créer la classe correspondant au Spliterator à deux endroits : soit comme une classe interne de la classe SeqImpl, soit comme une classe anonyme d'une méthode spliterator(start, end), quelle est à votre avis le meilleur endroit ?
    Écrire les 4 méthodes du Spliterator.
    Puis déclarer la méthode stream dans l'interface et implanter celle-ci dans SeqImpl sachant qu'il existe la méthode StreamSupport.stream qui permet de créer un Stream à partir de ce Spliterator.
    Vérifier que les tests marqués "Q5" passent.

  6. On souhaite ajouter une méthode of à l'interface Seq permettant d'initialiser un Seq à partir de valeurs séparées par des virgules.
    Par exemple, on pourra créer le Seq de la question 2 comme ceci
             var seq = Seq.of(78, 56, 34, 23);
           

    Vérifier que les tests marqués "Q6" passent.
    Note : si vous avez des warnings, vous avez un problème.
    Note 2 : si vous pensez un @SuppressWarnings, pensez plus fort !

  7. On souhaite faire en sorte que l'on puisse utiliser la boucle for-each-in sur un Seq.
    Par exemple,
            for(var value : seq) {
              System.out.println(value);
            }
          
    Modifier votre code en conséquence.
    Vérifier que les tests marqués "Q7" passent.

  8. À la question 5, on a vu comment cacher la déclaration d'une classe à l'intérieur d'une méthode, on peut faire la même chose avec la classe SeqImpl et déclarer l'implantation sous forme d'une classe anonyme à l'intérieur d'une méthode dans l'interface.
    Quelle doit être la visibilité de la méthode contenant la déclaration de la classe anonyme d'implantation ?
    Modifier votre code pour que l'implantation de l'interface Seq soit dans l'interface Seq sans que cela soit visible de l'extérieur.
    Vérifier que les tests marqués "Q8" passent.

Exercice 3 - Seq2 le retour

Pour les plus balèzes, en fait, l'implantation à base de liste de l'exercice précédent n'est pas très efficace en mémoire. Il vaut mieux stocker les éléments dans un tableau car c'est de toute façon le stockage utilisé par la liste (ce n'est pas complètement vrai si vous utilisez List.of(), on vous laisse trouver pourquoi).

Les tests unitaires JUnit 5 correspondants à l'implantation sont ici: Seq2Test.java.

  1. Ré-implanter toutes les méthodes publiques de Seq dans une interface Seq2 en utilisant comme implantation un tableau d'éléments en interne.