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

Slices of bread


Vue d'un tableau, classe interne, inner class, classe anonyme et encore un peu de lambdas
Le but de ce TP est d'écrire un slice (une vue partielle d'un tableau) pour comprendre les notions de vue, de classe interne et de classe anonyme en Java.

Exercice 1 - Maven

Comme pour le TP précédent, nous allons utiliser Maven avec une configuration (le pom.xml) très similaire, ici, nous n'avons pas besoin d'activer les preview features.
<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.slice</groupId>
    <artifactId>slice</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.11.0</version>
           <scope>test</scope>
       </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <release>23</release>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.5.0</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.slice , l'artefactId est slice et la version est 0.0.1-SNAPSHOT. Puis cliquer sur Finish.

Exercice 2 - The Slice and The furious

Un (slice) est une structure de données classique qui permet de découper "virtuellement" un tableau en gardant des indices de début et de fin (from et to) ainsi qu'un pointeur sur le tableau.
Une implantation d'une structure de données qui ne stocke pas les éléments est appelée une vue. C'est une structure de données plus efficace qu'une liste, car on ne copie pas les éléments du tableau, mais une vue ne respecte pas le principe d'encapsulation (les éléments n'appartiennent / ne sont pas de la responsabilité de la vue).
En Java, les slices ne sont pas appelés des slices, mais sont accessibles à travers l'interface List en utilisant les méthodes Arrays.asList(array) et List.subList(from, to), c'est ce que nous souhaitons réimplanter (donc on ne va pas utiliser ces méthodes !).

Les tests JUnit 5 de cet exercice sont SliceTest.java.

  1. En Java, un slice correspond à l'interface suivante
    public interface Slice<E> {
      int size();
      E get(int index);
    }
         

    Slice une interface paramétrée par le type d'élément du tableau sur lequel un Slice est créé. Les éléments peuvent ou on être null.
    Voici un exemple d'utilisation d'un Slice
    String[] array = new String[] { "foo", "bar", "baz", "whizz" };
    Slice<String> slice = Slice.of(array, 1, 3);
    System.out.println(slice.size());  // 2
    System.out.println(slice.get(0));  // bar
    System.out.println(slice.get(1));  // baz
         
    La méthode of permet de créer un slice sur un tableau avec les paramètres from et to, qui sont respectivement l'index du premier élément du slice et l'index après le dernier élément. Dit différemment, un slice correspond aux éléments dans l'intervalle [from, to[ (from inclus et to exclu).
    Rappeler pourquoi doit-on déclarer encore une fois E en début de la méthode of
          static <E> Slice<E> of(E[] elements, int from, int to) {
             ...
          }
         

    Écrire l'interface Slice et sa méthode of sachant que l'on va declarer l'implantation de l'interface SliceImpl en tant que classe interne de l'interface.
    Vérifier que les tests marqués "Q1" passent.
    Note : pour les préconditions, il existe les méthodes java.util.Objects.checkIndex() et Objects.checkFromToIndex().

  2. On souhaite pouvoir afficher le contenu d'un slice avec la même syntaxe que pour une List.
    var array = new String[] { "foo", "bar", "baz", "whizz" };
    var slice = Slice.of(array, 1, 3);
    System.out.println(slice);  // [bar, baz]
         

    Sachant qu'il existe une méthode java.util.Arrays.stream(array, start, end), implanter la méthode d'affichage en utilisant un stream.
    Vérifier que les tests marqués "Q2" passent.

  3. On souhaite écrire une méthode subSlice(from, to) qui renvoie un slice qui est une sous-partie du slice sur lequel on appelle la méthode subSlice.
    Par exemple
        var array = new String[]{ "foo", "bar", "baz", "whizz" };
        var slice = Slice.of(array, 1, 4);
        var subSlice = slice.subSlice(1, 2);
        System.out.println(subSlice.size());  // 1
        System.out.println(subSlice.get(0));  // "baz"
          
    Dans l'exemple ci-dessous, on crée un slice sur le tableau avec l'intervalle [1, 4[, puis on crée un second slice sur le premier slice avec l'intervalle [1, 2[. C'est équivalent à créer un slice sur [2, 3[ sur le tableau.
    Implanter la méthode subSlice de telle façon que le code ci-dessus fonctionne.
    Vérifier que les tests marqués "Q3" passent.

  4. On souhaite maintenant implanter une méthode reversed qui renvoie un nouveau slice (une vue) qui permet de voir les éléments en sens inverse (sans copier les éléments).
    Voici un exemple d'utilisation
        var array = new String[]{ "foo", "bar", "baz", "whizz" };
        var slice = Slice.of(array, 1, 4);
        var reversed = slice.reversed();
        System.out.println(reversed.size()); // 3
        System.out.println(reversed.get(0));  // whizz
        System.out.println(reversed.get(1)); // baz
        System.out.println(reversed.get(2)); // bar
          
    On peut remarquer que pour renverser un slice, il n'est pas nécessaire de connaître l'implantation si on a accès aux méthode size et get(index). On peut donc implanter la méthode reversed sous forme de méthode par défaut directement dans l'interface.
    Implanter la méthode reversed sachant qu'en termes d'implantation, nous allons utiliser une classe anonyme et que pour l'instant, on ne va pas implanter la méthode subSlice(from, to)
    Vérifier que les tests marqués "Q4" passent.
    Note : en Java, lorsqu'une méthode abstraite est déclarée dans l'interface, on doit l'implanter. Mais si on ne veut pas l'implanter tout de suite (comme c'est demandé ici), on peut faire qu'elle lève une exception. Habituellement, on utilise l'exception UnsupportedOperationException pour cela.
    Optimisation : si on reversed() un slice déjà reversed() on obtient le slice original.

  5. On souhaite ajouter à l'interface Slice une méthode replaceAll qui permet de remplacer chaque élément en appelant une fonction avec l'ancienne valeur de l'élément, la fonction renvoyant la nouvelle valeur de l'élément.
    Par exemple, pour ajouter des étoiles autour des chaînes de caractères, on peut écrire
        var array = new String[]{ "foo", "bar", "baz", "whizz" };
        var slice = Slice.of(array, 1, 4);
        slice.replaceAll(x -> "*" + x + "*");
        System.out.println(slice.get(0));  // *bar*
        System.out.println(slice.get(1));  // *baz*
        System.out.println(slice.get(2));  // *whizz*
          

    Quelle est l'interface fonctionnelle que l'on doit utiliser en paramètre de replaceAll ?
    Quelle est le type de retour de la méthode replaceAll ?
    Ajouter la méthode replaceAll à l'interface Slice et modifier les implantations en conséquence.
    Vérifier que les tests marqués "Q5" passent.

  6. On souhaite maintenant implanter la méthode subSlice(from, to) quand le Slice est reversed.
    Par exemple
        var array = new String[]{ "foo", "bar", "baz", "whizz" };
        var slice = Slice.of(array, 1, 4);
        var reversed = slice.reversed();
        var subSlice = reversed.subSlice(1, 3);
        System.out.println(subSlice.get(0));  // baz
        System.out.println(subSlice.get(0));  // bar
          

    Implanter la méthode subSlice(from, to) dans la classe anonyme dans la méthode reversed().
    Vérifier que les tests marqués "Q6" passent.
    Note : vous avez le droit de prendre un papier pour comprendre comment calculer les index correctement.

  7. Si vous ne l'avez pas déjà fait, on a oublié d'implanter la méthode d'affichage dans le cas où le Slice est reversed().
    Ajouter cette méthode
    Vérifier que les tests marqués "Q7" passent.

  8. [Revision] Enfin, pour les plus balèzes, on souhaiterait pouvoir créer un slice juste en passant des éléments séparés par des virgules.
    Par exemple
        var slice = Slice.of("foo", "bar", "baz", "whizz");
        System.out.println(slice.size());  // 4
         

    Implanter la méthode of sachant que les tests doivent compiler sans warnings (et non changer les tests n'est pas la solution !).
    Vérifier que les tests marqués "Q8" passent.