Rechercher
 

Méthodes abstraites, classes abstraites et interfaces

Mise en oeuvre du concept d'héritage Implémentations d'interfaces



Accès rapide :
La vidéo
Méthodes et classes abstraites
Faire usage de l'abstraction
Une classe abstraite ne peut pas être instanciée !
Implémentation de méthode abstraite
Utilisation de notre hiérarchie de classes
Travaux pratiques
Le sujet
La correction
Le concept d'interface
Assistance proposée par Eclipse pour implémenter vos méthodes abstraites

La vidéo

Cette vidéo vous présente les concepts de méthodes abstraites et de classes abstraites, à l'aide d'un exemple de code adapté. Au terme de cette vidéo, la notion d'interface (quasiment équivalente à une classe totalement abstraite) est présentée.


Méthodes abstraites, classes abstraites et interfaces

Méthodes et classes abstraites

Faire usage de l'abstraction

Dans le chapitre précédent, nous avons commencé à nous familiariser avec le concept d'héritage. L'héritage permet donc de factoriser un certains nombre de méthodes pour un ensemble de types. Mais nous allons voir que parfois on aimerait factoriser un comportement mais sans que l'on soit en mesure de l'implémenter au niveau d'abstraction dans lequel on se trouve : dans ce cas nous définiront une (ou plusieures), méthode(s) abstraites(s).

Qu'est ce que cela veut dire ? Le mieux, pour comprendre la chose, est de considérer un exemple concret. Je vous propose de repartir de l'exemple de code que vous avez produit dans le TP précédent : à savoir, la gestion des classes de figures géométriques. Pour rappel, voici un diagramme UML montrant la hiérarchie de classe développée.

Si l'on considère cet exemple, il peut paraître naturel d'annocer que l'on doit pouvoir calculer la surface de n'importe quelle figure géométrique. Nous avons donc envie d'ajouter une méthode area, qui calcule une valeur flottante, au niveau de la classe Shape. Mais comment implémenter une méthode de calcul de surface si on ne connait pas les spécificités de la figure géométrique ? C'est là qu'intervient le concept de méthode abstraite : nous allons déclarer cette méthode, mais nous ne l'implémenterons pas ! Nous laisserons le soin aux sous-classes de concrétiser la méthode.

Pour définir une méthode comme étant abstraite, il faut utiliser le mot clé abstract devant la méthode et, bien entendu, il ne faudra plus mettre de corps de méthode (plus de paire d'accolades). Voici comment déclarer une méthode abstraite.

 1 
public abstract double area();
Exemple de déclaration d'une méthode abstraite

Cela va entrainer une autre modification : si une classe contient au moins une méthode abstraite, alors la classe doit, elle aussi, être définie ainsi. Voici la nouvelle version de notre classe Shape intégrant cette méthode abstraite.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
package fr.koor.poo;

import java.awt.Point;

abstract public class Shape /* extends Object */ {

    // --- The center attribute ---
    private Point center;
    
    // --- 2 constructors ---
    public Shape() {
        this( new Point( 0, 0 ) );
    }
    
    public Shape( Point center ) {
        // super();     // Appel au constructeur parent sous-entendu
        this.setCenter( center );
    }
      
    // --- The center property ---
    public Point getCenter() {
        return center;
    }
    
    public void setCenter( Point center ) {
        if ( center == null ) {
            throw new NullPointerException( "center parameter cannot be null" );
        }
        this.center = center;
    }
    
    // --- An abstract method for compute the area of the shape ---
    public abstract double area();
    
    // --- A method for compute a shape representation string ---
    @Override public String toString() {
        return "Unknown shape placed at " + center;
    }
    
}
Exemple de définition d'une classe abstraite

Une classe abstraite ne peut pas être instanciée !

Il faut comprendre qu'arrivé à ce stade on ne peut plus directement utiliser notre classe Shape. Ce que j'entends par « directement », c'est que cette classe spécifie un comportement que l'on souhaite obtenir, mais sans l'implémenter. Cette classe est donc incomplète (abstraite). Si vous pouvions créer une instance de cette classe, que se passerait-il si on invoquait la méthode area ? Et bien ça planterait le programme ! Du coup, Java préfère retirer l'instanciation d'une classe abstraire plutôt que de générer des plantages à l'exécution. Le test suivant vous montre de qui se passe si l'on cherche à produire une instance à partir de la classe Shape.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
package fr.koor.poo;

public class Start {

    public static void main( String [] args ) {
        
        Shape shape = new Shape();
        
    }
    
}
Tentative d'instanciation d'une classe abstraite

Et voici le message d'erreur produit par le compilateur.

$> javac fr/koor/poo/Start.java
fr/koor/poo/Start.java:7: error: Shape is abstract; cannot be instantiated
        Shape shape = new Shape();
                      ^
1 error
$>

Mais alors, à quoi sert une classe abstraite qu'on ne peux pas instancier ? C'est simple, a imposer la présence de méthodes dans les classes filles. Effectivement, pour l'heure, on ne peut pas non plus instancier un cercle ou un carré. Pourquoi ? Par-ce que ces deux classes héritent de la méthode abstraite area. Donc pour l'heure Circle et Square sont aussi considérées comme abstraites ! Il faut donc, obligatoirement redéfinir la méthode area dans nos classes de figures géométriques si l'on souhaite que le projet recompile.

Implémentation de méthode abstraite

Nous sommes d'accord, le calcul de surface d'un cercle et celui d'un carré sont bien deux traitements distincts. Donc chaque classe doit implémenter sa méthode de calcul de surface. Dans l'exemple de code suivant, je ne précise que la méthode manquante, je vous laisse reprendre le reste de la classe de la correction du chapitre précédent.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
package fr.koor.poo;

import java.awt.Point;

public class Circle extends Shape {

    // Begin of the class: attributes, constructors, properties, ...
    
        
    @Override 
    public double area() {
        return Math.PI * this.radius * this.radius;
    }

}
Calcul de surface d'un cercle : a = PI*r²

Et on fait de même pour notre classe de manipulation de carrés.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
package fr.koor.poo;

import java.awt.Point;

public class Square extends Shape {

    // Begin of the class: attributes, constructors, properties, ...
    
        
    @Override 
    public double area() {
        return this.length * this.length;
    }

}
Calcul de surface d'un carré
Si vous ne redéfinissez pas, pour une classe de fille, la méthode abstraite area, vous êtes dans l'obligation de marquer cette classe comme étant elle aussi abstraite : elle hérite d'une méthode abstraite, elle est donc abstraite.

Utilisation de notre hiérarchie de classes

Maintenant il nous faut revoire la classe de démarrage : on ne peut plus produire d'instance de Shape. Mais, si on y réfléchi, qu'est ce que cela voulais dire d'instancier une figure dont on ne connaissait pas exactement la nature ? C'est presque logique. Du coup, je n'y instancie plus que des cercles et des carrès.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
package fr.koor.poo;

import java.awt.Point;
import java.util.ArrayList;

public class Start {

    public static void main( String [] args ) {
        
        ArrayList<Shape> shapes = new ArrayList<>();
        
        shapes.add( new Circle( new Point( 5, 5 ), 1 ) );
        shapes.add( new Square( new Point( 1, 1 ), -2 ) );
        shapes.add( new Circle() );
        
        for( Shape shape : shapes ) {
            System.out.println( shape + " -> " + shape.area() );
        }
        
    }
    
}
Utilisation de nos méthodes de calcul de surface

Et voici les résultats produits par cet exemple de code.

$> java fr.koor.poo.Start
A circle placed at java.awt.Point[x=5,y=5] and with a radius 1.0 -> 3.141592653589793
A square placed at java.awt.Point[x=1,y=1] and with a length 2.0 -> 4.0
A circle placed at java.awt.Point[x=0,y=0] and with a radius 1.0 -> 3.141592653589793
$>

Peut-être vous posez-vous une question arrivé à ce point : pourquoi être obligé de spécifier la méthode area au niveau de Shape alors qu'on invoque que les redéfinitions de cette méthode au niveau de Circle et de Square ? Ne pourrions-nous pas enlever la définition de la méthode abstraite sur Shape ? Et bien je vous propose de le tester et vous constaterez que le programme ne compilera plus. Pourquoi ?

La réponse est simple, c'est pour que le polymorphisme puisse fonctionner. En fait, il y a deux étapes bien distinctes : la compilation puis l'exécution du programme. Durant la phase de compilation, le compilateur ne peut pas savoir quelles seront les figures qui seront présentes dans la collection. Donc la seule chose qu'il sait, c'est que la collection contiendra un ensemble de Shape. Effectivement, en ligne 10, la collection est typée ArrayList<Shape> (c'est un type générique, nous reviendrons sur ce concept ultérieurement). Notez qu'on retrouve bien ce type Shape quand on cherche à sortir un élément de la collection en ligne 16. En conséquence, si l'on cherche à invoquer une méthode sur cet élément, cette méthode devra être définie sur le type de base : comme dans le for on invoque la méthode area, nous devons donc la définir dans Shape (pour que ça compile) et comme à ce niveau de définition on ne sait pas implémenter la méthode, on doit la définir abstraite.

La seconde étape, c'est l'exécution du programme. A ce stade, il faudra trouver la nature exacte de notre figure géométrique pour pouvoir invoquer la bonne méthode. Et nous avons l'obligation d'avoir concrétisé la méthode sur les classes filles, sans quoi nous n'aurions pas pu faire les instaciations (les new).

Travaux pratiques

Le sujet

Ajouter une méthode abstraite de calcul de périmètre au niveau de la classe Shape et faîte en sorte de l'implémenter sur nos deux classes filles. Voici un modèle UML présentant le contenu des classes souhaitées. Pour information, en UML, une méthode abstraite se note en italique : ainsi, et avec un peu d'habitude, on repère facilement les méthodes abstraites proposées par un modèle (bon, il faut quand même les voir).

la formule de calcul du périmètre d'un cercle est 2*PI*rayon. Celle d'un carré est 4*longueur.

Les plus courageux d'entre vous pourront aussi ajouter une nouvelle classe Triangle, mais je ne proposerais pas de correction sur ce point. Dans ce cas, mettez en attribut de cette classe un tableau de trois vecteurs reprensentant les positions des sommets du triangle par rapport au centre de la figure. Vous devrez alors aussi fournir une nouvelle classe Vector constituée de deux attributs (un décalage en X et un décalage en Y).

La correction

Etant donné que nous avions déjà corrigé certains de ces code dans le chapitre précédent (le chapitre relatif à l'héritage), je ne mettrais dans les code de corrections qui suivent, que les éléments importants relatifs à sujet du TP. Je vous laisse le soin de copier/coller les codes manquants à partir du chapitre précédent, si vous n'avez pas réussit à les produire. Nous commençons par définir notre nouvelle méthode abstraite.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
package fr.koor.poo;

import java.awt.Point;

abstract public class Shape /* extends Object */ {

    // BEGIN OF THE CLASS
    
    // --- An abstract method for compute the area of the shape ---
    public abstract double area();
    public abstract double perimeter();
    
    // END OF THE CLASS
    
}
Exemple de définition d'une classe abstraite

Ensuite, il faut implémenter cette méthode abstraite dans nos différentes classes filles (Circle et Square).

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
package fr.koor.poo;

import java.awt.Point;

public class Circle extends Shape {

    // Begin of the class: attributes, constructors, properties, ...
    
        
    @Override 
    public double area() {
        return Math.PI * this.radius * this.radius;
    }

    @Override 
    public double perimeter() {
        return 2 * Math.PI * this.radius;
    }

}
Calcul du périmètre d'un cercle : a = 2*PI*r
 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
package fr.koor.poo;

import java.awt.Point;

public class Square extends Shape {

    // Begin of the class: attributes, constructors, properties, ...
    
        
    @Override 
    public double area() {
        return this.length * this.length;
    }

    @Override 
    public double perimeter() {
        return 4 * length;
    }

}
Calcul du périmètre d'un carré : 4 * longueur

Ensuite, testez vos méthodes de calcul de périmètre.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
package fr.koor.poo;

import java.awt.Point;
import java.util.ArrayList;

public class Start {

    public static void main( String [] args ) {
        
        ArrayList<Shape> shapes = new ArrayList<>();
        
        shapes.add( new Circle( new Point( 5, 5 ), 1 ) );
        shapes.add( new Square( new Point( 1, 1 ), -2 ) );
        shapes.add( new Circle() );
        
        for( Shape shape : shapes ) {
            System.out.println( shape.perimeter() );
        }
        
    }
    
}
Utilisation de nos méthodes de calcul de périmètre

Le concept d'interface

En fait, ce mécanisme n'est qu'une généralisation du concept de classe abstraite. Plus précisément, une interface est une classe dont toutes les méthodes sont abstraites. On n'a donc plus besoin de spécifier que les méthodes sont abstraites (signalées par le mot clé abstract), car elle doivent forcément l'être.

Au niveau de la syntaxe, on introduit une interface non plus par le mot clé class mais par le mot interface (comme vous vous en doutiez très certainement). Petite subtilité au passage, on n'hérite pas d'une interface, mais on l'implémente. En d'autres termes, on doit forcément fournir le code de toutes les méthodes de l'interface utilisée (sauf dans le cas d'une classe abstraite qui implémente un interface, ou bien d'une interface dérivée d'une autre). Le fait d'implémenter une interface se réalise grâce au mot clé implements. Voici quelques exemples.

 
interface I1 {
    void m();
}

abstract class C1 {
    void g();
}

class C2 extends C1 implements I1{
    void m(){
        // Le code de m
    }

    void g(){
        // Le code de g
    }
}

interface I2 extends I1 {
    void n();
}

abstract C3 implements I2 {
    void m(){ 
        // Le code de m();
    }
}

La différence essentielle entre une classe abstraite (dont toute les méthodes seraient abstraites) et une interface réside dans le fait que l'on ne peut hériter que d'une seule classe (héritage simple), alors que l'on peut implémenter plusieurs interfaces. C'est une solution pour simuler l'héritage multiple.

 
class MyClass extends MotherClass implements Interface1, Interface2 { ... }

Assistance proposée par Eclipse pour implémenter vos méthodes abstraites



Mise en oeuvre du concept d'héritage Implémentations d'interfaces