java.lang.Object

C'est la seule classe java qui n'a pas de superclasse ; ancêtre commun à toutes les autres classes, qui héritent donc des méthodes de 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 :
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;
        System.out.println("p4 = " + p4);
        System.out.println("p4.toString() = " + p4.toString()); // java.lang.NullPointerException
    }
}
(code dans ToString1.java)
java ToString1
p1 = Personne@7bfcd12c
p2 = Mathematicien@24273305
p3 = Mathematicien@24273305
p4 = null
Exception in thread "main" java.lang.NullPointerException
	at ToString1.main(ToString1.java:17)

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 classe java.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 Mathematicien
Voir 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 :

Implémenter clone()

La signature de Object.clone() est
protected native Object clone() throws CloneNotSupportedException
Pour 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 et absorbé l'exception.

Shallow vs deep copy

L'implémentation de Object.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) ? false
On 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 de equals() 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 :
  1. Si deux objets sont égaux au sens de equals(), alors ils doivent renvoyer le même hash code.
  2. Mais deux objets différents au sens de equals() ne doivent pas obligatoirement renvoyer deux hash codes différents (bien que ce soit conseillé).
Exemple :
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() = 1761653376
Les 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.