Clonage des objets

Création de nouvelles instances à partir d'une instance existante en Java

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com

Le surchargement des classes, le clonage et autres possibilités offertes par Java peuvent parfois poser quelques soucis aux développeurs débutants. Voici les points auxquels ils devraient être vigilants.

Une alternative à clone en Java

Une solution alternative à l'implémentation de l'interface Cloneable peut être l'utilisation de copy constructors. Voici un exemple de l'implémentation du copy constructor :

public class Overriding { 
    public static void main(String[] args) {
        Dog d = new Dog("a", 4);
        Dog newDog = new Dog(d);
        System.out.println("Found two dogs : " + d.toString() + " and " + newDog.toString());
    }
}

class Dog {
    private String name;
    private int hands;

    public Dog(String n, int h) {
        name = n;
        hands = h;
    }

    public Dog(Dog dogInstance) {
        name = dogInstance.getName();
        hands = dogInstance.getHands();
    }

    public String getName() {
        return name;
    }
    
    public int getHands() {
        return hands;
    }

    public String toString() {
        return "Dog instance with params : name("+name+"), hands("+hands+")";
    }
}

Le résultat affiche :

Found two dogs : Dog instance with params : name(a), hands(4) and Dog instance with params : name(a), hands(4)

Comme on peut le constater, copy constructor est un type de constructeur qui initialise un objet depuis un autre objet de la même classe. Ce constructeur protège également l'objet intitial d'être accidentellement modifié.

Cependant, copy constructor ne paraît pas être une solution idéale pour la copie des objets. Les classes l'employant peuvent être confrontées à des problématiques liées à des sous-classes copiées ainsi qu'aux attributs finaux. Illustrons ceci sur cet exemple :

import java.util.List;
import java.util.ArrayList;

public class CopyConstructorProblems { 
    public static void main(String[] args) {
        Dog d = new Dog(new BadOwner("bad owner"), "a", 4, "beta");
        Dog newDog = new Dog(d);
        d.owners.add("teta");
        newDog.owners.add("eta");
        System.out.println("Found two dogs : " + d.toString() + " and " + newDog.toString());
    }
}

class Dog {
    public final List&t;String> owners;
    private String name;
    private int hands;
    private Owner owner;
  
    public Dog(Owner ow, String n, int h, final String o) {
        owner = ow;
        name = n;
        hands = h;
        owners = new ArrayList<String>(){{
          add(o);
        }};
    }

    public Dog(Dog dogInstance) {
        name = dogInstance.getName();
        hands = dogInstance.getHands();
        owner = new Owner(dogInstance.getOwner());
        owners = (ArrayList)dogInstance.getOwners();
        // owners = new ArrayList(dogInstance.getOwners());
    }

    public String getName() {
      return name;
    }

    public Owner getOwner() {
        return owner;
    }

    public int getHands() {
        return hands;
    }  
  
    public List getOwners() {
        return owners;
    }

    public String toString() {
        return "Dog instance with params : name("+name+"), hands("+hands+"), owners("+owners.toString()+"), owner object instance ("+owner.toString()+")";
    }
}

class Owner {
    private String name;
    public Owner() {
  
    }
  
    public Owner(String n) {
        name = n;
      }
  
    public Owner(Owner o) {
        name = o.getName();
    }
  
    public String getName() {
        return name;
      }
}

class BadOwner extends Owner {
    private String name;
  
    public BadOwner(String n) {
        name = n;
      }
}

Regardons maintenant un résultat surprenant :

Found two dogs : Dog instance with params : name(a), hands(4), owners([beta, teta, eta]), owner object instance (BadOwner@190d11) and Dog instance with params : name(a), hands(4), owners([beta, teta, eta]), owner object instance (Owner@a90653)

Le premier effet pervers est le fait que les deux objects Dog ont les mêmes valeurs dans la liste owners. Le deuxième problème se trouve dans l'instance passé au constructeur. Elle ne représente pas la même classe dans les deux cas. Comment éviter ces problèmes ? Pour le premier il suffit d'initialiser une nouvelle liste. Pour le second il faut implémenter le clonage de l'interface Cloneable. Comme dans cet exemple :

import java.util.List;
import java.util.ArrayList;

public class CopyConstructorProblems { 
    public static void main(String[] args) {
        Dog d = new Dog(new BadOwner("bad owner"), "a", 4, "beta");
        Dog newDog = new Dog(d);
        d.owners.add("teta");
        newDog.owners.add("eta");
      System.out.println("Found two dogs : " + d.toString() + " and " + newDog.toString());
    }
}

class Dog {
    public final List<String> owners;
    private String name;
    private int hands;
    private Owner owner;
    public Dog(Owner ow, String n, int h, final String o) {
        owner = ow;
        name = n;
        hands = h;
        owners = new ArrayList<String>(){{
          add(o);
        }};
    }
  
    public Dog(Dog dogInstance) {
        name = dogInstance.getName();
        hands = dogInstance.getHands();
        try {
          owner = (Owner) (dogInstance.getOwner()).clone();
        } catch (CloneNotSupportedException e) {
          System.out.println("A CloneNotSupportedException was caught " + e.getMessage());
        }
        // owners = (ArrayList)dogInstance.getOwners();
        owners = new ArrayList<String>(dogInstance.getOwners());
    }
    
    public String getName() {
        return name;
    }

    public Owner getOwner() {
        return owner;
    }
  
    public int getHands() {
      return hands;
    }
  
    public List getOwners() {
        return owners;
    }
  
    public String toString() {
        return "Dog instance with params : name("+name+"), hands("+hands+"), owners("+owners.toString()+"), owner object instance ("+owner.toString()+")";
    }
}

class Owner implements Cloneable {
    private String name;
    public Owner() {
  
    }
  
    public Owner(String n) {
        name = n;
    }
  
    public Owner(Owner o) {
        name = o.getName();
      }
  
    public String getName() {
        return name;
    }
  
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

class BadOwner extends Owner {
    private String name;
  
    public BadOwner(String n) {
        name = n;
    }
  
    public Object clone() throws CloneNotSupportedException {
        BadOwner owner = (BadOwner)super.clone();
        return owner;
    }
}

Et voici le résultat correct après les changements :

Found two dogs : Dog instance with params : name(a), hands(4), owners([beta, teta]), owner object instance (BadOwner@190d11) and Dog instance with params : name(a), hands(4), owners([beta, eta]), owner object instance (BadOwner@a90653)
+++ Décrire les problèmes potentiels avec les deux solutions - surtout les problèmes avec les classes héritantes des autres : http://www.xenoveritas.org/blog/xeno/java_copy_constructors_and_clone http://www.agiledeveloper.com/articles/cloning072002.htm http://www.programmerinterview.com/index.php/java-questions/how-copy-constructors-work/ http://www.javapractices.com/topic/TopicAction.do;jsessionid=0FEDF246D5EE473A481B2DCB2EDDCDA0?Id=71

Régles pour implémenter compareTo() en Java

L'utilisation de la méthode compareTo(), comprise dans l'interface Comparable sera sûrement utile lors de toute sorte de comparaison des objets. Cependant, pour l'implémenter correctement il faut avoir en tête quelques pré-requis :
- il doit accepter anti-interconversion; autrement dit le résultat du x.compareTo(y) doit être l'opposédu y.compareTo(x)
- il doit capter les mêmes exceptions dans les deux objets comparés
- il est conseillé de garder la cohérence avec la méthode equals() utilisée par exemple par les Set

Regardons l'implémentation de la méthode compareTo sur un exemple. Cette méthode doit retourner : un entier négatif (si l'objet comparé est inférieur), un zéro (si les deux objets sont égaux) ou un entier positif (si l'objet comparé est supérieur). Voici le cas de figure qui va le montrer :

public class CompareTo { 
    public static void main(String[] args) {
        Dog dog1 = new Dog("a", 4);
        Dog dog2 = new Dog("b", 3);
          System.out.println("Comparing first and second dog : " + dog1.compareTo(dog2));
        System.out.println("Comparing second and first dog : " + dog2.compareTo(dog1));
      }
}

class Dog implements Comparable<Dog> {
    private String name;
    private int hands;

    public Dog(String n, int h) {
        name = n;
        hands = h;
    }
  
    public Dog(Dog dogInstance) {
        name = dogInstance.getName();
        hands = dogInstance.getHands();
    }
  
    public String getName() {
        return name;
    }

    public int getHands() {
        return hands;
    }

    public int compareTo(Dog dog) {
        if (this.hands > dog.hands) return 1;
        else if (this.hands == dog.hands) return 0;
        else if (this.hands < dog.hands) return -1;
        return 0;
    }

    public String toString() {
        return "Dog instance with params : name("+name+"), hands("+hands+")";
    }
}

Voici le résultat affiché sur l'écran après l'exécution :

Comparing first and second dog : 1
Comparing second and first dog : -1
http://www.javapractices.com/topic/TopicAction.do;jsessionid=0FEDF246D5EE473A481B2DCB2EDDCDA0?Id=10 http://docs.oracle.com/javase/6/docs/api/java/lang/Comparable.html

Dépendance entre equals() et hashCode()

Si l'on ne souhaite pas implémenter l'interface Cloneable, on peut se contenter de faire appel à des méthodes fournies avec la classe Objet. Il s'agit du hashCode() et equals(). Dans l'implémentation de ces deux méthodes il faut se souvenir des 3 régles :
- une surcharge de la méthode equals() amène avec elle la surcharge de la méthode hashCode
- hashCode() doit générer les mêmes valeurs pour les mêmes objets
- equals() et hashCode() doivent utiliser les mêmes attributs pour effectuer l'opération de comparaison

Regardons comment respecter ces régles sur un cas précis :

public class Equals { 
    public static void main(String[] args) {
        Dog dog1 = new Dog("a", 4);
        Dog dog2 = new Dog("b", 3);
        Dog dog3 = new Dog(dog2);
        System.out.println("Comparing first and second dog : " + dog1.equals(dog2));
        System.out.println("Comparing second and first dog : " + dog2.equals(dog1));
        System.out.println("Comparing second and third dog : " + dog2.equals(dog3));
        System.out.println("Comparing third and second dog : " + dog3.equals(dog2));
    }
}

class Dog implements Comparable<Dog> {
    private String name;
    private int hands;
    private int hCode = -1;

    public Dog(String n, int h) {
        name = n;
        hands = h;
    }

    public Dog(Dog dogInstance) {
        name = dogInstance.getName();
      hands = dogInstance.getHands();
    }
  
    public String getName() {
        return name;
    }
  
    public int getHands() {
      return hands;
    }
  
    public int compareTo(Dog dog) {
        if (this.hands > dog.hands) return 1;
        else if (this.hands == dog.hands) return 0;
        else if (this.hands < dog.hands) return -1;
      return 0;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        Dog dog = (Dog)o;
        Object[] thisValues = getObjectsToCompare();
        Object[] thatValues = dog.getObjectsToCompare();
        boolean result = true;
        for (int i = 0; i < thisValues.length; i++) {
          if (!Comparator.compareTwo(thisValues[i], thatValues[i])) {
            return false;
          }
        }
        return result;
    }
  
    @Override
    public int hashCode() {
        if (hCode == -1) hCode = getHashCode();
        return hCode;
    }

    public Object[] getObjectsToCompare() {
        Object[] o = {name, hands};
        return o;
    }

    private int getHashCode() {
        Object[] elements = getObjectsToCompare();
        int f = 0;
        for (Object element : elements) {
            f += Hasher.hash(element);
        }
        System.out.println("Generated " + f);
        return f;
    }
  
    public String toString() {
        return "Dog instance with params : name("+name+"), hands("+hands+")";
    }
}

class Hasher {
    public static int hash(Object v) {
        return 0;
      }
    
    public int hash(String v) {
        return v.length();
    }
  
    public int hash(int v) {
        return v;
    }
}

class Comparator {
    public static boolean compareTwo(String a, String b) {
        return a.equals(b);
      }
  
    public static boolean compareTwo(int a, int b) {
        return a == b;
      }
  
    public static boolean compareTwo(Object a, Object b) {
        return (a != null && b != null && a.equals(b));
    }
}

Et le résultat sur l'écran :

Comparing first and second dog : false
Comparing second and first dog : false
Comparing second and third dog : true
Comparing third and second dog : true
Bartosz KONIECZNY Informations sur le langage

Une question ? Une remarque ?

*

*

Un conseil Symfony1

Comment récupérer un paramètre de la requête dans le template ?

Il suffit d'utiliser la méthode get() de l'objet $sf_params, par exemple: $sf_params->get('nom_du_parametre').

Comme c'est déjà le cas dans les actions, on peut également préciser la valeur par défaut, au cas où le paramètre n'existe pas. Exemple de l'utilisation: $sf_params->get('nom_du_parametre', 'valeur par défaut');