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 

Une fois les détails d'implémentation de clone() compris, il est facile de créer des classes facilement duplicables pour produire des copies locales :

//: appendixa:LocalCopy.java
// Créer des copies locales avec clone().
import java.util.*;

class MyObject implements Cloneable {
  int i;
  MyObject(int ii) { i = ii; }
  public Object clone() {
    Object o = null;
    try {
      o = super.clone();
    } catch(CloneNotSupportedException e) {
      System.err.println("MyObject can't clone");
    }
    return o;
  }
  public String toString() {
    return Integer.toString(i);
  }
}

public class LocalCopy {
  static MyObject g(MyObject v) {
    // Passage par référence, modifie l'objet extérieur :
    v.i++;
    return v;
  }
  static MyObject f(MyObject v) {
    v = (MyObject)v.clone(); // Copie locale
    v.i++;
    return v;
  }
  public static void main(String[] args) {
    MyObject a = new MyObject(11);
    MyObject b = g(a);
    // On teste l'équivalence des références,
    // non pas l'équivalence des objets :
    if(a == b)
      System.out.println("a == b");
    else
      System.out.println("a != b");
    System.out.println("a = " + a);
    System.out.println("b = " + b);
    MyObject c = new MyObject(47);
    MyObject d = f(c);
    if(c == d)
      System.out.println("c == d");
    else
      System.out.println("c != d");
    System.out.println("c = " + c);
    System.out.println("d = " + d);
  }
} ///:~

Tout d'abord, clone() doit être accessible, il faut donc la rendre public. Ensuite, il faut que clone() commence par appeler la version de clone() de la classe de base. La méthode clone() appelée ici est celle prédéfinie dans Object, et on peut l'appeler car elle est protected et donc accessible depuis les classes dérivées.

Object.clone() calcule la taille de l'objet, réserve assez de mémoire pour en créer un nouveau, et copie tous les bits de l'ancien dans le nouveau. On appelle cela une copie bit à bit, et c'est typiquement ce qu'on attend d'une méthode clone(). Mais avant que Object.clone() ne réalise ces opérations, elle vérifie d'abord que la classe est Cloneable - c'est à dire, si elle implémente l'interface Cloneable. Si ce n'est pas le cas, Object.clone() génère une exception CloneNotSupportedException pour indiquer qu'on ne peut la cloner. C'est pourquoi il faut entourer l'appel à super.clone() dans un bloc try-catch, pour intercepter une exception qui théoriquement ne devrait jamais arriver (parce qu'on a implémenté l'interface Cloneable).

Dans LocalCopy, les deux méthodes g() et f() démontrent la différence entre les deux approches concernant le passage d'arguments. g() montre le passage par référence en modifiant l'objet extérieur et en retournant une référence à cet objet extérieur, tandis que f() clone l'argument, se détachant de lui et laissant l'objet original inchangé. Elle peut alors faire ce qu'elle veut, et même retourner une référence sur ce nouvel objet sans impacter aucunement l'original. À noter l'instruction quelque peu curieuse :

v = (MyObject)v.clone();

C'est ici que la copie locale est créée. Afin d'éviter la confusion induite par une telle instruction, il faut se rappeler que cet idiome plutôt étrange est tout à fait légal en Java parce que chaque identifiant d'objet est en fait une référence. La référence v est donc utilisée pour réaliser une copie de l'objet qu'il référence grâce à clone(), qui renvoie une référence au type de base Object (car c'est ainsi qu'est définie Object.clone()) qui doit ensuite être transtypée dans le bon type.

Dans main(), la différence entre les effets des deux approches de passage d'arguments est testée. La sortie est :

a == b
a = 12
b = 12
c != d
c = 47
d = 48

Il est important de noter que les tests d'équivalence en Java ne regardent pas à l'intérieur des objets comparés pour voir si leurs valeurs sont les mêmes. Les opérateurs == et != comparent simplement les références. Si les adresses à l'intérieur des références sont les mêmes, les références pointent sur le même objet et sont donc « égales ». Les opérateurs testent donc si les références sont aliasées sur le même objet !

Le mécanisme de Object.clone( )

Que se passe-t-il réellement quand Object.clone() est appelé, qui rende si essentiel d'appeler super.clone() quand on redéfinit clone() dans une classe ? La méthode clone() dans la classe racine (ie, Object) est chargée de la réservation de la mémoire nécessaire au stockage et de la copie bit à bit de l'objet original dans le nouvel espace de stockage. C'est à dire, elle ne crée pas seulement l'emplacement et copie un Object - elle calcule précisément la taille de l'objet copié et le duplique. Puisque tout cela se passe dans le code de la méthode clone() définie dans la classe de base (qui n'a aucune idée de ce qui est dérivé à partir d'elle), vous pouvez deviner que le processus implique RTTI pour déterminer quel est réellement l'objet cloné. De cette façon, la méthode clone() peut réserver la bonne quantité de mémoire et réaliser une copie bit à bit correcte pour ce type.

Quoi qu'on fasse, la première partie du processus de clonage devrait être un appel à super.clone(). Ceci pose les fondations de l'opération de clonage en créant une copie parfaite. On peut alors effectuer les autres opérations nécessaires pour terminer le clonage.

Afin de savoir exactement quelles sont ces autres opérations, il faut savoir ce que Object.clone() nous fournit. En particulier, clone-t-il automatiquement la destination de toutes les références ? L'exemple suivant teste cela :

//: appendixa:Snake.java
// Teste le clonage pour voir si la destination
// des références sont aussi clonées.

public class Snake implements Cloneable {
  private Snake next;
  private char c;
  // Valeur de i == nombre de segments
  Snake(int i, char x) {
    c = x;
    if(--i > 0)
      next = new Snake(i, (char)(x + 1));
  }
  void increment() {
    c++;
    if(next != null)
      next.increment();
  }
  public String toString() {
    String s = ":" + c;
    if(next != null)
      s += next.toString();
    return s;
  }
  public Object clone() {
    Object o = null;
    try {
      o = super.clone();
    } catch(CloneNotSupportedException e) {
      System.err.println("Snake can't clone");
    }
    return o;
  }
  public static void main(String[] args) {
    Snake s = new Snake(5, 'a');
    System.out.println("s = " + s);
    Snake s2 = (Snake)s.clone();
    System.out.println("s2 = " + s2);
    s.increment();
    System.out.println(
      "after s.increment, s2 = " + s2);
  }
} ///:~

Un Snake est composé d'un ensemble de segments, chacun de type Snake. C'est donc une liste chaînée simple. Les segments sont créés récursivement, en décrémentant le premier argument du constructeur pour chaque segment jusqu'à ce qu'on atteigne zéro. Afin de donner à chaque segment une étiquette unique, le deuxième argument, un char, est incrémenté pour chaque appel récursif au constructeur.

La méthode increment() incrémente récursivement chaque étiquette afin de pouvoir observer les modifications, et toString() affiche récursivement chaque étiquette. La sortie est la suivante :

s = :a:b:c:d:e
s2 = :a:b:c:d:e
after s.increment, s2 = :a:c:d:e:f

Ceci veut dire que seul le premier segment est dupliqué par Object.clone(), qui ne réalise donc qu'une copie superficielle. Si on veut dupliquer tout le Snake - une copie profonde - il faut réaliser d'autres opérations dans la méthode clone() redéfinie.

Typiquement il faudra donc faire un appel à super.clone() dans chaque classe dérivée d'une classe cloneable pour s'assurer que toutes les opérations de la classe de base (y compris Object.clone()) soient effectuées. Puis cela sera suivi par un appel explicite à clone() pour chaque référence contenue dans l'objet ; sinon ces références seront aliasées sur celles de l'objet original. Le mécanisme est le même que lorsque les constructeurs sont appelés - constructeur de la classe de base d'abord, puis constructeur de la classe dérivée suivante, et ainsi de suite jusqu'au constructeur de la classe dérivée la plus lointaine de la classe de base. La différence est que clone() n'est pas un constructeur, il n'y a donc rien qui permette d'automatiser le processus. Il faut s'assurer de le faire soi-même.

Cloner un objet composé

Il se pose un problème quand on essaye de faire une copie profonde d'un objet composé. Il faut faire l'hypothèse que la méthode clone() des objets membres va à son tour réaliser une copie profonde de leurs références, et ainsi de suite. Il s'agit d'un engagement. Cela veut dire que pour qu'une copie profonde fonctionne il faut soit contrôler tout le code dans toutes les classes, soit en savoir suffisamment sur les classes impliquées dans la copie profonde pour être sûr qu'elles réalisent leur propre copie profonde correctement.

Cet exemple montre ce qu'il faut accomplir pour réaliser une copie profonde d'un objet composé :

//: appendixa:DeepCopy.java
// Clonage d'un objet composé.

class DepthReading implements Cloneable {
  private double depth;
  public DepthReading(double depth) {
    this.depth = depth;
  }
  public Object clone() {
    Object o = null;
    try {
      o = super.clone();
    } catch(CloneNotSupportedException e) {
      e.printStackTrace(System.err);
    }
    return o;
  }
}

class TemperatureReading implements Cloneable {
  private long time;
  private double temperature;
  public TemperatureReading(double temperature) {
    time = System.currentTimeMillis();
    this.temperature = temperature;
  }
  public Object clone() {
    Object o = null;
    try {
      o = super.clone();
    } catch(CloneNotSupportedException e) {
      e.printStackTrace(System.err);
    }
    return o;
  }
}

class OceanReading implements Cloneable {
  private DepthReading depth;
  private TemperatureReading temperature;
  public OceanReading(double tdata, double ddata){
    temperature = new TemperatureReading(tdata);
    depth = new DepthReading(ddata);
  }
  public Object clone() {
    OceanReading o = null;
    try {
      o = (OceanReading)super.clone();
    } catch(CloneNotSupportedException e) {
      e.printStackTrace(System.err);
    }
    // On doit cloner les références :
    o.depth = (DepthReading)o.depth.clone();
    o.temperature =
      (TemperatureReading)o.temperature.clone();
    return o; // Transtypage en Object
  }
}

public class DeepCopy {
  public static void main(String[] args) {
    OceanReading reading =
      new OceanReading(33.9, 100.5);
    // Maintenant on le clone :
    OceanReading r =
      (OceanReading)reading.clone();
  }
} ///:~

DepthReading et TemperatureReading sont quasi identiques ; elles ne contiennent toutes les deux que des scalaires. La méthode clone() est donc relativement simple : elle appelle super.clone() et renvoie le résultat. Notez que le code de clone() des deux classes est identique.

OceanReading est composée d'objets DepthReading et TemperatureReading ; pour réaliser une copie profonde, sa méthode clone() doit donc cloner les références à l'intérieur de OceanReading. Pour réaliser ceci, le résultat de super.clone() doit être transtypé dans un objet OceanReading (afin de pouvoir accéder aux références depth et temperature).

Copie profonde d'une ArrayList

Reprenons l'exemple ArrayList exposé plus tôt dans cette annexe. Cette fois-ci la classe Int2 est cloneable, on peut donc réaliser une copie profonde de l'ArrayList :

//: appendixa:AddingClone.java
// Il faut apporter quelques modifications
// pour que vos classes soient cloneables.
import java.util.*;

class Int2 implements Cloneable {
  private int i;
  public Int2(int ii) { i = ii; }
  public void increment() { i++; }
  public String toString() {
    return Integer.toString(i);
  }
  public Object clone() {
    Object o = null;
    try {
      o = super.clone();
    } catch(CloneNotSupportedException e) {
      System.err.println("Int2 can't clone");
    }
    return o;
  }
}

// Une fois qu'elle est cloneable, l'héritage
// ne supprime pas cette propriété :
class Int3 extends Int2 {
  private int j; // Automatiquement dupliqué
  public Int3(int i) { super(i); }
}

public class AddingClone {
  public static void main(String[] args) {
    Int2 x = new Int2(10);
    Int2 x2 = (Int2)x.clone();
    x2.increment();
    System.out.println(
      "x = " + x + ", x2 = " + x2);
    // Tout objet hérité est aussi cloneable :
    Int3 x3 = new Int3(7);
    x3 = (Int3)x3.clone();

    ArrayList v = new ArrayList();
    for(int i = 0; i < 10; i++ )
      v.add(new Int2(i));
    System.out.println("v: " + v);
    ArrayList v2 = (ArrayList)v.clone();
    // Maintenant on clone chaque élément :
    for(int i = 0; i < v.size(); i++)
      v2.set(i, ((Int2)v2.get(i)).clone());
    // Incrémente tous les éléments de v2 :
    for(Iterator e = v2.iterator();
        e.hasNext(); )
      ((Int2)e.next()).increment();
    // Vérifie si les éléments de v ont été modifiés :
    System.out.println("v: " + v);
    System.out.println("v2: " + v2);
  }
} ///:~

Int3 est dérivée de Int2 et un nouveau membre scalaire int j a été ajouté. On pourrait croire qu'il faut redéfinir clone() pour être sûr que j soit copié, mais ce n'est pas le cas. Lorsque la méthode clone() de Int2 est appelée à la place de la méthode clone() de Int3, elle appelle Object.clone(), qui détermine qu'elle travaille avec un Int3 et duplique tous les bits de Int3. Tant qu'on n'ajoute pas de références qui ont besoin d'être clonées, l'appel à Object.clone() réalise toutes les opérations nécessaires au clonage, sans se préoccuper de la profondeur hiérarchique où clone() a été définie.

Pour réaliser une copie profonde d'une ArrayList, il faut donc la cloner, puis la parcourir et cloner chacun des objets pointés par l'ArrayList. Un mécanisme similaire serait nécessaire pour réaliser une copie profonde d'un HashMap.

Le reste de l'exemple prouve que le clonage s'est bien passé en montrant qu'une fois cloné, un objet peut être modifié sans que l'objet original n'en soit affecté.

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