IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java

  Chapitre 9 - Stockage des objets

pages : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 

Ceci est plus qu'ennuyeux. Cela peut mener de plus à des bugs relativement durs à trouver. Si une partie (ou plusieurs parties) du programme insère des objets dans le conteneur, et qu'on découvre dans une partie complètement différente du programme via une exception qu'un objet du mauvais type a été placé dans le conteneur, il faut alors déterminer où l'insertion coupable s'est produite. Cependant, il est pratique de démarrer avec les classes conteneur standard pour programmer, en dépit de leurs limitations et de leur lourdeur.

Quelquefois ça marche quand même

Dans certains cas les choses semblent fonctionner correctement sans avoir à transtyper vers le type originel. Un cas particulier est constitué par la classe String que le compilateur traite de manière particulière pour la faire fonctionner de manière idoine. Quand le compilateur attend un objet String et qu'il obtient autre chose, il appellera automatiquement la méthode toString() définie dans Object et qui peut être redéfinie par chaque classe Java. Cette méthode produit l'objet String désiré, qui est ensuite utilisé là où il était attendu.

Il suffit donc de redéfinir la méthode toString() pour afficher un objet d'une classe donnée, comme on peut le voir dans l'exemple suivant :

//: c09:Mouse.java
// Redéfinition de toString().
public class Mouse {
  private int mouseNumber;
  Mouse(int i) { mouseNumber = i; }
  // Redféinition de Object.toString():
  public String toString() {
    return "This is Mouse #" + mouseNumber;
  }
  public int getNumber() {
    return mouseNumber;
  }
} ///:~

//: c09:WorksAnyway.java
// Dans certains cas spéciaux, les choses
// semblent fonctionner correctement.
import java.util.*;

class MouseTrap {
  static void caughtYa(Object m) {
    Mouse mouse = (Mouse)m; // Transtypage depuis un Object
    System.out.println("Mouse: " +
      mouse.getNumber());
  }
}

public class WorksAnyway {
  public static void main(String[] args) {
    ArrayList mice = new ArrayList();
    for(int i = 0; i < 3; i++)
      mice.add(new Mouse(i));
    for(int i = 0; i < mice.size(); i++) {
      // Aucun transtypage nécessaire, appel
      // automatique à Object.toString() :
      System.out.println(
        "Free mouse: " + mice.get(i));
      MouseTrap.caughtYa(mice.get(i));
    }
  }
} ///:~

La méthode toString() est redéfinie dans Mouse. Dans la deuxième boucle for de main() on peut voir l'instruction :

System.out.println("Free mouse: " + mice.get(i));

Après le signe « + » le compilateur s'attend à trouver un objet String. get() renvoie un Object, le compilateur appelle donc implicitement la méthode toString() pour obtenir l'objet String désiré. Cependant, ce comportement magique n'est possible qu'avec les String, il n'est pas disponible pour les autres types.

Une seconde approche pour cacher le transtypage est de le placer dans la classe MouseTrap. La méthode caughtYa() n'accepte pas une Mouse mais un Object, qu'elle transtype alors en Mouse. Ceci ne fait que repousser le problème puisqu'en acceptant un Object on peut passer un objet de n'importe quel type à la méthode. Cependant, si le transtypage n'est pas valide - si un objet du mauvais type est passé en argument - une exception est générée lors de l'exécution. Ce n'est pas aussi bien qu'un contrôle lors de la compilation, mais l'approche reste robuste. Notez qu'aucun transtypage n'est nécessaire lors de l'utilisation de cette méthode :

MouseTrap.caughtYa(mice.get(i));

Créer une ArrayList consciente du type

Pas question toutefois de s'arrêter en si bon chemin. Une solution encore plus robuste consiste à créer une nouvelle classe utilisant une ArrayList, n'acceptant et ne produisant que des objets du type voulu :

//: c09:MouseList.java
// Une ArrayList consciente du type.
import java.util.*;

public class MouseList {
  private ArrayList list = new ArrayList();
  public void add(Mouse m) {
    list.add(m);
  }
  public Mouse get(int index) {
    return (Mouse)list.get(index);
  }
  public int size() { return list.size(); }
} ///:~

Voici un test pour le nouveau conteneur :

//: c09:MouseListTest.java
public class MouseListTest {
  public static void main(String[] args) {
    MouseList mice = new MouseList();
    for(int i = 0; i < 3; i++)
      mice.add(new Mouse(i));
    for(int i = 0; i < mice.size(); i++)
      MouseTrap.caughtYa(mice.get(i));
  }
} ///:~

Cet exemple est similaire au précédent, sauf que la nouvelle classe MouseList dispose d'un membre private de type ArrayList, et de méthodes identiques à celles fournies par ArrayList. Cependant, ces méthodes n'acceptent et ne produisent pas des Objects génériques, mais seulement des objets Mouse.

Notez que si MouseList avait été dérivée de ArrayList, la méthode add(Mouse) aurait simplement surchargé la méthode existante add(Object) et aucune restriction sur le type d'objets acceptés n'aurait donc été ajoutée. La MouseList est donc un substitut à l'ArrayList, réalisant certaines opérations avant de déléguer la responsabilité (cf. Thinking in Patterns with Java, téléchargeable sur www.BruceEckel.com).

Du fait qu'une MouseList n'accepte qu'une Mouse, l'instruction suivante :

mice.add(new Pigeon());

provoque un message d'erreur durant la phase de compilation. Cette approche, bien que plus fastidieuse du point de vue du code, signalera immédiatement si on utilise un type de façon incorrecte.

Notez aussi qu'aucun transtypage n'est nécessaire lors d'un appel à get() - elle renvoie toujours une Mouse.

Types paramétrés

Ce type de problème n'est pas isolé - nombreux sont les cas dans lesquels on a besoin de créer de nouveaux types basés sur d'autres types, et dans lesquels il serait bon de récupérer des informations de type lors de la phase de compilation. C'est le concept des types paramétrés. En C++, ceci est directement supporté par le langage via les templates. Les futures versions de Java supporteront probablement une implémentation des types paramétrés ; actuellement il faut se contenter de créer des classes similaires à MouseList.

Itérateurs

Chaque classe conteneur fournit des méthodes pour stocker des objets et pour les extraire - après tout, le but d'un conteneur est de stocker des choses. Dans ArrayList, on insère des objets via la méthode add(), et get() est l'un des moyens de récupérer ces objets. ArrayList est relativement souple - il est possible de sélectionner n'importe quel élément à n'importe quel moment, ou de sélectionner plusieurs éléments en même temps en utilisant différents index.

Si on se place à un niveau d'abstraction supérieur, on s'aperçoit d'un inconvénient : on a besoin de connaître le type exact du conteneur afin de l'utiliser. Ceci peut sembler bénin à première vue, mais qu'en est-il si on commence à programmer en utilisant une ArrayList, et qu'on se rende compte par la suite qu'il serait plus efficace d'utiliser une LinkedList à la place ? Ou alors si on veut écrire une portion de code générique qui ne connait pas le type de conteneur avec lequel elle travaille, afin de pouvoir être utilisé avec différents types de conteneurs sans avoir à réécrire ce code ?

Le concept d'itérateur peut être utilisé pour réaliser cette abstraction. Un itérateur est un objet dont le travail est de se déplacer dans une séquence d'objets et de sélectionner chaque objet de cette séquence sans que le programmeur client n'ait à se soucier de la structure sous-jacente de cette séquence. De plus, un itérateur est généralement ce qu'il est convenu d'appeler un objet « léger » : un objet bon marché à construire. Pour cette raison, vous trouverez souvent des contraintes étranges sur les itérateurs ; par exemple, certains itérateurs ne peuvent se déplacer que dans un sens.

L'Iterator Java est l'exemple type d'un itérateur avec ce genre de contraintes. On ne peut faire grand-chose avec mis à part :

  1. Demander à un conteneur de renvoyer un Iterator en utilisant une méthode appelée iterator(). Cet Iterator sera prêt à renvoyer le premier élément dans la séquence au premier appel à sa méthode next().
  2. Récupérer l'objet suivant dans la séquence grâce à sa méthode next().
  3. Vérifier s'il reste encore d'autres objets dans la séquence via la méthode hasNext().
  4. Enlever le dernier élément renvoyé par l'itérateur avec la méthode remove().

Et c'est tout. C'est une implémentation simple d'un itérateur, mais néanmoins puissante (et il existe un ListIterator plus sophistiqué pour les Lists). Pour le voir en action, revisitons le programme CatsAndDogs.java rencontré précédemment dans ce chapitre. Dans sa version originale, la méthode get() était utilisée pour sélectionner chaque élément, mais la version modifiée suivante se sert d'un Iterator :

//: c09:CatsAndDogs2.java
// Conteneur simple utilisant un Iterator.
import java.util.*;

public class CatsAndDogs2 {
  public static void main(String[] args) {
    ArrayList cats = new ArrayList();
    for(int i = 0; i < 7; i++)
      cats.add(new Cat(i));
    Iterator e = cats.iterator();
    while(e.hasNext())
      ((Cat)e.next()).print();
  }
} ///:~

Les dernières lignes utilisent maintenant un Iterator pour se déplacer dans la séquence à la place d'une boucle for. Avec l'Iterator, on n'a pas besoin de se soucier du nombre d'éléments dans le conteneur. Cela est géré via les méthodes hasNext() et next().

Comme autre exemple, considérons maintenant la création d'une méthode générique d'impression :

//: c09:HamsterMaze.java
// Utilisation d'un Iterator.
import java.util.*;

class Hamster {
  private int hamsterNumber;
  Hamster(int i) { hamsterNumber = i; }
  public String toString() {
    return "This is Hamster #" + hamsterNumber;
  }
}

class Printer {
  static void printAll(Iterator e) {
    while(e.hasNext())
      System.out.println(e.next());
  }
}

public class HamsterMaze {
  public static void main(String[] args) {
    ArrayList v = new ArrayList();
    for(int i = 0; i < 3; i++)
      v.add(new Hamster(i));
    Printer.printAll(v.iterator());
  }
} ///:~

Examinez attentivement la méthode printAll() et notez qu'elle ne dispose d'aucune information sur le type de séquence. Tout ce dont elle dispose est un Iterator, et c'est la seule chose dont elle a besoin pour utiliser la séquence : elle peut récupérer l'objet suivant, et savoir si elle se trouve à la fin de la séquence. Cette idée de prendre un conteneur d'objets et de le parcourir pour réaliser une opération sur chaque élément est un concept puissant, et on le retrouvera tout au long de ce livre.

Cet exemple est même encore plus générique, puisqu'il utilise implicitement la méthode Object.toString(). La méthode println() est surchargée pour tous les types scalaires ainsi que dans Object ; dans chaque cas une String est automatiquement produite en appelant la méthode toString() appropriée.

Bien que cela ne soit pas nécessaire, on pourrait être plus explicite en transtypant le résultat, ce qui aurait pour effet d'appeler toString() :

System.out.println((String)e.next());

En général, cependant, on voudra certainement aller au-delà de l'appel des méthodes de la classe Object, et on se heurte de nouveau au problème du transtypage. Il faut donc assumer qu'on a récupéré un Iterator sur une séquence contenant des objets du type particulier qui nous intéresse, et transtyper les objets dans ce type (et recevoir une exception à l'exécution si on se trompe).

Récursion indésirable

Comme les conteneurs standard Java héritent de la classe Object (comme toutes les autres classes), ils contiennent une méthode toString(). Celle-ci a été redéfinie afin de produire une représentation String d'eux-mêmes, incluant les objets qu'ils contiennent. A l'intérieur d'ArrayList, la méthode toString() parcourt les éléments de l'ArrayList et appelle toString() pour chacun d'eux. Supposons qu'on veuille afficher l'adresse de l'instance. Il semble raisonnable de se référer à this (en particulier pour les programmeurs C++ qui sont habitués à cette approche) :

//: c09:InfiniteRecursion.java
// Récursion accidentelle.
import java.util.*;

public class InfiniteRecursion {
  public String toString() {
    return " InfiniteRecursion address: "
      + this + "\n";
  }
  public static void main(String[] args) {
    ArrayList v = new ArrayList();
    for(int i = 0; i < 10; i++)
      v.add(new InfiniteRecursion());
    System.out.println(v);
  }
} ///:~

Si on crée un objet InfiniteRecursion et qu'on veut l'afficher, on se retrouve avec une séquence infinie d'exceptions. C'est également vrai si on place des objets InfiniteRecursion dans une ArrayList et qu'on imprime cette ArrayList comme c'est le cas ici. Ceci est du à la conversion automatique de type sur les Strings. Quand on écrit :

"InfiniteRecursion address: " + this

Le compilateur voit une String suivie par un « + » et quelque chose qui n'est pas une String, il essaie donc de convertir this en String. Il réalise cette conversion en appelant toString(), ce qui produit un appel récursif.

Si on veut réellement imprimer l'adresse de l'objet dans ce cas, la solution est d'appeler la méthode Object.toString(), qui réalise exactement ceci. Il faut donc utiliser super.toString() à la place de this (ceci ne fonctionnera que si on hérite directement de la classe Object, ou si aucune classe parent n'a redéfini la méthode toString()).

Classification des conteneurs

Les Collections et les Maps peuvent être implémentés de différentes manières, à vous de choisir la bonne selon vos besoins. Le diagramme suivant peut aider à s'y retrouver parmi les conteneurs Java 2 :    

Image

Ce diagramme peut sembler un peu surchargé à première vue, mais il n'y a en fait que trois types conteneurs de base : les Maps, les Lists et les Sets, chacun d'entre eux ne proposant que deux ou trois implémentations (avec typiquement une version préférée). Quand on se ramène à cette observation, les conteneurs ne sont plus aussi intimidants.

Les boîtes en pointillé représentent les interfaces, les boîtes en tirets représentent des classes abstract, et les boîtes pleines sont des classes normales (concrètes). Les lignes pointillées indiquent qu'une classe particulière implémente une interface (ou dans le cas d'une classe abstract, implémente partiellement cette interface). Une flèche pleine indique qu'une classe peut produire des objets de la classe sur laquelle la flèche pointe. Par exemple, une Collection peut produire un Iterator, tandis qu'une List peut produire un ListIterator (ainsi qu'un Iterator ordinaire, puisque List est dérivée de Collection).

Les interfaces concernées par le stockage des objets sont Collection, List, Set et Map. Idéalement, la majorité du code qu'on écrit sera destinée à ces interfaces, et le seul endroit où on spécifiera le type précis utilisé est lors de la création. On pourra donc créer une List de cette manière :

List x = new LinkedList();

Ce livre a été écrit par Bruce Eckel ( télécharger la version anglaise : Thinking in java )
Ce chapitre a été traduit par Jérome Quelin ( groupe de traduction )
télécharger la version francaise (PDF) | Commandez le livre en version anglaise (amazon) | télécharger la version anglaise
pages : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java