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 

Quand on examine la sérialisation d'objets dans Java (présentée au Chapitre 11), on se rend compte qu'un objet sérialisé puis désérialisé est, en fait, cloné.

Pourquoi alors ne pas utiliser la sérialisation pour réaliser une copie profonde ? Voici un exemple qui compare les deux approches en les chronométrant :

//: appendixa:Compete.java
import java.io.*;

class Thing1 implements Serializable {}
class Thing2 implements Serializable {
  Thing1 o1 = new Thing1();
}

class Thing3 implements Cloneable {
  public Object clone() {
    Object o = null;
    try {
      o = super.clone();
    } catch(CloneNotSupportedException e) {
      System.err.println("Thing3 can't clone");
    }
    return o;
  }
}

class Thing4 implements Cloneable {
  Thing3 o3 = new Thing3();
  public Object clone() {
    Thing4 o = null;
    try {
      o = (Thing4)super.clone();
    } catch(CloneNotSupportedException e) {
      System.err.println("Thing4 can't clone");
    }
    // Clone aussi la donnée membre :
    o.o3 = (Thing3)o3.clone();
    return o;
  }
}

public class Compete {
  static final int SIZE = 5000;
  public static void main(String[] args)
  throws Exception {
    Thing2[] a = new Thing2[SIZE];
    for(int i = 0; i < a.length; i++)
      a[i] = new Thing2();
    Thing4[] b = new Thing4[SIZE];
    for(int i = 0; i < b.length; i++)
      b[i] = new Thing4();
    long t1 = System.currentTimeMillis();
    ByteArrayOutputStream buf =
      new ByteArrayOutputStream();
    ObjectOutputStream o =      new ObjectOutputStream(buf);
    for(int i = 0; i < a.length; i++)
      o.writeObject(a[i]);
    // Récupère les copies:
    ObjectInputStream in =      new ObjectInputStream(
        new ByteArrayInputStream(
          buf.toByteArray()));
    Thing2[] c = new Thing2[SIZE];
    for(int i = 0; i < c.length; i++)
      c[i] = (Thing2)in.readObject();
    long t2 = System.currentTimeMillis();
    System.out.println(
      "Duplication via serialization: " +
      (t2 - t1) + " Milliseconds");
    // Maintenant on tente le clonage :
    t1 = System.currentTimeMillis();
    Thing4[] d = new Thing4[SIZE];
    for(int i = 0; i < d.length; i++)
      d[i] = (Thing4)b[i].clone();
    t2 = System.currentTimeMillis();
    System.out.println(
      "Duplication via cloning: " +
      (t2 - t1) + " Milliseconds");
  }
} ///:~

Thing2 et Thing4 contiennent des objets membres afin qu'une copie profonde soit nécessaire. Il est intéressant de noter que bien que les classes Serializable soient plus faciles à implémenter, elles nécessitent plus de travail pour les copier. Le support du clonage demande plus de travail pour créer la classe, mais la duplication des objets est relativement simple. Les résultats sont édifiants. Voici la sortie obtenue pour trois exécutions :

Duplication via serialization: 940 Milliseconds
Duplication via cloning: 50 Milliseconds

Duplication via serialization: 710 Milliseconds
Duplication via cloning: 60 Milliseconds

Duplication via serialization: 770 Milliseconds
Duplication via cloning: 50 Milliseconds

Outre la différence significative de temps entre la sérialisation et le clonage, vous noterez aussi que la sérialisation semble beaucoup plus sujette aux variations, tandis que le clonage a tendance à être plus stable.

Supporter le clonage plus bas dans la hiérarchie

Si une nouvelle classe est créée, sa classe de base par défaut est Object, qui par défaut n'est pas cloneable (comme vous le verrez dans la section suivante). Tant qu'on n'implémente pas explicitement le clonage, celui-ci ne sera pas disponible. Mais on peut le rajouter à n'importe quel niveau et la classe sera cloneable à partir de ce niveau dans la hiérarchie, comme ceci :

//: appendixa:HorrorFlick.java
// On peut implémenter le Clonage
// à n'importe quel niveau de la hiérarchie.
import java.util.*;

class Person {}
class Hero extends Person {}
class Scientist extends Person
    implements Cloneable {
  public Object clone() {
    try {
      return super.clone();
    } catch(CloneNotSupportedException e) {
      // Ceci ne devrait jamais arriver :
      // la classe est Cloneable !
      throw new InternalError();
    }
  }
}
class MadScientist extends Scientist {}

public class HorrorFlick {
  public static void main(String[] args) {
    Person p = new Person();
    Hero h = new Hero();
    Scientist s = new Scientist();
    MadScientist m = new MadScientist();

    // p = (Person)p.clone(); // Erreur lors de la compilation
    // h = (Hero)h.clone(); // Erreur lors de la compilation
    s = (Scientist)s.clone();
    m = (MadScientist)m.clone();
  }
} ///:~

Tant que le clonage n'est pas supporté, le compilateur bloque toute tentative de clonage. Si le clonage est ajouté dans la classe Scientist, alors Scientist et tous ses descendants sont cloneables.

Pourquoi cet étrange design ?

Si tout ceci vous semble étrange, c'est parce que ça l'est réellement. On peut se demander comment on en est arrivé là. Que se cache-t-il derrière cette conception ?

Originellement, Java a été conçu pour piloter des boîtiers, sans aucune pensée pour l'Internet. Dans un langage générique tel que celui-ci, il semblait sensé que le programmeur soit capable de cloner n'importe quel objet. C'est ainsi que clone() a été placée dans la classe de base Object, mais c'était une méthode public afin qu'un objet puisse toujours être cloné. Cela semblait l'approche la plus flexible, et après tout, quel mal y avait-il à cela ?

Puis, quand Java s'est révélé comme le langage de programmation idéal pour Internet, les choses ont changé. Subitement, des problèmes de sécurité sont apparus, et bien sûr, ces problèmes ont été réglés en utilisant des objets, et on ne voulait pas que n'importe qui soit capable de cloner ces objets de sécurité. Ce qu'on voit donc est une suite de patchs appliqués sur l'arrangement initialement simple : clone() est maintenant protected dans Object. Il faut la redéfinir et implementer Cloneableet traiter les exceptions.

Il est bon de noter qu'on n'est obligé d'utiliser l'interface Cloneable que si on fait un appel à la méthode clone() de Object, puisque cette méthode vérifie lors de l'exécution que la classe implémente Cloneable. Mais dans un souci de cohérence (et puisque de toute façon Cloneable est vide), il vaut mieux l'implémenter.

Contrôler la clonabilité

On pourrait penser que, pour supprimer le support du clonage, il suffit de rendre private la méthode clone(), mais ceci ne marchera pas car on ne peut prendre une méthode de la classe de base et la rendre moins accessible dans une classe dérivée. Ce n'est donc pas si simple. Et pourtant, il est essentiel d'être capable de contrôler si un objet peut être cloné ou non. Une classe peut adopter plusieurs attitudes à ce propos :

  1. L'indifférence. Rien n'est fait pour supporter le clonage, ce qui signifie que la classe ne peut être clonée, mais qu'une classe dérivée peut implémenter le clonage si elle veut. Ceci ne fonctionne que si Object.clone() traite comme il faut tous les champs de la classe.
  2. Implémenter clone(). Respecter la marche à suivre pour l'implémentation de Cloneable et redéfinir clone(). Dans la méthode clone() redéfinie, appeler super.clone() et intercepter toutes les exceptions (afin que la méthode clone() redéfinie ne génère pas d'exceptions).
  3. Supporter le clonage conditionnellement. Si la classe contient des références sur d'autres objets qui peuvent ou non être cloneables (une classe conteneur, par exemple), la méthode clone() peut essayer de cloner tous les objets référencés, et s'ils génèrent des exceptions, relayer ces exceptions au programmeur. Par exemple, prenons le cas d'une sorte d'ArrayList qui essayerait de cloner tous les objets qu'elle contient. Quand on écrit une telle ArrayList, on ne peut savoir quelle sorte d'objets le programmeur client va pouvoir stocker dans l'ArrayList, on ne sait donc pas s'ils peuvent être clonés.
  4. Ne pas implémenter Cloneable mais redéfinir clone() en protected, en s'assurant du fonctionnement correct du clonage pour chacun des champs. De cette manière, toute classe dérivée peut redéfinir clone() et appeler super.clone() pour obtenir le comportement attendu lors du clonage. Cette implémentation peut et doit invoquer super.clone() même si cette méthode attend un objet Cloneable (elle génère une exception sinon), car personne ne l'invoquera directement sur un objet de la classe. Elle ne sera invoquée qu'à travers une classe dérivée, qui, elle, implémente Cloneable si elle veut obtenir le fonctionnement désiré.
  5. Tenter de bloquer le clonage en n'implémentant pas Cloneable et en redéfinissant clone() afin de générer une exception. Ceci ne fonctionne que si toutes les classes dérivées appellent super.clone() dans leur redéfinition de clone(). Autrement, un programmeur est capable de contourner ce mécanisme.
  6. Empêcher le clonage en rendant la classe final. Si clone() n'a pas été redéfinie par l'une des classes parentes, alors elle ne peut plus l'être. Si elle a déjà été redéfinie, la redéfinir à nouveau et générer une exception CloneNotSupportedException. Rendre la classe final est la seule façon d'interdire catégoriquement le clonage. De plus, si on manipule des objets de sécurité ou dans d'autres situations dans lesquelles on veut contrôler le nombre d'objets créés, il faut rendre tous les constructeurs private et fournir une ou plusieurs méthodes spéciales pour créer les objets. De cette manière, les méthodes peuvent restreindre le nombre d'objets créés et les conditions dans lesquelles ils sont créés (un cas particulier en est le patron singleton présenté dans Thinking in Patterns with Java, téléchargeable à www.BruceEckel.com).

Voici un exemple qui montre les différentes façons dont le clonage peut être implémenté et interdit plus bas dans la hiérarchie :

//: appendixa:CheckCloneable.java
// Vérifie si une référence peut être clonée.

// Ne peut être clonée car ne redéfinit pas clone() :
class Ordinary {}

// Redéfinit clone, mais n'implémente pas
// Cloneable :
class WrongClone extends Ordinary {
  public Object clone()
      throws CloneNotSupportedException {
    return super.clone(); // Génère une exception
  }
}

// Fait le nécessaire pour le clonage :
class IsCloneable extends Ordinary
    implements Cloneable {
  public Object clone()
      throws CloneNotSupportedException {
    return super.clone();
  }
}

// Interdit le clonage en générant une exception :
class NoMore extends IsCloneable {
  public Object clone()
      throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
  }
}

class TryMore extends NoMore {
  public Object clone()
      throws CloneNotSupportedException {
    // Appelle NoMore.clone(), génère une excpetion :
    return super.clone();
  }
}

class BackOn extends NoMore {
  private BackOn duplicate(BackOn b) {
    // Crée une copie de b d'une façon ou d'une autre
    // et renvoie cette copie. C'est une copie sans
    // intérêt, juste pour l'exemple :
    return new BackOn();
  }
  public Object clone() {
    // N'appelle pas NoMore.clone() :
    return duplicate(this);
  }
}

// On ne peut dériver cette classe, donc on ne peut
// redéfinir la méthode clone comme dans BackOn:
final class ReallyNoMore extends NoMore {}

public class CheckCloneable {
  static Ordinary tryToClone(Ordinary ord) {
    String id = ord.getClass().getName();
    Ordinary x = null;
    if(ord instanceof Cloneable) {
      try {
        System.out.println("Attempting " + id);
        x = (Ordinary)((IsCloneable)ord).clone();
        System.out.println("Cloned " + id);
      } catch(CloneNotSupportedException e) {
        System.err.println("Could not clone "+id);
      }
    }
    return x;
  }
  public static void main(String[] args) {
    // Transtypage ascendant :
    Ordinary[] ord = {
      new IsCloneable(),
      new WrongClone(),
      new NoMore(),
      new TryMore(),
      new BackOn(),
      new ReallyNoMore(),
    };
    Ordinary x = new Ordinary();
    // Ceci ne compilera pas, puisque clone()
    // est protected dans Object:
    //! x = (Ordinary)x.clone();
    // tryToClone() vérifie d'abord si
    // une classe implémente Cloneable :
    for(int i = 0; i < ord.length; i++)
      tryToClone(ord[i]);
  }
} ///:~

La première classe, Ordinary, représente le genre de classes que nous avons rencontré tout au long de ce livre : pas de support du clonage, mais pas de contrôle sur la clonabilité non plus. Mais si on dispose d'une référence sur un objet Ordinary qui peut avoir été transtypé à partir d'une classe dérivée, on ne peut savoir s'il est peut être cloné ou non.

La classe WrongClone montre une implémentation incorrecte du clonage. Elle redéfinit bien Object.clone() et rend la méthode public, mais elle n'implémente pas Cloneable, donc quand super.clone() est appelée (ce qui revient à un appel à Object.clone()), une exception CloneNotSupportedException est générée et le clonage échoue.

La classe IsCloneable effectue toutes les actions nécessaires au clonage : clone() est redéfinie et Cloneable implémentée. Cependant, cette méthode clone() et plusieurs autres qui suivent dans cet exemple n'interceptent pasCloneNotSupportedException, mais la font suivre à l'appelant, qui doit alors l'envelopper dans un bloc try-catch. Dans les méthodes clone() typiques il faut intercepter CloneNotSupportedException à l'intérieur de clone() plutôt que de la propager. Cependant dans cet exemple, il est plus intéressant de propager les exceptions.

La classe NoMore tente d'interdire le clonage comme les concepteurs de Java pensaient le faire : en générant une exception CloneNotSupportedException dans la méthode clone() de la classe dérivée. La méthode clone() de la classe TryMore appelle super.clone(), ce qui revient à appeler NoMore.clone(), qui génère une exception et empêche donc le clonage.

Mais que se passe-t-il si le programmeur ne respecte pas la chaîne d'appel « recommandée » et n'appelle pas super.clone() à l'intérieur de la méthode clone() redéfinie ? C'est ce qui se passe dans la classe BackOn. Cette classe utilise une méthode séparée duplicate() pour créer une copie de l'objet courant et appelle cette méthode dans clone()au lieu d'appeler super.clone(). L'exception n'est donc jamais générée et la nouvelle classe est cloneable. La seule solution vraiment sûre est montrée dans ReallyNoMore, qui est final et ne peut donc être dérivée. Ce qui signifie que si clone() génère une exception dans la classe final, elle ne peut être modifiée via l'héritage et la prévention du clonage est assurée (on ne peut appeler explicitement Object.clone() depuis une classe qui a un niveau arbitraire d'héritage ; on en est limité à appeler super.clone(), qui a seulement accès à sa classe parente directe). Implémenter des objets qui traite de sujets relatifs à la sécurité implique donc de rendre ces classes final.

La première méthode qu'on voit dans la classe CheckCloneable est tryToClone(), qui prend n'importe quel objet Ordinary et vérifie s'il est cloneable grâce à instanceof. Si c'est le cas, il transtype l'objet en IsCloneable, appelle clone() et retranstype le résultat en Ordinary, interceptant toutes les exceptions générées. Remarquez l'utilisation de l'identification dynamique du type (voir Chapitre 12) pour imprimer le nom de la classe afin de suivre le déroulement du programme.

Dans main(), différents types d'objets Ordinary sont créés et transtypés en Ordinary dans la définition du tableau. Les deux premières lignes de code qui suivent créent un objet Ordinary et tentent de le cloner. Cependant ce code ne compile pas car clone() est une méthode protected dans Object. Le reste du code parcourt le tableau et essaye de cloner chaque objet, reportant le succès ou l'échec de l'opération. Le résultat est :

Attempting IsCloneable
Cloned IsCloneable
Attempting NoMore
Could not clone NoMore
Attempting TryMore
Could not clone TryMore
Attempting BackOn
Cloned BackOn
Attempting ReallyNoMore
Could not clone ReallyNoMore

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