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

  A) Passage et Retour d'Objets

pages : 1 2 3 4 5 6 

  1. Implémenter l'interface Cloneable.
  2. Redéfinir clone().
  3. Appeler super.clone() depuis la méthode clone() de la classe.
  4. Intercepter les exceptions à l'intérieur de la méthode clone().

Ceci produira l'effet désiré.

Le constructeur de copie

Le clonage peut sembler un processus compliqué à mettre en oeuvre. On se dit qu'il doit certainement exister une autre alternative, et souvent on envisage (surtout les programmeurs C++) de créer un constructeur spécial dont le travail est de dupliquer un objet. En C++, on l'appelle le constructeur de copie. Cela semble a priori la solution la plus évidente, mais en fait elle ne fonctionne pas. Voici un exemple.

//: appendixa:CopyConstructor.java
// Un constructeur pour copier un objet du même
// type, dans une tentaive de créer une copie locale.

class FruitQualities {
  private int weight;
  private int color;
  private int firmness;
  private int ripeness;
  private int smell;
  // etc...
  FruitQualities() { // Constucteur par défaut.
    // fait des tas de choses utiles...
  }
  // D'autres constructeurs :
  // ...
  // Constructeur de copie :
  FruitQualities(FruitQualities f) {
    weight = f.weight;
    color = f.color;
    firmness = f.firmness;
    ripeness = f.ripeness;
    smell = f.smell;
    // etc...
  }
}

class Seed {
  // Membres...
  Seed() { /* Constructeur par défaut */ }
  Seed(Seed s) { /* Constructeur de copie */ }
}

class Fruit {
  private FruitQualities fq;
  private int seeds;
  private Seed[] s;
  Fruit(FruitQualities q, int seedCount) {
    fq = q;
    seeds = seedCount;
    s = new Seed[seeds];
    for(int i = 0; i < seeds; i++)
      s[i] = new Seed();
  }
  // Autres constructeurs :
  // ...
  // Constructeur de copie :
  Fruit(Fruit f) {
    fq = new FruitQualities(f.fq);
    seeds = f.seeds;
    // Appelle le constructeur de copie sur toutes les Seed :
    for(int i = 0; i < seeds; i++)
      s[i] = new Seed(f.s[i]);
    // D'autres activités du constructeur de copie...
  }
  // Pour permettre aux constructeurs dérivés (ou aux
  // autres méthodes) de changer les qualités :
  protected void addQualities(FruitQualities q) {
    fq = q;
  }
  protected FruitQualities getQualities() {
    return fq;
  }
}

class Tomato extends Fruit {
  Tomato() {
    super(new FruitQualities(), 100);
  }
  Tomato(Tomato t) { // Constructeur de copie.
    super(t); // Transtypage pour le constructeur de copie parent.
    // D'autres activités du constructeur de copie...
  }
}

class ZebraQualities extends FruitQualities {
  private int stripedness;
  ZebraQualities() { // Constructeur par défaut.
    // Fait des tas de choses utiles...
  }
  ZebraQualities(ZebraQualities z) {
    super(z);
    stripedness = z.stripedness;
  }
}

class GreenZebra extends Tomato {
  GreenZebra() {
    addQualities(new ZebraQualities());
  }
  GreenZebra(GreenZebra g) {
    super(g); // Appelle Tomato(Tomato)
    // Restitue les bonnes qualités :
    addQualities(new ZebraQualities());
  }
  void evaluate() {
    ZebraQualities zq =
      (ZebraQualities)getQualities();
    // Utilise les qualités
    // ...
  }
}

public class CopyConstructor {
  public static void ripen(Tomato t) {
    // Utilise le « constructeur de copie » :
    t = new Tomato(t);
    System.out.println("In ripen, t is a " +
      t.getClass().getName());
  }
  public static void slice(Fruit f) {
    f = new Fruit(f); // Hmmm... est-ce que cela va marcher ?
    System.out.println("In slice, f is a " +
      f.getClass().getName());
  }
  public static void main(String[] args) {
    Tomato tomato = new Tomato();
    ripen(tomato); // OK
    slice(tomato); // OOPS!
    GreenZebra g = new GreenZebra();
    ripen(g); // OOPS!
    slice(g); // OOPS!
    g.evaluate();
  }
} ///:~

Ceci semble un peu étrange à première vue. Bien sûr, un fruit a des qualités, mais pourquoi ne pas mettre les données membres représentant ces qualités directement dans la classe Fruit ? Deux raisons à cela. La première est qu'on veut pouvoir facilement insérer ou changer les qualités. Notez que Fruit possède une méthode protected addQualities() qui permet aux classes dérivées de le faire (on pourrait croire que la démarche logique serait d'avoir un constructeur protected dans Fruit qui accepte un argument FruitQualities, mais les constructeurs ne sont pas hérités et ne seraient pas disponibles dans les classes dérivées). En créant une classe séparée pour la qualité des fruits, on dispose d'une plus grande flexibilité, incluant la possibilité de changer les qualités d'un objet Fruit pendant sa durée de vie.

La deuxième raison pour laquelle on a décidé de créer une classe FruitQualities est dans le cas où on veut ajouter de nouvelles qualités ou en changer le comportement via héritage ou polymorphisme. Notez que pour les GreenZebra (qui sont réellement un type de tomates - j'en ai cultivé et elles sont fabuleuses), le constructeur appelle addQualities() et lui passe un objet ZebraQualities, qui est dérivé de FruitQualities et peut donc être attaché à la référence FruitQualities de la classe de base. Bien sûr, quand GreenZebra utilise les FruitQualities il doit le transtyper dans le type correct (comme dans evaluate()), mais il sait que le type est toujours ZebraQualities.

Vous noterez aussi qu'il existe une classe Seed, et qu'un Fruit (qui par définition porte ses propres graines)  name="fnB82">[82]contient un tableau de Seeds.

Enfin, vous noterez que chaque classe dispose d'un constructeur de copie, et que chaque constructeur de copie doit s'occuper d'appeler le constructeur de copie de la classe de base et des objets membres pour réaliser une copie profonde. Le constructeur de copie est testé dans la classe CopyConstructor. La méthode ripen() accepte un argument Tomato et réalise une construction de copie afin de dupliquer l'objet :

t = new Tomato(t);

tandis que slice( ) accepte un objet Fruit plus générique et le duplique aussi :

f = new Fruit(f);

Ces deux méthodes sont testées avec différents types de Fruit dans main(). Voici la sortie produite :

In ripen, t is a Tomato
In slice, f is a Fruit
In ripen, t is a Tomato
In slice, f is a Fruit

C'est là que le problème survient. Après la construction de copie réalisée dans slice() sur l'objet Tomato, l'objet résultant n'est plus un objet Tomato, mais seulement un Fruit. Il a perdu toute sa tomaticité. De même, quand on prend une GreenZebra, ripen() et slice() la transforment toutes les deux en Tomato et Fruit, respectivement. La technique du constructeur de copie ne fonctionne donc pas en Java pour créer une copie locale d'un objet.

Pourquoi cela fonctionne-t-il en C++ et pas en Java ?

Le constructeur de copie est un mécanisme fondamental en C++, puisqu'il permet de créer automatiquement une copie locale d'un objet. Mais l'exemple précédent prouve que cela ne fonctionne pas en Java. Pourquoi ? En Java, toutes les entités manipulées sont des références, tandis qu'en C++ on peut manipuler soit des références sur les objets soit les objets directement. C'est le rôle du constructeur de copie en C++ : prendre un objet et permettre son passage par valeur, donc dupliquer l'objet. Cela fonctionne donc très bien en C++, mais il faut garder présent à l'esprit que ce mécanisme est à proscrire en Java.

Classes en lecture seule

Bien que la copie locale produite par clone() donne les résultats escomptés dans les cas appropriés, c'est un exemple où le programmeur (l'auteur de la méthode) est responsable des effets secondaires indésirables de l'aliasing. Que se passe-t-il dans le cas où on construit une bibliothèque tellement générique et utilisée qu'on ne peut supposer qu'elle sera toujours clonée aux bons endroits ? Ou alors, que se passe-t-il si on veut permettre l'aliasing dans un souci d'efficacité - afin de prévenir la duplication inutile d'un objet - mais qu'on n'en veut pas les effets secondaires négatifs ?

Une solution est de créer des objets immuables appartenant à des classes en lecture seule. On peut définir une classe telle qu'aucune méthode de la classe ne modifie l'état interne de l'objet. Dans une telle classe, l'aliasing n'a aucun impact puisqu'on peut seulement lire son état interne, donc même si plusieurs portions de code utilisent le même objet cela ne pose pas de problèmes.

Par exemple, la bibliothèque standard Java contient des classes name="Index2266">« wrapper »  pour tous les types fondamentaux. Vous avez peut-être déjà découvert que si on veut stocker un int dans un conteneur tel qu'une ArrayList (qui n'accepte que des références sur un Object), on peut insérer l'int dans la classe Integer de la bibliothèque standard :

//: appendixa:ImmutableInteger.java
// La classe Integer ne peut pas être modifiée.
import java.util.*;

public class ImmutableInteger {
  public static void main(String[] args) {
    ArrayList v = new ArrayList();
    for(int i = 0; i < 10; i++)
      v.add(new Integer(i));
    // Mais comment changer l'int à
    // l'intérieur de Integer?
  }
} ///:~

La classe Integer (de même que toutes les classes « wrapper » pour les scalaires) implémentent l'immuabilité d'une manière simple : elles ne possèdent pas de méthodes qui permettent de modifier l'objet.

Si on a besoin d'un objet qui contient un scalaire qui peut être modifié, il faut la créer soi-même. Heureusement, ceci se fait facilement :

//: appendixa:MutableInteger.java
// Une classe wrapper modifiable.
import java.util.*;

class IntValue {
  int n;
  IntValue(int x) { n = x; }
  public String toString() {
    return Integer.toString(n);
  }
}

public class MutableInteger {
  public static void main(String[] args) {
    ArrayList v = new ArrayList();
    for(int i = 0; i < 10; i++)
      v.add(new IntValue(i));
    System.out.println(v);
    for(int i = 0; i < v.size(); i++)
      ((IntValue)v.get(i)).n++;
    System.out.println(v);
  }
} ///:~

Notez que n est amical pour simplifier le codage.

IntValue peut même être encore plus simple si l'initialisation à zéro est acceptable (auquel cas on n'a plus besoin du constructeur) et qu'on n'a pas besoin d'imprimer cet objet (auquel cas on n'a pas besoin de toString()) :

class IntValue { int n; }

La recherche de l'élément et son transtypage par la suite est un peu lourd et maladroit, mais c'est une particularité de ArrayList et non de IntValue.

Créer des classes en lecture seule

Il est possible de créer ses propres classes en lecture seule. Voici un exemple :

//: appendixa:Immutable1.java
// Des objets qu'on ne peut modifier
// ne craignent pas l'aliasing.

public class Immutable1 {
  private int data;
  public Immutable1(int initVal) {
    data = initVal;
  }
  public int read() { return data; }
  public boolean nonzero() { return data != 0; }
  public Immutable1 quadruple() {
    return new Immutable1(data * 4);
  }
  static void f(Immutable1 i1) {
    Immutable1 quad = i1.quadruple();
    System.out.println("i1 = " + i1.read());
    System.out.println("quad = " + quad.read());
  }
  public static void main(String[] args) {
    Immutable1 x = new Immutable1(47);
    System.out.println("x = " + x.read());
    f(x);
    System.out.println("x = " + x.read());
  }
} ///:~

Toutes les données sont private, et aucune méthode public ne modifie les données. En effet, la méthode qui semble modifier l'objet, quadruple(), crée en fait un nouvel objet Immutable1 sans modifier l'objet original.

La méthode f() accepte un objet Immutable1 et effectue diverses opérations avec, et la sortie de main() démontre que x ne subit aucun changement. Ainsi, l'objet x peut être aliasé autant qu'on le veut sans risque puisque la classe Immutable1 a été conçue afin de guarantir que les objets ne puissent être modifiés.

L'inconvénient de l'immuabilité

Créer une classe immuable semble à première vue une solution élégante. Cependant, dès qu'on a besoin de modifier un objet de ce nouveau type, il faut supporter le coût supplémentaire de la création d'un nouvel objet, ce qui implique aussi un passage plus fréquent du ramasse-miettes. Cela n'est pas un problème pour certaines classes, mais cela est trop coûteux pour certaines autres (telles que la classes String).

La solution est de créer une classe compagnon qui, elle, peut être modifiée. Ainsi, quand on effectue beaucoup de modifications, on peut basculer sur la classe compagnon modifiable et revenir à la classe immuable une fois qu'on en a terminé.

L'exemple ci-dessus peut être modifié pour montrer ce mécanisme :

//: appendixa:Immutable2.java
// Une classe compagnon pour modifier
// des objets immuables.

class Mutable {
  private int data;
  public Mutable(int initVal) {
    data = initVal;
  }
  public Mutable add(int x) {
    data += x;
    return this;
  }
  public Mutable multiply(int x) {
    data *= x;
    return this;
  }
  public Immutable2 makeImmutable2() {
    return new Immutable2(data);
  }
}

public class Immutable2 {
  private int data;
  public Immutable2(int initVal) {
    data = initVal;
  }
  public int read() { return data; }
  public boolean nonzero() { return data != 0; }
  public Immutable2 add(int x) {
    return new Immutable2(data + x);
  }
  public Immutable2 multiply(int x) {
    return new Immutable2(data * x);
  }
  public Mutable makeMutable() {
    return new Mutable(data);
  }
  public static Immutable2 modify1(Immutable2 y){
    Immutable2 val = y.add(12);
    val = val.multiply(3);
    val = val.add(11);
    val = val.multiply(2);
    return val;
  }
  // Ceci produit le même résultat :
  public static Immutable2 modify2(Immutable2 y){
    Mutable m = y.makeMutable();
    m.add(12).multiply(3).add(11).multiply(2);
    return m.makeImmutable2();
  }
  public static void main(String[] args) {
    Immutable2 i2 = new Immutable2(47);
    Immutable2 r1 = modify1(i2);
    Immutable2 r2 = modify2(i2);
    System.out.println("i2 = " + i2.read());
    System.out.println("r1 = " + r1.read());
    System.out.println("r2 = " + r2.read());
  }
} ///:~

Immutable2 contient des méthodes qui, comme précédemment, préservent l'immuabilité des objets en créant de nouveaux objets dès qu'une modification est demandée. Ce sont les méthodes add() et multiply(). La classe compagnon est appelée Mutable, et possède aussi des méthodes add() et multiply(), mais ces méthodes modifient l'objet Mutable au lieu d'en créer un nouveau. De plus, Mutable possède une méthode qui utilise ses données pour créer un objet Immutable2 et vice-versa.

Les deux méthodes static modify1() et modify2() montrent deux approches différentes pour arriver au même résultat. Dans modify1(), tout est réalisé dans la classe Immutable2 et donc quatre nouveaux objets Immutable2 sont créés au cours du processus (et chaque fois que val est réassignée, l'instance précédente est récupérée par le ramasse-miettes).

Dans la méthode modify2(), on peut voir que la première action réalisée est de prendre l'objet Immutable2 y et d'en produire une forme Mutable (c'est comme si on appelait clone() vue précédemment, mais cette fois un différent type d'objet est créé). L'objet Mutable est alors utilisé pour réaliser un grand nombre d'opérations sans nécessiter la création de nombreux objets. Puis il est retransformé en objet Immutable2. On n'a donc créé que deux nouveaux objets (l'objet Mutable et le résultat Immutable2) au lieu de quatre.

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 
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