Object
.
La documentation de référence se trouve sur le javadoc de la classe
Object
.
public class Object { public Object() public String toString() public final native Class getClass() protected native Object clone() throws CloneNotSupportedException public equals(java.lang.Object) public native int hashCode() // méthodes liées à la gestion des threads public final native void notify() public final native void notifyAll() public final void wait(long) throws InterruptedException public final void wait(long, int) throws InterruptedException }
Constructeur
Ce constructeur est appelé à chaque création d'objet, voir la page "Classes, interfaces", paragraphe "Chaînage d'appel".toString()
Permet d'obtenir une représentation textuelle de l'objet.Comportement par défaut
Par défaut, cette chaîne est constituée du type de l'objet, suivi de "@", suivi du hashcode (en hexadécimal) de l'objet ; égale à :getClass().getName() + '@' + Integer.toHexString(hashCode())Par exemple
Personne@7bfcd12c
.
A noter :
- Cette chaîne identifie un objet de manière unique.
-
System.out.println
utilisetoString()
pour afficher les objets, avec une petite différence de comportement si l'objet vautnull
, comme l'illustre le code suivant :
class Personne { private String nom, prenom; private int anneeNaissance; public Personne(String nom, String prenom, int anneeNaissance){ this.nom = nom; this.prenom = prenom; this.anneeNaissance = anneeNaissance; } } class Mathematicien extends Personne { public Mathematicien(String nom, String prenom, int anneeNaissance){ super(nom, prenom, anneeNaissance); } } public class ToString1 { public static void main(String[] args) { Personne p1 = new Personne("Curry", "Haskell", 1900); System.out.println("p1 = " + p1); Personne p2 = new Mathematicien("Curry", "Haskell", 1900); System.out.println("p2 = " + p2); Personne p3 = (Personne)p2; System.out.println("p3 = " + p3); Personne p4 = null; // Lorsqu'on utilise l'opérateur de concaténation de chaîne ou println(), // on obtient la chaîne "null" System.out.println("p4 = " + p4); // p4 = null String affichage = "affichage : p4 = " + p4; System.out.println(affichage); // affichage : p4 = null System.out.println(p4); // null // Mais lorsqu'on appelle une méthode sur un objet null, on obtient une exception. System.out.println("p4.toString() = " + p4.toString()); // java.lang.NullPointerException } }(code dans ToString1.java)
java ToString1
p1 = Personne@7344699f p2 = Mathematicien@7e9e5f8a p3 = Mathematicien@7e9e5f8a p4 = null affichage : p4 = null null Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Object.toString()" because "" is null at ToString1.main(ToString1.java:39)
Overriding
L'implémentation par défaut n'étant pas très parlante, elle est en général réimplémentée dans les sous-classes, comme conseillé par la documentation :It is recommended that all subclasses override this method.Suivant les besoins, on est régulèrement amené à overrider cette méthode.
Par exemple, pour la classe
Personne
:
@Override public String toString(){ return prenom + " " + nom + " (" + String.valueOf(anneeNaissance) + ")"; }(code dans ToString2.java)
java ToString2
p1 = Haskell Curry (1900)
getClass()
Renvoie un objet de la classejava.lang.Class
, qui représente la "runtime class" de l'objet (son type constaté, pas son type déclaré).
Personne p1 = new Personne("Curry", "Haskell", 1900); System.out.println("p1.getClass() = " + p1.getClass()); Personne p2 = new Mathematicien("Curry", "Haskell", 1900); System.out.println("p2.getClass() = " + p2.getClass()); Personne p3 = (Personne)p2; System.out.println("p3.getClass() = " + p3.getClass());(code dans GetClass.java)
java GetClass
p1.getClass() = class Personne p2.getClass() = class Mathematicien p3.getClass() = class MathematicienVoir la page Reflection pour plus de détails
clone()
Le but est d'obtenir une copie exacte de l'objet, donc de même classe, ayant le même état (les mêmes valeurs des membres) et indépendant de l'objet original.En java, cela est fait en général en overridant
Object.clone()
, mais il existe d'autres manières de faire :
-
Ecrire un constructeur par recopie, comme par exemple le constructeur de
java.lang.String
public String(String original)
- Utilisation de la sérialization (ce qui peut être "cher").
Implémenter clone()
La signature deObject.clone()
est
protected native Object clone() throws CloneNotSupportedExceptionPour pouvoir être clonée, une classe doit implémenter l'interface
Cloneable
. Cloneable
est une marker interface, c'est à dire une interface vide, qui n'est là que pour signaler que la classe peut être clonée.
Le plus pratique pour du code utilisateur est de changer la signature de la méthode. Par exemple, pour la classe
Personne
:
class Personne implements Cloneable{ // ... @Override public Personne clone() { Personne p = null; try{ p = (Personne)super.clone(); } finally { return p; } } }On a à la fois
- rendu la méthode
public
, - modifié son type de retour,
- absorbé l'exception.
Shallow vs deep copy
L'implémentation deObject.clone()
effectue une shallow copy de l'objet : elle recopie les membres de l'objet.
Pour les types primitifs ou les objets immuables, cela implique l'indépendance de l'objet et de son clone.
Mais pour les types références mutables, l'objet original et son clone vont partager les mêmes membres.
Les deux objets ne seront donc pas indépendants.
On peut voir cette différence dans le code suivant :
public class Clone1 { public static void main(String[] args) { try{ Personne p1 = new Personne("Curry", "Haskell", 1900, new Pays("Etats-Unis")); // note : on utilise l'implémentation de Personne.clone() présentée ci-dessus. // cette implémentation utilise en interne l'implémentation par défaut // donc va reproduire les problèmes liés à l'implémentation par défaut. Personne p2 = p1.clone(); System.out.println("======== p1 et p2 = p1.clone() ========"); System.out.println("p1 = " + p1); System.out.println("p2 = " + p2); System.out.println("p1.getClass() = " + p1.getClass()); System.out.println("p2.getClass() = " + p2.getClass()); System.out.println("\n======== Modification de p1.nom et p1.annee ========"); p1.setNom("Curry modifié"); p1.setAnnee(2000); System.out.println("p1 = " + p1); // Haskell Curry modifié (2000) Etats-Unis System.out.println("p2 = " + p2); // Haskell Curry (1900) Etats-Unis // On voit que pour des champs simples, l'implémentation par défaut de clone() // a bien recopié, on a l'impression que p1 et p2 sont indépendants. System.out.println("\n======== Modification de p1.pays ========"); p1.getPays().setNom("Etats-Unis modifié"); System.out.println("p1 = " + p1); // Haskell Curry modifié (2000) Etats-Unis modifié System.out.println("p2 = " + p2); // Haskell Curry (1900) Etats-Unis modifié // Pour un champ composé (le pays), l'implémentation par défaut de clone() // a seulement recopié la référence ; on constate que p1 et p2 ne sont pas indépendants } catch(Exception e){ e.printStackTrace(); } } } class Pays{ private String nom; public Pays(String nom){ this.nom = nom; } public void setNom(String nom) { this.nom = nom; } public String getNom() { return nom; } } class Personne implements Cloneable{ private String nom, prenom; private int annee; private Pays pays; public Personne(String nom, String prenom, int annee, Pays pays){ this.nom = nom; this.prenom = prenom; this.annee = annee; this.pays = pays; } public void setNom(String nom) { this.nom = nom; } public void setPrenom(String prenom) { this.prenom = prenom; } public void setAnnee(int annee) { this.annee = annee; } public void setPays(Pays pays) { this.pays = pays; } public Pays getPays() { return pays; } @Override public String toString(){ return prenom + " " + nom + " (" + String.valueOf(annee) + ") " + pays.getNom(); } // Implémentation non satisfaisante car le champ pays est mutable. @Override public Personne clone() { Personne p = null; try{ p = (Personne)super.clone(); } finally { return p; } } }(code dans Clone1.java)
java Clone1
======== p1 et p2 = p1.clone() ======== p1 = Haskell Curry (1900) Etats-Unis p2 = Haskell Curry (1900) Etats-Unis p1.getClass() = class Personne p2.getClass() = class Personne ======== Modification de p1.nom et p1.annee ======== p1 = Haskell Curry modifié (2000) Etats-Unis p2 = Haskell Curry (1900) Etats-Unis ======== Modification de p1.pays ======== p1 = Haskell Curry modifié (2000) Etats-Unis modifié p2 = Haskell Curry (1900) Etats-Unis modifiéLorsqu'on fait
clone()
:
-
p1.nom
et p2.nom
pointent vers la même zone mémoire.
-
p1.prenom
et p2.prenom
pointent vers la même zone mémoire.
-
p1.annee
et p2.annee
contiennent deux valeurs identiques dans deux zones mémoires différentes (type primitif).
Lorsqu'on modifie
p1.annee
, la valeur est modifiée sur p1
mais pas sur p2
car les deux sont indépendantes.
Lorsqu'on modifie
p1.nom
, une nouvelle chaîne est créée (parceque nom
est de type String
, qui est immuable), et p1.nom
va pointer vers cette nouvelle chaîne, laissant p2.nom
inchangé.
Mais lorsqu'on modifie
p1.pays
, comme Pays
est mutable et que p1.pays
et p2.pays
pointent vers le même objet, la modification apparaît à la fois dans p1
et p2
.
On voit donc que les deux objets ne sont pas indépendants.
Pour rendre vraiment indépendants ces deux objets, il faut faire une deep copy, c'est à dire créer un nouveau pays dans la méthode
clone()
Par exemple :
class Personne implements Cloneable{ // ... @Override public Personne clone() { Personne p = null; try{ p = (Personne)super.clone(); p.pays = new Pays(pays.getNom()); } finally { return p; } } }(code dans Clone2.java)
java Clone2
# ... Même affichage que dans Clone1 ======== Modification de p1.pays ======== p1 = Haskell Curry modifié (2000) Etats-Unis modifié p2 = Haskell Curry (1900) Etats-Unis
equals(), hashCode()
Pour les types références, l'opérateur==
teste l'égalité des références, renvoie donc true
si deux objets pointent vers le même zone mémoire.
L'implémentation de
Object.equals()
ne fait rien de plus, comme on peut le voir dans cet extrait du code source de java.lang.Object
(open JDK).
public boolean equals(Object obj) { return (this == obj); }Donc dans un cas général, ni
equals()
ni l'opérateur ==
ne donnent un résultat intuitif.
public class Equals1 { public static void main(String[] args) { Classe o1 = new Classe("o1"); Classe o2 = o1; System.out.println("o1 == o2 ? " + (o1 == o2)); System.out.println("o1.equals(o2) ? " + (o1.equals(o2))); Classe o3 = new Classe("o1"); System.out.println("\no1 == o3 ? " + (o1 == o3)); System.out.println("o1.equals(o3) ? " + (o1.equals(o3))); } } class Classe{ private String nom; public Classe(String nom){ this.nom = nom; } }(code dans Equals1.java)
java Equals1
o1 == o2 ? true o1.equals(o2) ? true o1 == o3 ? false o1.equals(o3) ? falseOn s'attend à ce que
o1
et o3
soient égaux ; ce n'est pas le cas car ces deux symboles pointent vers deux adresses mémoire différentes.
Pour obtenir un comportement "correct", il faut donc overrider
equal()
.
public class Equals2 { public static void main(String[] args) { Classe o1 = new Classe("o1"); Classe o2 = o1; System.out.println("o1 == o2 ? " + (o1 == o2)); System.out.println("o1.equals(o2) ? " + (o1.equals(o2))); Classe o3 = new Classe("o1"); System.out.println("\no1 == o3 ? " + (o1 == o3)); System.out.println("o1.equals(o3) ? " + (o1.equals(o3))); } } class Classe{ private String nom; public Classe(String nom){ this.nom = nom; } public String getNom() { return nom; } @Override public boolean equals(Object obj){ if(obj.getClass() != this.getClass()){ return false; } return ((Classe)obj).getNom() == nom; } }(code dans Equals2.java)
java Equals2
o1 == o2 ? true o1.equals(o2) ? true o1 == o3 ? false o1.equals(o3) ? true
Cas particulier de String
Attention,String
a parfois un comportement différent, voir la page Strings, paragraphe "Comparaison de chaînes".
Importance dans les collections
equals()
est utilisé par certaines fonctions de l'API Collection
, en particulier contains()
et remove()
.
Liens entre hashCode() et equals()
La documentation deequals()
indique :
Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.En effet, la documentation de
hashCode()
indique :
- Si deux objets sont égaux au sens de
equals()
, alors ils doivent renvoyer le même hash code. - Mais deux objets différents au sens de
equals()
ne doivent pas obligatoirement renvoyer deux hash codes différents (bien que ce soit conseillé).
public class HashCode1 { public static void main(String[] args) { Personne p1 = new Personne("Curry", "Haskell", 1900); System.out.println("p1.hashCode() = " + p1.hashCode()); Personne p2 = new Personne("Haskell", "Curry", 1900); System.out.println("p2.hashCode() = " + p2.hashCode()); Personne p3 = new Personne("Hilbert", "David", 1862); System.out.println("p3.hashCode() = " + p3.hashCode()); } } class Personne{ private String nom, prenom; private int anneeNaissance; public Personne(String nom, String prenom, int anneeNaissance){ this.nom = nom; this.prenom = prenom; this.anneeNaissance = anneeNaissance; } @Override public int hashCode() { return nom.hashCode() * prenom.hashCode() * anneeNaissance; } }(code dans HashCode1.java)
java Hashcode1
p1.hashCode() = 869721104 p2.hashCode() = 869721104 p3.hashCode() = 1761653376Les règles 1 et 2 sont respectées ; on voit bien qu'il peut exister deux personnes différentes ayant même hashCode (entre p1 et p2, on a permuté nom et prénom).
Si on a une classe ayant un id unique (cas fréquent de classes représentant des objets stockés en base dont l'id est le clé primaire), l'implémentation est plus simple :
public class HashCode2 { public static void main(String[] args) { Personne p1 = new Personne(1, "Curry", "Haskell", 1900); System.out.println("p1.hashCode() = " + p1.hashCode()); Personne p2 = new Personne(1, "Curry", "Haskell", 1900); System.out.println("p2.hashCode() = " + p2.hashCode()); Personne p3 = new Personne(2, "Hilbert", "David", 1862); System.out.println("p3.hashCode() = " + p3.hashCode()); } } class Personne{ private int id; // unique id private String nom, prenom; private int anneeNaissance; public Personne(int id, String nom, String prenom, int anneeNaissance){ this.id = id; this.nom = nom; this.prenom = prenom; this.anneeNaissance = anneeNaissance; } @Override public int hashCode() { return id; } }Les deux règles sont respectées, et on a en plus : si deux personnes ont un hash code différent, alors elles sont différentes.
Dans le cas de
HashCode2
, l'implémentation de equals()
pourrait utiliser hashCode()
:
@Override public boolean equals(Object o) { if(!o.getClass().getName().equals(this.getClass().getName())){ return false; } return o.hashCode() == hashCode(); // ou encore : // return ((Personne)o).id == id; }Mais ce n'est pas un cas général, voir l'exemple précédent
HashCode1
.
Importance de hashCode()
hashCode()
est utilisée dans les maps (voir page Collections), lorsque l'objet sert de clé. equals()
et hashCode()
sont utilisés pour stocker / retrouver les objets d'une map.