9.    L’héritage

Définir une nouvelle classe, dite classe dérivée, à partir d’une classe existante dite classe de base.

Remarques:


9.1    Définition d'une classe dérivée

public class Point {
    protected double x=3.4, y=5.6 ;
    public String  toString(){
        return "point: (" + x + "," + y+")" ;
    }
    public void move(double dx, double dy){
        System.out.println("Moving the point");
        x+=dx;
        y+=dy;
    }
}
-------------------------------------------------
public class PointC extends Point{
    private byte col=3;
    public void dspl(){
        System.out.println("point col:("+x+"," +y+","+col+")");
    }
    public void inc(){
        x++; y++;
    }
}
--------------------------------------------
public class Test {
    public static void main(String arg[]){
        Point p = new Point();
        System.out.println(""+p);
        PointC pc = new PointC();
        pc.inc();
        pc.dspl();       
    }
}

Remarques:

 



9.2    Les variables avec le même nom

Il est possible que dans la classe de base et dans la classe dérivée sont déclarées des variables qui portent le même nom. Dans ce cas les deux variables coexistent, mais dans la classe dérivée la variable locale masque (fait l'ombre à) la variable de la classe de base. Si on veut utiliser la variable de la classe de base il faut utiliser le mot clé super.


public class Point {
    protected double x=3.4, y=5.6 ;
    protected String name ="base point";
    public String  toString(){
        return "point: (" + x + "," + y+")" ;
    }
    public void move(double dx, double dy){
        System.out.println("Moving the point");
        x+=dx;
        y+=dy;
    }
}
-------------------------------------------------

public class PointC extends Point{
    private byte col=3;
    private String name ="color point";
    public void dspl(){
        System.out.print("point col:("+x+","+y+
                                        ","+col+")");
        System.out.print("\tname:"+name);        //name locale
        System.out.println("\tname of parent object:"+super.name);
    }
    public void inc(){
        x++; y++;
    }
}
-----------------------------------------------------------------------

public class Test {
         public static void main(String arg[]){
               Point p = new Point();
               System.out.println(""+p);
               PointC pc = new PointC();
               pc.inc();
               pc.dspl();      
        
}

}




9.3    Construction et initialisation des objets dérivés


9.3.1    Appels des constructeurs

Dans l'exemple suivant la classe Point dispose de deux constructeurs – sans arguments et avec deux arguments. Les deux constructeurs affichent sur la console un message correspondant, juste pour savoir quel constructeur est appelé.

public class Point {
    protected double x, y;
    public Point(){
        System.out.println("Constructor base no prs");
        x=3.4; y=5.6;
    }
    public Point(double x, double y){
        System.out.println("Constructor base 2 prs "+x+","+y);
        this.x=x;   this.y=y;
    }
    public String  toString(){
        return "point: (" + x + "," + y+")" ;
    }
    public void move(double dx, double dy){
        System.out.println("Moving the point");
        x+=dx;
        y+=dy;
    }
}

----------------------------------------------------------------------
public class PointC extends Point{
    private byte col;
    public PointC(){
        System.out.println("Constructor drvd no par");
        col=3;
    }
    public PointC(double x,double y,byte col){
        super(x,y);
        System.out.println("Constructor drvd 3 par "+x+","+y+","+col);
        this.col = col;
    }
    public void dspl(){
        System.out.println("point col:("+x+","+y+","+col+")");
    }
    public void inc(){
        x++; y++;
    }
}
--------------------------------------------------------------------------
public class Test {
    public static void main(String arg[]){
        Point p = new Point();
        Point p1 = new Point(1.4,2.6);
        System.out.println(p +"  "+p1);
        PointC pc = new PointC();
        pc.dspl();  
        PointC pc1 = new PointC(4.1, 5.2,(byte)4);
        pc1.dspl();
    }
}

La construction de l'objet de la classe dérivée pc est fait par le constructeur sans arguments. On voit des messages affichés, qu'avant de faire quoi que soit le constructeur fait un appel de constructeur de la classe de base sans argument. Si dans la classe de base un tel constructeur n'existe pas il y aura une faute.

La construction de l'objet pc2 montre une autre possibilité. Dans se cas le constructeur avec deux arguments de la classe base est appelé. Les arguments nécessaires son livré  par le mot clé super. L'appel explicite de constructeur de la classe de base par le mot clé super devrait être obligatoirement la première instruction dans le constructeur de la classe dérivé.

Remarques:


9.3.2    Initialisation d’un objet dérivé

class B extends A { ..... }

La création d’un objet de type B se déroule en 6 étapes.
•    Allocation mémoire pour un objet de type B. Il s’agit bien de l’intégralité de la mémoire nécessaire pour un tel objet, et pas seulement pour les variables non hérités de A.
•    Initialisation par défaut de tous les variables de B (aussi bien ceux hérités de A, que ceux propres à B aux valeurs "nulles" habituelles).
•    Initialisation explicite, s’il y a lieu, des variables  hérités de A
•    Exécution du constructeur choisi de A.
•    Initialisation explicite, s’il y a lieu, des variables propres à B.
•    Exécution du constructeur choisi de B

9.4    Dérivations successives

Une classe dérivée peut, à son tour, servir de classe de base pour une autre. D’une même classe peuvent être dérivées plusieurs classes différentes.





        Fig. 9.1 Arborescence des classes dérivées

•    En Java une classe dérivée ne peut avoir qu'une seule classe de base au niveau immédiatement supérieur


9.5    Redéfinition  et surcharge de méthodes

9.5.1    Surcharge de méthodes (overloading)

La notion de surcharge (des méthodes qui porte le même nom mais qui se different par leurs arguments) se généralise dans le cadre de l’héritage: une classe dérivée pourra à son tour surcharger une ou plusieurs méthodes d’une classe ascendante. Bien entendu, la nouvelle méthode ne deviendra utilisable que par la classe dérivée ou ses descendantes, mais pas par ses ascendantes.



public class Point {
    protected double x, y;
    public Point(double x, double y){
        System.out.println("Constructor base");
        this.x=x;   this.y=y;
    }
    public String  toString(){
        return "point: (" + x + "," + y+")" ;
    }
    public void move(double dx, double dy){
        System.out.println("Moving the point");
        x+=dx;     y+=dy;
    }
}
-----------------------------------------------------------
public class PointC extends Point{

    private byte col;
    public PointC(double x,double y,byte col){
        super(x,y);
        System.out.println("Construct drvd");
        this.col = col;
    }
    public void dspl(){
        System.out.println("point col:("
                +x+","+y+","+col+")");
    }
    public void move(double dxy){
        System.out.println("Moving overloaded ");
        x+=dxy; y+=dxy;
    }
}
-------------------------------------------------------------
public class Test {
    public static void main(String arg[]){
        Point p1 = new Point(1.4,2.6);
        System.out.println(""+p1);
        p1.move(2.2,3.5);
        //p1.move(4.4);     error not in the base class
        System.out.println(""+p1);
        PointC pc = new PointC(2.1,3.2,(byte)3);
        pc.dspl();   
        pc.move(1.4, 2.3);     // inherited method
        pc.dspl();
        pc.move(3.3);        //overloaded method
        pc.dspl();
    }
}
--------------------------------------------------------------

Remarque:

9.5.2    Redéfinition de méthodes (overriding)

La redéfinition (en anglais overriding) de méthodes représente une technologie puissante en Java. Elle permet à une classe dérivée de redéfinir une méthode de sa classe de base, en proposant une nouvelle définition. Il faut respecter la signature de la méthode (type et nombre des arguments), ainsi que le type de la valeur de retour.


public class Point {
    protected double x, y;
    public Point(double x, double y){
        System.out.println("Constructor base");
        this.x=x;   this.y=y;
    }
    public String  toString(){
        return "point: (" + x + "," + y+")" ;
    }
    public void move(double dx, double dy){
        System.out.println("Moving the point");
        x+=dx;     y+=dy;
    }
}
-----------------------------------------------------------------
public class PointC extends Point{
    private byte col;
    public PointC(double x,double y,byte col){
        super(x,y);
        System.out.println("Construct drvd");
        this.col = col;
    }
    public String toString(){        //overrided
        return "color "+col+" "+super.toString();
    }
    public void move(double dxy){
        System.out.println("Moving overloaded ");
        x+=dxy; y+=dxy;
    }
}
----------------------------------------------------------------------------
public class Test {
    public static void main(String arg[]){
        PointC pc = new PointC(1.3,2.4,(byte)2);
        System.out.println("after creating "+pc);
        pc.move(2, 2);
        System.out.println("after first move "+pc);
        pc.move(3);
        System.out.println("after second move "+pc);
    }
}

----------------------------------------------------------------------------
La nouvelle méthode doit retourner une valeur d’un type identique ou dérivé de celui de la méthode qu’elle redéfinit.

La redéfinition d’une méthode ne doit pas diminuer les droits d’accès à cette méthode. En revanche, elle peut les augmenter.

class A {
    public void f (int n) { ..... }
}
class B extends A {
    private void f (int n) {     //erreur!
    .....
    }
}
----------------------------------------------------------------
    class A {
    private void f (int n) { ..... }
}
class B extends A {
    public void f (int n) {     //OK!
    .....
    }
}
--------------------------

Une méthode statique ne peut pas être redéfinie dans une classe dérivée.

Remarque:


9.6    Le polymorphisme

Un concept extrêmement puissant en P.O.O., qui complète l’héritage. On peut caractériser le polymorphisme en disant qu’il permet de manipuler des objets sans en connaître exactement leur type.
Il faut noter qu'il ne s’agit pas de traiter ainsi n’importe quel objet. Le polymorphisme exploite la relation  induite par l’héritage en appliquant la règle suivante : un objet d'une classe dérivée "is a" un objet de classe de base.  Un point coloré est aussi un point, on peut donc bien le traiter comme un point.

9.6.1    Les bases

Considérons la situation dans laquelle les classes "Point" et "PointC" disposent chacune d’une méthode "toString()", ainsi que des constructeurs habituels:

public class Point {
    public Point (int x, int y) { ..... }
    public String toString() { ..... }
}
public class PointC extends Point {
    public PointC (int x, int y, byte couleur) { ..... }
    public String toString() { ..... }
}
------------------------
Point p ;
p = new Point (2.3, 5.4);



          Fig. 9.2 Objet de type Point


p = new PointC (2.3, 5.4, (byte)7) ;    // p de type Point contient la référence à un objet de type PointC



       Fig. 9.3 Objet PointC référencé par Point


D’une manière générale, Java permet d’affecter à un variable objet non seulement la référence à un objet du type correspondant, mais aussi une référence à un objet d’un type dérivé. On peut dire qu’on est en présence d’une conversion implicite (légale) d'un objet de type dérivé vers un objet de classe de base (upcasting). On parle aussi de compatibilité par affectation entre un type classe et un type ascendant.


Point p ;
p = new Point (2.3, 5.4);
System.out.println(""+p);                // appelle la méthode toString() de la classe Point
p = new PointC (2.3, 5.4, (byte)7) ;
System.out.println(""+p);                // appelle la méthode toString() de la classe PointC

Ce choix d’une méthode au moment de l’exécution (et non plus de la compilation) porte généralement le nom de ligature dynamique (ou encore de liaison dynamique).
Le polymorphisme permet d’obtenir un comportement adapté à chaque type d’objet, sans avoir besoin de tester sa nature.

-------------------------------------------

tableau "hétérogène" d’objets

public class Point {
    protected double x, y;
    public Point(double x, double y){
        System.out.println("Constructor point:"
                +x+","+y);
        this.x=x;   this.y=y;
    }
    public String  toString(){
        return "point: (" + x + "," + y+")" ;
    }
}   

public class PointC extends Point{
    private byte col;
    public PointC(double x,double y,byte col){
        super(x,y);
        System.out.println("Construct PointC col="+col);
        this.col = col;
    }
    public String toString(){
        return "color "+col+" "+super.toString();
    }
}

public class Test {
    public static void main(String arg[]){
        Point p[] = new Point[6];
        for(int i =0; i< p.length;i++){
            if(Math.random()>0.5){
                p[i]= new Point(5*i,2*i);
            }
            else{
                p[i]= new PointC(3*i,4*i,(byte)i);
            }
        }
        System.out.println("\nThe points in the array are");
        for(int i =0;i<p.length;i++){
            System.out.println(i+":"+p[i]);
        }
    }
}

9.6.2    Généralisation à plusieurs classes

Les classes marquées d’un astérisque définissent ou redéfinissent une méthode fx().



Fig.9.4. Polymorphisme en arborescence des classes dérivées

Supposons qu'on a créé des références à partir de chaque classe de la Fig. 9.4:
A a ; B b ; C c ; D d ; E e ; F f ;

Les affectations suivantes sont légales:
a = b ; a = c ; a = d ; a = e ; a =f ;
b = d ; b = e ;
c = f ;

En revanche, celles-ci ne le sont pas :
b = a ;             // erreur : A ne descend pas de B
d = c ;             // erreur : C ne descend pas de D
c = d ;             // erreur : D ne descend pas de C

Appel de la méthode fx()
a=new A();  a.fx();            méthode fx() de A
a=new B();  a.fx();                méthode fx() de A
a=new C();  a.fx();            méthode fx() de C
a=new D();  a.fx();            méthode fx() de D
a=new E();  a.fx();                méthode fx() de A
a=new F();  a.fx();                méthode fx() de C

9.6.3    Les conversions explicites de références

La conversion implicite d'un objet de type de base  vers un objet de type dérivé  (downcasting) n'est pas permis! 

Point p ;
PointC pc = new PointC(...), pc2 ;
.....
p = pc ;                                         // p contient la référence à un objet de type PointC
.....
pc2 = p ;                                     // refusé en compilation


Par contre la conversion "downcasting"explicite et acceptée en compilation, mais il faut s'assurer que  la référence de type de base est orientée à un objet du type dérivé.


pc2 = (PointC) p ;        // accepté en compilation, ClassCastException possible
                                    // lors execution

operateur    instanceof  pour verifier On peut s’assurer qu’un objet est bien une instance d’une classe donnée en recourant à l’opérateur instanceof. Par exemple, l’expression
"p instanceof PointC" vaudra true si p est (exactement) de type PointC

p instanceof PointC      //true /false


9.7    La superclasse Object

Il existe une classe nommée Object dont toutes les classes en Java sont implicitement dérivées

class Point {
    .....
}

tout se passe en fait comme si vous aviez écrit (vous pouvez d’ailleurs le faire):

class Point extends Object {
    .....
}
----------
Point p = new Point (...) ;
PointC pc = new PointC (...) ;
Object obj ;
obj = p ;                                     // OK
obj = pc ;                                     // OK
--------------

La classe Object dispose de quelques méthodes qu’on peut soit utiliser telles quelles, soit redéfinir. Les plus importantes sont toString() et equals().

toString() - fournit le nom de la classe concernée et l'adress de l'objet en hexadecimale (précédé de @)

package deriv1;
public class Point {
    protected double x, y;
    public Point(double x, double y){
        System.out.println("Constructor point:"
                        +x+","+y);
        this.x=x;   this.y=y;
    }
}   
-----------------
package deriv1;
public class Test3 {
    public static void main(String arg[]){
        Point p = new Point(3.2,4.3);
        System.out.println(""+p);
    }
}
------------------

equals() - se contente de comparer les adresses des deux objets

public class Point {
    protected double x, y;
    public Point(double x, double y){
        System.out.println("Constructor point:"
                        +x+","+y);
        this.x=x;   this.y=y;
    }
}   
------------------
public class Test3 {
    public static void main(String arg[]){
        Point p = new Point(3.2,4.3),p1=p;
        Point p2 =new Point(3.2,4.3);
        System.out.println("p.equals(p1):"+p.equals(p1)
                +"\tp.equals(p2):"+p.equals(p2));
    }
}

Pour comparer les objets (pas leur réferenceces) "equals()" doit être redéfini convenablement!

9.8    Classes et méthodes finales

Une classe déclarée final ne peut plus être dérivée.  But - liaisons statiques, optimization.

Une méthode déclarée final ne peut pas être redéfinie dans une classe dérivée.

9.9    Les classes abstraites

Une classe abstraite est une classe qui ne permet pas d’instancier des objets. Elle ne peut servir que de classe de base pour une dérivation.

Dans une classe abstraite, on peut trouver classiquement des méthodes et des variables membres, dont héritera toute classe dérivée. Mais on peut aussi trouver des méthodes dites abstraites. Une méthode abstraite ne fournit que la signature et le type de la valeur de retour.

 

-------------------------
abstract class A{
    …
    public void fx() { ..... }                 // fx() est définie dans A
    public abstract void fa(int n) ;     // fa() – méthode abstraite, elle n’est pas définie dans A
                                            // on n’en a fourni que l’en-tête(pas de corps de la méthode)
}
-------------------------------


A va ;                     // OK : va n’est qu’une référence sur un objet de type A ou dérivé

va = new A(...) ;         // erreur : pas d’instanciation d’objets d’une classe abstraite

----------------------------

class B extends A{
    public void fa(int n) { ..... } // ici, on définit la méthode abstraite fa()
.....
}

A va = new B(...) ; // OK

-------------------

•    Une méthode abstraite ne peut pas être déclarée private, ce qui est logique puisque sa vocation est d’être redéfinie dans une classe dérivée.
•    Dans l’en-tête d’une méthode déclarée abstraite, les noms d’arguments muets doivent figurer (bien qu’ils ne servent à rien).
•    Une classe dérivée d’une classe abstraite n’est pas obligée de (re)définir toutes les méthodes abstraites de sa classe de base (elle peut même n’en redéfinir aucune). Dans ce cas, elle reste simplement abstraite (il est quant même nécessaire de mentionner abstract dans sa déclaration).
•    Une classe dérivée d’une classe non abstraite peut être déclarée abstraite et contenir des méthodes abstraites.
•    En générale dans la programmation orientée objet, une classe abstraite est une classe de quelle on ne peut pas instancier des variables. Une méthode abstraite est une méthode avec une définition différée ou retardée pour être fait dans une classe dérivée.

Exemple:

abstract public class Displayable {
    abstract public void dspl();
}
---------------------------------------
public class Two extends Displayable{
    private double value;
    Two(double v){
        this.value =v;
    }
    public void dspl(){
        System.out.println("double value: "+value);
    }
}
--------------------------------------------------
public class One extends Displayable{
    private int value;
    One(int v){
        this.value =v;
    }
    public void dspl(){
        System.out.println("int value: "+value);
    }
}
-------------------------------------------
public class Test {
    public static void main(String arg[]){
        Displayable tb[] = new Displayable[6];
        for(int i =0;i<tb.length;i++){
            double d;
            if((d=Math.random())>0.5){
                tb[i]=new One((int)(d*10));
            }
            else {
                tb[i]=new Two(d*10);
            }
        }
        for(int i=0;i<tb.length;i++){
            tb[i].dspl();
        }
    }
}


9.10    Les interfaces

Si l’on considère une classe abstraite n’implantant aucune méthode et aucun variable de classe, on aboutit à la notion d’interface. En effet, une interface définit les en-têtes d’un certain nombre de méthodes, ainsi que des constantes.

•    Une classe pourra hériter (mot clé - impléments) plusieurs interfaces (alors qu’une classe ne pouvait hériter  (mot clé extends) qu' une seule classe abstraite).
•    Les interfaces pourront se dériver (mot clé extends).
•    On pourra utiliser des références de type interface.

9.10.1    Définition


public interface Int {
    void fx(int n) ;             // en-tête d’une méthode fx()  public abstract facultatifs
    void gx() ;                 // en-tête d’une méthode gx() public abstract facultatifs
}

9.10.2    Implémentation d’une interface


class A implements Int {
    // A doit (re)définir les méthodes f x() et gx() prévues dans l’interface Int
}
-----------------------------------------------------
class A implements Int, Int1, Int2 {
    // A doit (re)définir les méthodes  prévues dans les interface Int, Int1 et Int2
}

9.10.3    Les références de type interface


public interface Int { .....}
.....
Int intrf ; // intrf peut référencier un objet d’une classe implémentant l’interface I

----------------------------

class A implements Int { ..... }
.....
Int intrf = new A(...) ; // OK


9.10.4    Interfaces et constantes


Une interface peut  renfermer des constantes qui sont accessibles à toutes les classes implémentant l’interface.
Ces constantes sont automatiquement considérées comme si elles avaient été déclarées public, static et final.

9.10.5    Dérivation d’une interface

interface Int1 {
    void fx(int n) ;
    static final int MAXI = 100 ;
}   

interface Int2 extends Int1 {
    void gx() ;
    static final int MINI = 20 ;
}

En fait, la définition de Int2 est équivalente à  :

interface Int2{
    void f(int n) ;
    void g() ;
    static final int MAXI = 100 ;
    static final int MINI = 20 ;
}

La possibilité partager la définition en deux augmente la flexibilité du programme.

9.10.6    Exemple

interface Modif {
    void zoom();
}
interface Displayable extends Modif{
    void dspl();
}
---------------------------------------
public class Two implements Displayable{
    private double value;
    Two(double v){
        this.value =v;
    }
    public void dspl(){
        System.out.println("double value: "+value);
    }
    public void zoom(){
        value/=2;
    }
}
--------------------------------------------------
 
public class One implements Displayable{
    private int value;
    One(int v){
        this.value =v;
    }
    public void dspl(){
        System.out.println("int value: "+value);
    }
    public void zoom(){
        value*=2;
    }
}
-------------------------------------------
public class Test {
    public static void main(String arg[]){
        Displayable tb[] = new Displayable[6];
        for(int i =0;i<tb.length;i++){
            double d;
            if((d=Math.random())>0.5){
                tb[i]=new One((int)(d*10));
            }
            else {
                tb[i]=new Two(d*10);
            }
        }
        for(int i=0;i<tb.length;i++){
            tb[i].dspl();
            tb[i].zoom();
            tb[i].dspl();
            System.out.println();
        }
    }
}