Copy of https://perso.isima.fr/loic/java/exo_jse_generics.php
tete du loic

 Loïc YON [KIUX]

  • Enseignant-chercheur
  • Référent Formation Continue
  • Responsable des contrats pros ingénieur
  • Référent entrepreneuriat
  • Responsable de la filière F2 ingénieur
  • Secouriste Sauveteur du Travail
mail
loic.yon@isima.fr
phone
(+33 / 0) 4 73 40 50 42
location_on
Institut d'informatique ISIMA
  • twitter
  • linkedin
  • viadeo

[JavaSE] Généricité

 Cette page commence à dater. Son contenu n'est peut-être plus à jour. Contactez-moi si c'est le cas!

Date de première publication : 2010/04/22

Ce TP est une séance légère de manipulation des génériques. Pour une utilisation un peu plus avancée, je vous conseille les lectures suivantes :

Depuis la plateforme 1.5, le java s’est doté des generics. L’intuition des développeurs C++ sera fortement perturbée car les choix d’implémentation des classes paramétrées formelles sont fondamentalement différents. En Java, une classe paramétrée (dénommée le plus souvent par un seul caractère majuscule) est compilée et partagée par toutes ses invocations (les paramètres formels ne sont que des paramètres). Ce choix a tout de même un avantage, les vieux codes (ou Legacy, ie sans generics) sont directement utilisables avec des compilateurs récents.

Vous allez devoir faire quelque chose de désagréable : coder différentes versions d'un conteneur qui existe déjà en Java :

Legacy code

Créer une classe Tableau qui peut contenir des objets dans un tableau de capacité fixée. La capacité initiale sera de 1 ou de 2. On mémorisera le nombre d’éléments réellement présents dans le tableau par un attribut taille (on suppose qu’il n’y a pas de trous).

Vous pourriez également réutiliser l'exception qui existe déjà mais il faut connaître la manière de créer une nouvelle exception. Vous devez implémenter la gestion des indices sur le tableau car la machine virtuelle ne peut détecter que les cas où l'index est négatif ou supérieur à la capacité du tableau (et non pas sur le nombre d'éléments effectivement présents dans le taleau).

Pour sécuriser le downcasting, vous pouvez utiliser l'opérateur de réflexion instance of :

Object o;

if  (o  instanceof  Point)  ((Point)o).getX() ;

Vous pouvez utiliser le fichier de tests suivant pour JUnit 4:

import static org.junit.Assert.*;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;


public class TableauTest {

  Tableau t;

  @Before
  public void initialisation() {
    t = new Tableau();
    assertNotNull("@Before en échec", t);

  }

  @After 
  public void finalisation(){
    t = null;
  }

  @Test
  public void testTableauVide() {
    assertEquals(t.getCapacite(), 1);
    assertEquals(t.getTaille(), 0);
  }

  @Test
  public void testAjout1Element() throws OutOfMemoryError {
    // t.addElement(new Integer(3)); // java 8
    t.addElement(3);                 // java 11 et plus
    assertEquals(t.getTaille(), 1);
  }

  @Test
  public void testAjout2Elements() throws OutOfMemoryError {
    testAjout1Element();
    // t.addElement(new Integer(4)); 
    t.addElement(4);
    assertEquals(t.getTaille(), 2);
    assertEquals(t.getCapacite(), 2);
  }

  @Test
  public void testAjout3Elements() throws OutOfMemoryError {
    testAjout2Elements();
    // t.addElement(new Integer(1)); 
    t.addElement(1);
    assertEquals(t.getTaille(), 3);
    assertEquals(t.getCapacite(), 4);
  }

  @Test
  public void testAccesElements1() throws Exception {
    testAjout3Elements();
    assertEquals(t.getElementAt(0), 3);
    assertEquals(t.getElementAt(1), 4);
    assertEquals(t.getElementAt(2), 1);
    // auto boxing ...
    t.addElement(7);
    assertEquals(t.getElementAt(3), 7);
  }

  @Test(expected=MyArrayOutOfBoundsException.class)
  public void testAccesElements2()  throws Exception {
    testAccesElements1();
    t.getElementAt(-1);
  }

  @Test(expected=MyArrayOutOfBoundsException.class)
  public void testAccesElements3() throws Exception {
    testAccesElements1();
    t.getElementAt(4);
  }

  @Test(expected=MyArrayOutOfBoundsException.class)
  public void testExceptionPourIncredule() throws Exception {
    // evidemment ce test est toujours FAUX
    throw new OutOfMemoryError();
  }

  @Test(expected=OutOfMemoryError.class)
  @Ignore
  public void testAgrandirException() throws Exception  {
    t = new Tableau(Integer.MAX_VALUE);
    try {
      while (true) {
        t.addElement(new Double(Math.random()));
        //System.out.println(t.getTaille());
      }
    }  catch(Exception e) {
      System.out.println("Exception a la taille"+t.getTaille());
      throw e;
    }
  }
}

Pour JUnit 5 :




public class TableauTest {

  Tableau t;

  @BeforeEach
  public void initialisation() {
    t = new Tableau();
    assertNotNull(t);

  }

  @AfterEach 
  public void finalisation(){
    t = null;
  }

  @Test
  public void testTableauVide() {
    assertEquals(1, t.getCapacite());
    assertEquals(0, t.getTaille());
  }

  @Test
  public void testAjout1Element(){
    // t.addElement(new Integer(3)); // java 8
    t.addElement(3);                 // java 11 et plus
    assertEquals(1, t.getTaille());
  }

  @Test
  public void testAjout2Elements() {
    testAjout1Element();
    // t.addElement(new Integer(4)); 
    t.addElement(4);
    assertEquals(2, t.getTaille());
    assertEquals(2, t.getCapacite());
  }

  @Test
  public void testAjout3Elements() {
    testAjout2Elements();
    // t.addElement(new Integer(1)); 
    t.addElement(1);
    assertEquals(3,t.getTaille());
    assertEquals(4,t.getCapacite());
  }

  @Test
  public void testAccesElements1(){
    testAjout3Elements();
    assertEquals(3, t.getElementAt(0));
    assertEquals(4, t.getElementAt(1));
    assertEquals(1, t.getElementAt(2));
    // auto boxing ...
    t.addElement(7);
    assertEquals(7, t.getElementAt(3));
  }

  @Test
  public void testAccesElements2(){
    testAccesElements1();
    assertThrows( MyArrayOutOfBoundsException.class,
      () -> t.getElementAt(-1);;
    );
    
  }

  @Test
  public void testAccesElements3() {
    testAccesElements1();
    assertThrows( MyArrayOutOfBoundsException.class,
      () -> t.getElementAt(4);
    );
  }

  @Test
  public void testExceptionPourIncredule() {
    // evidemment ce test est toujours FAUX
    assertThrows(MyArrayOutofBoundsException.class, 
                 () -> throw new OutOfMemoryError(); 
    );
    
  }

/*
  @Test
  public void testAgrandirException() {
    t = new Tableau(Integer.MAX_VALUE);
    try {
      while (true) {
        t.addElement(new Double(Math.random()));
        //System.out.println(t.getTaille());
      }
    }  catch(Exception e) {
      System.out.println("Exception a la taille"+t.getTaille());
      assertTrue(true);
    }
    assertTrue(false);
  }
}*/

Les fichiers de tests sont à adapter pour la suite.

Conteneur générique

Adapter la classe Tableau pour en faire une classe paramétrée sur le type O.

Avec ce que vous savez faire en C++, ce n'est pas hyper difficile, voici la syntaxe :

// définition de classe
class Parametree<O> {
    Point p ;
    O o;
}

// instanciation
Parametree<Integer> c1 = Parametree<>();

Dans l'instanciation, le type est déduit par le compilateur (opérateur diamant). Cette syntaxe n'est pas utilisable avant java 6 inclus.

La première vraie différence avec le C++ est que le type O est un type objet. Cela ne peut pas être un type primitif (non objet). On ne peut pas écrire Parametree<double>.

Bon d’accord, ce n’est pas si facile ! Java nous empêche d’écrire une instruction comme :

O[] o = new O[10];

lorsque O est une classe paramétrée. Une solution possible est de convertir un tableau d’Object :

O[] o = (O[]) new Object[10];

On ajoutera une annotation @SuppressWarnings("unchecked") pour enlever l’avertissement à la compilation.

Les génériques et les tableaux ne vont pas bien ensemble : c’est une histoire de reification et d’erasure pour ceux que cela intéressent.

Il n’est pas possible non plus de créer un objet d’une classe paramétrée :

O o = new O() ;

Une des solutions les plus élégantes pour faire cela est de se servir d’un élément de référence de la classe O et de le cloner ! Cela n’enlèvera toutefois pas l’annotation.

Écrire le même genre de tests que dans la partie précédente pour vérifier le tableau paramétré. On notera bien que le downcasting des éléments renvoyés par getElementAt() n’est plus nécessaire.

Conteneur générique trié

Il n’y a aucune condition sur le type de paramètre des classes paramétrées. Imaginons que l’on veuille faire un tableau trié. Pour cela, il faut s’assurer que la classe donnée en paramètre respecte ce que l’on appelle en mathématiques un ordre. On peut trier des objets qui implémentent l’interface Comparable<T>.

La comparaison d'objets se fait grâce à une méthode d'une interface en Java car il n'y a pas de surcharge d'opérateur comme l'operator<() en C++. En C++, l'opérateur de comparaison est une fonction mais cela n'existe pas en Java !

En Java, on peut imposer des conditions sur les classes génériques (on appelle cela des concepts), chose que l'on ne peut pas faire en C++. L'écriture est très simple, que la condition soit une classe ou une interface :

class Parametree1<O extends Classe    > {}
class Parametree2<O extends Interface > {}

Remarque : pour instancier un tableau de O, il faut utiliser Object ou un surtype (dans sa version non générique) si un surtype (même générique) est précisé (si T2 dérive de T1 alors T1 est un surtype de T2).

Conclusion

Vous l’aurez compris : l’utilisation des génériques en java n’est pas si simple : il est bien plus facile de lire un code que de l’écrire. Implémenter un tableau n’est pas évident, les concepteurs conseillent plutôt d’utiliser les listes ;-) N’hésitez pas à explorer les différentes collections offertes par la plateforme pour utiliser celle qui correspond le plus à vos besoins !