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 

Il est possible de transformer un tableau en List grâce à la méthode Arrays.asList() :

//: c09:Unsupported.java
// Quielquefois les méthodes définies dans les
// interfaces Collection ne fonctionnent pas!
import java.util.*;

public class Unsupported {
  private static String[] s = {
    "one", "two", "three", "four", "five",
    "six", "seven", "eight", "nine", "ten",
  };
  static List a = Arrays.asList(s);
  static List a2 = a.subList(3, 6);
  public static void main(String[] args) {
    System.out.println(a);
    System.out.println(a2);
    System.out.println(
      "a.contains(" + s[0] + ") = " +
      a.contains(s[0]));
    System.out.println(
      "a.containsAll(a2) = " +
      a.containsAll(a2));
    System.out.println("a.isEmpty() = " +
      a.isEmpty());
    System.out.println(
      "a.indexOf(" + s[5] + ") = " +
      a.indexOf(s[5]));

    // Traversée à reculons :
    ListIterator lit = a.listIterator(a.size());
    while(lit.hasPrevious())
      System.out.print(lit.previous() + " ");
    System.out.println();
    // Modification de la valeur des éléments :
    for(int i = 0; i < a.size(); i++)
      a.set(i, "47");
    System.out.println(a);
    // Compiles, mais ne marche pas :
    lit.add("X"); // Opération non supportée
    a.clear(); // Non supporté
    a.add("eleven"); // Non supporté
    a.addAll(a2); // Non supporté
    a.retainAll(a2); // Non supporté
    a.remove(s[0]); // Non supporté
    a.removeAll(a2); // Non supporté
  }
} ///:~

En fait, seule une partie des interfaces Collection et List est implémentée. Le reste des méthodes provoque l'apparition déplaisante d'une chose appelée UnsupportedOperationException. Vous en apprendrez plus sur les exceptions dans le chapitre suivant, mais pour résumer l'interface Collection - de même que certaines autres interfaces de la bibliothèque de conteneurs Java - contient des méthodes « optionnelles », qui peuvent ou non être « supportées » dans la classe concrète qui implémente cette interface. Appeler une méthode non supportée provoque une UnsupportedOperationException pour indiquer une erreur de programmation.

« Comment ?!? » vous exclamez-vous, incrédule. « Tout l'intérêt des interfaces et des classes de base provient du fait qu'elles promettent que ces méthodes feront quelque chose d'utile ! Cette affirmation rompt cette promesse - non seulement ces méthodes ne réalisent pas d'opération intéressante, mais en plus elles arrêtent le programme ! Qu'en est-il du contrôle de type ? »

Ce n'est pas si grave que ça. Avec une Collection, une List, un Set ou une Map, le compilateur empêche toujours d'appeler des méthodes autres que celles présentes dans cette interface, ce n'est donc pas comme Smalltalk (qui permet d'appeler n'importe quelle méthode sur n'importe objet, appel dont on ne verra la pertinence que lors de l'exécution du programme). De plus, la plupart des méthodes acceptant une Collection comme argument ne font que lire cette Collection - et les méthodes de « lecture » de Collection ne sont pas optionnelles.

Cette approche empêche l'explosion du nombre d'interfaces dans la conception. Les autres conceptions de bibliothèques de conteneurs semblent toujours se terminer par une pléthore d'interfaces pour décrire chacune des variations sur le thème principal et sont donc difficiles à appréhender. Il n'est même pas possible de capturer tous les cas spéciaux dans les interfaces, car n'importe qui peut toujours créer une nouvelle interface. L'approche « opération non supportée » réalise l'un des buts fondamentaux de la bibliothèque de conteneurs Java : les conteneurs sont simples à apprendre et à utiliser ; les opérations non supportées sont des cas spéciaux qui peuvent être appris par la suite. Pour que cette approche fonctionne, toutefois :

  1. Une UnsupportedOperationException doit être un événement rare. C'est à dire que toutes les opérations doivent être supportées dans la majorité des classes, et seuls quelques rares cas spéciaux peuvent ne pas supporter une certaine opération. Ceci est vrai dans la bibliothèque de conteneurs Java, puisque les classes que vous utiliserez dans 99 pour cent des cas - ArrayList, LinkedList, HashSet et HashMap, de même que les autres implémentations concrètes - supportent toutes les opérations. Cette conception fournit une « porte dérobée » si on veut créer une nouvelle Collection sans fournir de définition sensée pour toutes les méthodes de l'interface Collection, et s'intégrer tout de même dans la bibliothèque.
  2. Quand une opération n'est pas supportée, il faut une probabilité raisonnable qu'une UnsupportedOperationException apparaisse lors de l'implémentation plutôt qu'une fois le produit livré au client. Après tout, elle indique une erreur de programmation : une implémentation a été utilisée de manière incorrecte. Ce point est moins certain, et c'est là où la nature expérimentale de cette conception entre en jeu. Nous ne nous apercevrons de son intérêt qu'avec le temps.

Dans l'exemple précédent, Arrays.asList() produit une List supportée par un tableau de taille fixe. Il est donc sensé que les seules opérations supportées soient celles qui ne changent pas la taille du tableau. D'un autre côté, si une nouvelle interface était requise pour exprimer ce différent type de comportement (peut-être appelée « FixedSizeList »), cela laisserait la porte ouverte à la complexité et bientôt on ne saurait où donner de la tête lorsqu'on voudrait utiliser la bibliothèque.

La documentation d'une méthode qui accepte une Collection, une List, un Set ou une Map en argument doit spécifier quelles méthodes optionnelles doivent être implémentées. Par exemple, trier requiert les méthodes set() et Iterator.set(), mais pas add() ou remove().

Les conteneurs Java 1.0 / 1.1

Malheureusement, beaucoup de code a été écrit en utilisant les conteneurs Java 1.0 / 1.1, et ajourd'hui encore ces classes continuent d'être utilisées dans du nouveau code. Bien qu'il faille éviter d'utiliser les anciens conteneurs lorsqu'on produit du code nouveau, il faut toutefois être conscient qu'ils existent. Cependant, les anciens conteneurs étaient plutôt limités, il n'y a donc pas tellement à en dire sur eux (puisqu'ils appartiennent au passé, j'éviterais de trop me pencher sur certaines horribles décisions de conception).

Vector & Enumeration

Le Vector était la seule séquence auto-redimensionnable dans Java 1.0 / 1.1, ce qui favorisa son utilisation. Ses défauts sont trop nombreux pour être décrits ici (se référer à la première version de ce livre, disponible sur le CD ROM de ce livre et téléchargeable sur www.BruceEckel.com). Fondamentalement, il s'agit d'une ArrayList avec des noms de méthodes longs et maladroits. Dans la bibliothèque de conteneurs Java 2, la classe Vector a été adaptée afin de pouvoir s'intégrer comme une Collection et une List, et donc dans l'exemple suivant la méthode Collections2.fill() est utilisée avec succès. Ceci peut mener à des effets pervers, car on pourrait croire que la classe Vector a été améliorée alors qu'elle n'a été incluse que pour supporter du code pré-Java2.

La version Java 1.0 / 1.1 de l'itérateur a choisi d'inventer un nouveau nom, « énumération », au lieu d'utiliser un terme déjà familier pour tout le monde. L'interface Enumeration est plus petite qu'Iterator, ne possédant que deux méthodes, et utilise des noms de méthodes plus longs : boolean hasMoreElements() renvoie true si l'énumération contient encore des éléments, et Object nextElement() renvoie l'élément suivant de cette énumération si il y en a encore (autrement elle génére une exception).

Enumeration n'est qu'une interface, et non une implémentation, et même les nouvelles bibliothèques utilisent encore quelquefois l'ancienne Enumeration - ce qui est malheureux mais généralement sans conséquence. Bien qu'il faille utiliser un Iterator quand on le peut, on peut encore rencontrer des bibliothèques qui renvoient des Enumerations.

Toute Collection peut produire une Enumeration via la méthode Collections.enumerations(), comme le montre cet exemple :

//: c09:Enumerations.java
// Vectors et Enumerations de Java 1.0 / 1.1.
import java.util.*;
import com.bruceeckel.util.*;

class Enumerations {
  public static void main(String[] args) {
    Vector v = new Vector();
    Collections2.fill(
      v, Collections2.countries, 100);
    Enumeration e = v.elements();
    while(e.hasMoreElements())
      System.out.println(e.nextElement());
    // Produit une Enumeration à partir d'une Collection:
    e = Collections.enumeration(new ArrayList());
  }
} ///:~

La classe Vector de Java 1.0 / 1.1 ne dispose que d'une méthode addElement(), mais fill() utilise la méthode add() qui a été copiée quand Vector a été transformé en List. Pour produire une Enumeration, il faut appeler elements(), on peut alors l'utiliser pour réaliser une itération en avant.

La dernière ligne crée une ArrayList et utilise enumeration() pour adapter une Enumeration à partir de l'Iterator de l'ArrayList. Ainsi, si on a du vieux code qui requiert une Enumeration, on peut tout de même utiliser les nouveaux conteneurs.

Hashtable

Comme on a pu le voir dans les tests de performances de ce chapitre, la Hashtable de base est très similaire au HashMap, et ce même jusqu'aux noms des méthodes. Il n'y a aucune raison d'utiliser Hashtable au lieu de HashMap dans du nouveau code.

Stack

Le concept de la pile a déjà été introduit plus tôt avec les LinkedLists. Ce qui est relativement étrange à propos de la classe Stack Java 1.0 / 1.1 est qu'elle est dérivée de Vector au lieu d'utiliser un Vector comme composant de base. Elle possède donc toutes les caractéristiques et comportements d'un Vector en plus des comportements additionnels d'une Stack. Il est difficile de savoir si les concepteurs ont choisi délibérément cette approche en la jugeant particulièrement pratique ou si c'était juste une conception naïve.

Voici une simple illustration de Stack qui « pousse » chaque ligne d'un tableau String :

//: c09:Stacks.java
// Illustration de la classe Stack.
import java.util.*;

public class Stacks {
  static String[] months = {
    "January", "February", "March", "April",
    "May", "June", "July", "August", "September",
    "October", "November", "December" };
  public static void main(String[] args) {
    Stack stk = new Stack();
    for(int i = 0; i < months.length; i++)
      stk.push(months[i] + " ");
    System.out.println("stk = " + stk);
    // Traiter une pile comme un Vector :
    stk.addElement("The last line");
    System.out.println(
      "element 5 = " + stk.elementAt(5));
    System.out.println("popping elements:");
    while(!stk.empty())
      System.out.println(stk.pop());
  }
} ///:~

Chaque ligne du tableau months est insérée dans la Stack avec push(), et récupérée par la suite au sommet de la pile avec un pop(). On peut aussi réaliser des opérations de Vector sur l'objet Stack. Ceci est possible car, de par les propriétés de l'héritage, une Stack est un Vector. Donc toutes les opérations qui peuvent être effectuées sur un Vector peuvent aussi être réalisées sur une Stack, comme elementAt().

Ainsi que mentionné précédemment, il vaut mieux utiliser une LinkedList si on souhaite un comportement de pile.

BitSet

Un BitSet est utile si on veut stocker efficacement un grand nombre d'informations on-off. Cette classe n'est efficace toutefois que sur le plan de la taille ; si le but recherché est un accès performant, mieux vaut se tourner vers un tableau de quelque type natif.

De plus, la taille minimum d'un BitSet est celle d'un long, soit 64 bits. Ce qui implique que si on stocke n'importe quelle quantité plus petite, telle que 8 bits, un BitSet introduira du gaspillage ; il vaudrait donc mieux créer sa propre classe, ou utiliser un tableau, pour stocker les flags si la taille est un problème.

Un conteneur normal se redimensionne si on ajoute de nouveaux éléments, et un BitSet n'échappe pas à la règle. L'exemple suivant montre comme le BitSet fonctionne :

//: c09:Bits.java
// Illustration des BitSets.
import java.util.*;

public class Bits {
  static void printBitSet(BitSet b) {
    System.out.println("bits: " + b);
    String bbits = new String();
    for(int j = 0; j < b.size() ; j++)
      bbits += (b.get(j) ? "1" : "0");
    System.out.println("bit pattern: " + bbits);
  }
  public static void main(String[] args) {
    Random rand = new Random();
    // Récupère les LSB (Less Significant Bits - NdT: bit de poids fort) de nextInt() :
    byte bt = (byte)rand.nextInt();
    BitSet bb = new BitSet();
    for(int i = 7; i >=0; i--)
      if(((1 << i) &  bt) != 0)
        bb.set(i);
      else
        bb.clear(i);
    System.out.println("byte value: " + bt);
    printBitSet(bb);

    short st = (short)rand.nextInt();
    BitSet bs = new BitSet();
    for(int i = 15; i >=0; i--)
      if(((1 << i) &  st) != 0)
        bs.set(i);
      else
        bs.clear(i);
    System.out.println("short value: " + st);
    printBitSet(bs);

    int it = rand.nextInt();
    BitSet bi = new BitSet();
    for(int i = 31; i >=0; i--)
      if(((1 << i) &  it) != 0)
        bi.set(i);
      else
        bi.clear(i);
    System.out.println("int value: " + it);
    printBitSet(bi);

    // Teste les bitsets >= 64 bits:
    BitSet b127 = new BitSet();
    b127.set(127);
    System.out.println("set bit 127: " + b127);
    BitSet b255 = new BitSet(65);
    b255.set(255);
    System.out.println("set bit 255: " + b255);
    BitSet b1023 = new BitSet(512);
    b1023.set(1023);
    b1023.set(1024);
    System.out.println("set bit 1023: " + b1023);
  }
} ///:~

Le générateur de nombres aléatoires est utilisé pour générer un byte, un short et un int au hasard, et chacun est transformé en motif de bits dans un BitSet. Ceci fonctionne bien puisque la taille d'un BitSet est de 64 bits, et donc aucune de ces opérations ne nécessite un changement de taille. Un BitSet de 512 bits est alors créé. Le constructeur alloue de la place pour deux fois ce nombre de bits. Cependant, on peut tout de même positionner le bit 1024 ou même au delà.

Résumé

Pour résumer les conteneurs fournis dans la bibliothèque standard Java :

  1. Un tableau associe des indices numériques à des objets. Il stocke des objets d'un type connu afin de ne pas avoir à transtyper le résultat quand on récupère un objet. Il peut être multidimensions, et peut stocker des scalaires. Cependant, sa taille ne peut être modifiée une fois créé.
  2. Une Collection stocke des éléments, alors qu'une Map stocke des paires d'éléments associés.
  3. Comme un tableau, une List associe aussi des indices numériques à des objets - on peut penser aux tableaux et aux Lists comme à des conteneurs ordonnés. Une List se redimensionne automatiquement si on y ajoute des éléments. Mais une List ne peut stocker que des références sur des Objects, elle ne peut donc stocker des scalaires et il faut toujours transtyper le résultat quand on récupère une référence sur un Object du conteneur.
  4. Utiliser une ArrayList si on doit effectuer un grand nombre d'accès aléatoires, et une LinkedList si on doit réaliser un grand nombre d'insertions et de suppressions au sein de la liste.
  5. Le comportement de files, files doubles et piles est fourni via les LinkedLists.
  6. Une Map est une façon d'associer non pas des nombres, mais des objets à d'autres objets. La conception d'un HashMap est focalisée sur les temps d'accès, tandis qu'un TreeMap garde ses clefs ordonnées, et n'est donc pas aussi rapide qu'un HashMap.
  7. Un Set n'accepte qu'une instance de valeur de chaque objet. Les HashSets fournissent des temps d'accès optimaux, alors que les TreeSets gardent leurs éléments ordonnés.
  8. Il n'y a aucune raison d'utiliser les anciennes classes Vector, Hashtable et Stack dans du nouveau code.

Les conteneurs sont des outils qu'on peut utiliser jour après jour pour rendre les programmes plus simples, plus puissants et plus efficaces.

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