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

Structure de données spécialisée pour les types primitifs


Le but de ce TP noté est d'implanter une structure de données appelée NumericVec qui est une séquence de valeurs numériques d'entiers, d'entiers long ou de nombres flottants.
L'intérêt de cette structure de données est d'avoir une représentation très compacte en mémoire des données sans pour autant dupliquer le code pour chaque type primitif.

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.numeric</groupId>
    <artifactId>numeric</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.9.0</version>
        <scope>test</scope>
      </dependency>
    </dependencies>

    <build>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.10.1</version>
          <configuration>
            <release>19</release>
          </configuration>
        </plugin>

        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>3.0.0-M7</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.numeric , l'artefactId est numeric et la version est 0.0.1-SNAPSHOT. Puis cliquer sur Finish.

Exercice 2 - NumericVec

La classe fr.uge.numeric.NumericVec est un conteneur de valeurs numériques homogènes de type int, long ou double mais qui en interne stocke les valeurs dans un tableau de long.
En effet, un entier long 64 bits (long) peut contenir un int 32 bits ; il suffit de faire cast en int si on veut obtenir la valeur comme un int. Un entier long 64 bits (long) peut aussi contenir un nombre flottant 64 bits (double) car c'est la même taille en mémoire, il faut être capable pour cela de voir un double comme un long sans changer la représentation bit à bit. La méthode Double.doubleToRawLongBits(double valeur) renvoie le long correspondant bit à bit au double passé en paramètre et la méthode Double.longBitsToDouble(long value) fait l'opération inverse.
La classe NumericVec permet de créer des séquences d'int, de long ou de double de la façon suivante.
  NumericVec<Integer> seq1 = NumericVec.ints(1, 4, 7);  // séquence d'entiers 32 bits
  NumericVec<Long> seq2 = NumericVec.longs(123L, -45L);  // séquence d'entiers 64 bits
  NumericVec<Double> seq3 = NumericVec.doubles(48.0, 123.25);  // séquence de flottants 64 bits
     
La classe NumericVec possède, de plus,
  • les méthodes add(value), get(index) et size qui permettent respectivement d'ajouter un élément, d'obtenir un élément à un index donné et d'obtenir le nombre de valeurs dans la séquence.
  • un mécanisme qui permet d'afficher une séquence : les valeurs sont séparées par des virgules suivies d'un espace, et le tout entre '[' et ']'
      var seq1 = NumericVec.ints(1, 4, 7);
      System.out.println(seq1);   // affiche [1, 4, 7]
           
  • un mécanisme qui permet de parcourir la séquence avec une boucle, comme dans l'exemple ci-dessous
      var seq1 = NumericVec.ints(1, 4, 7);
      for(var value: seq1) {
        System.out.println("value: " + value);
      }
           
  • une méthode addAll(anotherSequence) qui permet d'ajouter les valeurs d'une autre séquence à la séquence courante si elles ont le même type de valeurs numériques
  • une méthode map(function, factory) qui renvoie une nouvelle séquence créée en utlisant la méthode de création factory et qui contient les valeurs de retour de l'appel à la fonction prise en premier paramètre pour chaque valeur de la séquence courante.
      var seq1 = NumericVec.ints(1, 4, 7);
      var seq4 = seq1.map(v -> v * 2.5, NumericVec::double);
      System.out.println(seq4);  // affiche [2.5, 10.0, 17.5]
           
  • les méthodes toNumericVec(factory) et stream(), qui permettent respectivement de collecter des valeurs dans un NumericVec et de renvoyer un Stream.

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

  1. Dans la classe fr.uge.numeric.NumericVec, on souhaite écrire une méthode longs sans paramètre qui permet de créer un NumericVec vide ayant pour l'instant une capacité fixe de 16 valeurs. Cela doit être la seule façon de pouvoir créer un NumericVec.
    Écrire la méthode longs puis ajouter les méthodes add(value), get(index) et size.
    Vérifier que les tests unitaires marqués "Q1" passent.

  2. On veut maintenant que le tableau utilisé par NumericVec puisse s'agrandir pour permettre d'ajouter un nombre arbitraire de valeurs.
    On veut, de plus, que NumericVec soit économe en mémoire, donc la capacité du tableau d'un NumericVec vide doit être 0 (si vous n'y arrivez pas, faites la suite).
    Vérifier que les tests unitaires marqués "Q2" passent.
    Note: agrandir un tableau une case par une case est très inefficace !

  3. Faire en sorte d'utiliser un Stream pour que l'on puisse afficher un NumericVec avec le format attendu.
    Vérifier que les tests unitaires marqués "Q3" passent.

  4. On veut maintenant ajouter les 3 méthodes ints, longs et doubles qui permettent respectivement de créer des NumericVec d'int, de long ou de double en prenant en paramètre des valeurs séparées par des virgules.
    En termes d'implantation, l'idée est de convertir les int ou les double en long avant de les insérer dans le tableau. Et dans l'autre sens, lorsque l'on veut lire une valeur, c'est à dire quand on prend un long dans le tableau, on le convertit en le type numérique attendu. Pour cela, l'idée est de stocker dans chaque NumericVec une fonction into qui sait convertir une valeur en long, et une fonction from qui sait convertir un long vers la valeur attendue.
    Vérifier que les tests unitaires marqués "Q4" passent.

  5. On souhaite maintenant pouvoir parcourir un NumericVec avec une boucle for(:). Dans le cas où l'on modifie un NumericVec avec la méthode add lors de l'itération, les valeurs ajoutées ne sont pas visibles pour la boucle.
    Modifier la classe NumericVec pour implanter le support de la boucle for(:).
    Vérifier que les tests unitaires marqués "Q5" passent.

  6. On souhaite ajouter une méthode addAll qui permet d'ajouter un NumericVec à un NumericVec déjà existant.
    Écrire le code de la méthode addAll.
    Vérifier que les tests unitaires marqués "Q6" passent.
    Note: on peut remarquer qu'il y a une implantation efficace car les deux NumericVec utilisent en interne des tableaux de long.

  7. On souhaite maintenant écrire une méthode map(function, factory) qui prend en paramètre une fonction qui peut prendre en paramètre un élément du NumericVec et renvoie une nouvelle valeur ainsi qu' une référence de méthode permettant de créer un nouveau NumericVec qui contiendra les valeurs renvoyées par la fonction.
    Écrire la méthode map.
    Vérifier que les tests unitaires marqués "Q7" passent.

  8. On souhaite écrire une méthode toNumericVec(factory) qui prend en paramètre une référence de méthode permettant de créer un NumericVec et renvoie un Collector qui peut être utilisé pour collecter des valeurs numériques dans un/des NumericVec créés par la référence de méthode.
    Écrire la méthode toNumericVec.
    Vérifier que les tests unitaires marqués "Q8" passent.

  9. Enfin, écrire une méthode stream() qui renvoie un Stream qui voit l'ensemble des valeurs par ordre d'insertion dans le NumericVec courant.
    Le Stream renvoyé devra être parallélisable.
    Vérifier que les tests unitaires marqués "Q9" passent.
    Note : pour que le Stream parallèle soit un peu efficace, on ne coupera pas en deux les Stream ayant moins de 1024 valeurs.