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 :
- le tutoriel "originel" qui n'en est pas un : https://www.oracle.com/technetwork/java/javase/generics-tutorial-159168.pdf
- mais surtout le tutoriel officiel même s'il est resté à la version 8 du langage
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 :
- un tableau dynamique "à l'ancienne" (legacy) qui stocke des
Object
- un tableau dynamique paramétré ("générique")
- un tableau dynamique paramétré et trié, c'est-à-dire que vous imposerez des contraintes sur le type paramétré.
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).
- Écrire le constructeur sans argument qui initialise les différents attributs.
- Écrire
getTaille()
pour l’attribut qui donne le nombre d’éléments effectivement présents dans le tableau. - Écrire
getCapacite()
qui donne la taille maximale du tableau - Écrire une méthode privée
agrandir()
qui double la capacité du tableau. La méthode lancera l’exceptionOutOfMemoryError
si cela est impossible. Grâce àSystem.arraycopy()
les éléments déjà présents dans le tableau seront préservés. - Écrire une méthode
addElement()
qui permet d’ajouter l’Object
en paramètre au tableau si cela est possible.addElement()
fera appel àagrandir()
si nécessaire. - Écrire une méthode
getElementAt()
qui renvoie l’Object
dont l’index est précisé en paramètre. Si l’index ne correspond pas, on lancera l’exceptionMyArrayOutOfBoundsException
(exception à créer).
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).
- Redéfinir la méthode
toString()
pour qu’elle retourne le nom de la classe, le hashcode, la taille réelle et la capacité du tableau d’objets. - Écrire une application qui peuple ce tableau de quelques entiers puis qui en fait la somme,
élément par élément. Il est obligatoire de faire un downcasting d’
Object
enInteger
pour pouvoir connaître la valeur de l’entier contenu dans le tableau
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 > {}
- Écrire un tableau générique où les éléments sont triés. On s’assurera que la classe est paramétrée par un élément qui implémente l’interface Comparable
- Chaque élément ajouté par la méthode
addElement()
sera positionné à sa bonne place. - On redéfinira la méthode
toString()
du tableau pour renvoyer une chaine qui représente les éléments du tableau. - On testera avec une classe
Point
avec deux attributs de type double :x
ety
. L’ordre sera simplement la distance à l’origine. Le point le plus grand sera celui qui est le plus loin de l’origine. Tester avec quelques éléments.
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 !