Héritage

Usage

Il convient de considérer deux héritages : Hériter d'un type signifie supporter toutes les actions pouvant être effectuées sur les objets de ce type. Hériter d'une implémentation signifie posséder les champs et réutiliser le code de la classe dont on hérite.

Lorsqu'une classe implémente une interface :
class C implements I {
ou qu'une interface en étend une autre :
interface I2 extends I {
il s'agit d'héritage de type : les types C et I2 héritent du type I, c'est à dire que partout où l'on utilise des objets du type I, on pourra utiliser un objet de type I2 ou C.

Lorsqu'une classe en étend une autre :
class C2 extends C {
il y a à la fois héritage de type et héritage d'implémentation. Malheureusement en Java, il est impossible de faire de l'héritage d'implémentation seul (parfois appelé héritage privé). Si l'on veut implémenter une classe C2 à l'aide du code d'une classe C, sans qu'un objet instance de C2 ne soit également une instance de C, il faut utiliser le principe de la délégation :
class C2 {
    private C delegué;
Il est rare que l'on ait besoin d'utiliser à la fois les deux héritages, car la liaison entre les classes est alors très forte. Par conséquent, extends doit être manié avec précaution. Dans les deux cas suivant, son utilisation est incontournable :

Héritage implicite

Toute classe hérite de la classe java.lang.Object. C'est à la fois un héritage de type (toute object est un Object) et d'implémentation.

Résolution

Dans le code suivant :
class C1 {
    String vi = "C1.vi";
    static String vc = "C1.vc";
    void mi() {
        System.out.println("Je suis la méthode d'instance C1.mi()");
    }
    static void mc() {
        System.out.println("Je suis la méthode de classe C1.mc()");
    }
}

class C2 extends C1 {
    String vi = "C2.vi";
    static String vc = "C2.vc";
    void mi() {
        System.out.println("Je suis la méthode d'instance C2.mi()");
    }
    static void mc() {
        System.out.println("Je suis la méthode de classe C2.mc()");
    }
}

class Test {
    static void test(C1 o) {
    System.out.println("Variable d'instance : " + o.vi);
    System.out.println("Variable de classe : " + o.vc);
    o.mi();
    o.mc();
    }
    public static void main(String [] args) {
        C2 o = new C2();
        test(o);
    }
}

o est une instance de la classe C2, sous-classe de la classe C1. C1 et C2 possèdent toutes les deux une variable d'instance String vi, une méthode d'instance void mi() , une variable de classe static String vc, et une méthode de classe static void mc(). Dans la méthode Test.test(C1), le type déclaré de o est C1, son type réel C2.
Alors, java Test provoquera l'affichage :
Variable d'instance : C1.vi
Variable de classe : C1.vc
Je suis la méthode d'instance C2.mi()
Je suis la méthode de classe C1.mc()
ce qui se traduit de la façon suivante : pour les méthodes d'instance, la résolution de la méthode à exécuter est dynamique et dépendra du type réel de l'objet et non de son type déclaré. Dans tous les autres cas (méthode de classe, variable de classe ou d'instance), seul le type déclaré compte. Tout le travail de sélection est donc effectué à la compilation.

Attention :
soit le code suivant :
class C1 {
    void mi(C1 o) {
        System.out.println("Je suis la méthode d'instance C1.mi(C1)");
    }
}

class C2 extends C1 {
    void mi(C2 o) {
        System.out.println("Je suis la méthode d'instance C2.mi(C2)");
    }
}

class Test {
    public static void main(String [] args) {
        C1 o = new C2();
        o.mi(o);
    }
}

java Test provoquera l'affichage :
Je suis la méthode d'instance C1.mi(C1)
En effet, la méthode à sélectionner est mi(C1) ! Or C2 ne redéfinit pas cette méthode. Il se contente de la surcharger par une méthode mi(C2). Par conséquent, la seule méthode mi(C1) qu'une instance de C2 possède est celle héritée de C1.
C'est pour cette raison que lorsque l'on redéfinit au niveau d'une classe C la méthode equals(Object) héritée de Object, il conviend d'écrire
class C {
    public boolean equals(Object o) {
        return o != null && o instanceof C && <test spécifique>;
    }
}

Accès aux constructeurs, méthodes et variables de la sur-classe

Lorsque l'on instancie un objet d'une classe C2, héritant d'une classe C1, les constructeurs de C2 commencent par faire un appel à un constructeur de C1. Si cet appel est implicite, le constructeur utilisé est le constructeur vide C1(). Il est possible de faire un appel explicite à un constructeur de C1, disons C1(<signature>), par exemple pour initialiser une variable. Cet appel se fait en utilisant la méthode super(<signature>).
Si la classe C2 possède une variable de même nom qu'une variable de la classe C1, disons a, une instance de C2 possèdera deux attributs de même nom a. L'accès à celui de la classe mère se fera en utilisant le mot-clé super : super.a. Il en est de même pour les méthodes : si C1 et C2 possède toutes les deux une méthode m(<signature>), l'utilisation explicite de la méthode m(<signature>) de C1 se fera par l'appel de super.m(<signature>).
Le code suivant illustre l'utilisation de super :
class C1 {
    int val;

    C1() {
        val = 0;
    }

    C1(int i) {
        val = i;
    }

    int val() {
        return val;
    }
}

class C2 extends C1 {
    int val;

    C2() {
    // appelle implicitement C1()
    val = 1;
    }

    C2(int i) {
        super(i-1); // appelle explicitement C1(int)
        val = i;
    }

    int val() {
        return val;
    }

    void affiche() {
        System.out.println("val = " + val + ", super.val = " + super.val);
        System.out.println("val() = " + val() + ", super.val() = " + super.val());
    }
}

class Test {
    public static void main(String [] args) {
        C2 o = new C2(3);
        o.affiche();
    }
}

java Test affichera :
val = 3, super.val = 2
val() = 3, super.val() = 2

Héritage multiple

En Java, seul l'héritage multiple de types est possible. L'héritage multiple d'implémentation est interdit. Ainsi, une même classe peut implémenter plusieurs interfaces, mais ne peut étendre qu'une seule classe abstraite ou concrète.
class C2 extends C1 implements I1, I2, I3 {
...
Il est également possible de définir une interface étendant plusieurs interfaces.
interface I extends I1, I2, I3 {
...