Rappel de Java - héritage / classes abstraites / interfaces

par Tom
le 15/12/2019

Sommaire


Temps de lecture estimé : 45min-1h


Héritage

En programmation orientée objet, on est souvent amenés à créer des classes liées entre elles, par une relation de parenté ou de caractéristique commune.

Exemple : On veut créer des classes pouvant décrire :

  • une personne quelconque
  • un client de l'entreprise
  • un employé de l'entreprise

On pourrait tout à fait créer trois classes différentes, mais on pourrait aussi remarquer que ces trois notions partagent des informations. Elles possèdent toutes un nom, un prénom, une adresse e-mail, etc.

La programmation orientée objet (notée POO) nous permet alors de faire ceci :

  • une classe Personne contenant les informations communes à tout le monde
    • une classe Client contenant les informations spécifiques aux clients
    • une classe Employe contenant les informations spécifiques aux employés

où les classes Client et Employe héritent de la classe Personne

Sur un schéma UML, un tiret (-) devant une ligne signifie que l'élément est privé (private).

Sur un schéma UML, l'héritage est noté par un trait continu terminé par une flèche vide de la classe fille à la classe mère.

En Java, cela se traduit par l'utilisation du mot-clé extends :

class A extends B

signifie que la classe A hérite de la classe B.

Avant de défiler plus bas, essayez de reproduire les classes du schéma ci-dessus en Java.

Voici le résultat :

public class Personne
{
    String nom;
    String prenom;
    String email;
}

public class Client extends Personne
{
    String nomEntreprise;
}

public class Employe extends Personne
{
    double salaire;
}

On peut rajouter le constructeur ainsi que les getters de Personne :

public class Personne
{
    String nom;
    String prenom;
    String email;

    public Personne(String nom, String prenom, String email)
    {
        this.nom = nom;
        this.prenom = prenom;
        this.email = email;
    }

    public String getNom()
    {
        return this.nom;
    }

    public String getPrenom()
    {
        return this.prenom;
    }

    public String getEmail()
    {
        return this.email;
    }
}

Essayons maintenant de définir le constructeur de Client :

public class Client extends Personne
{
    String nomEntreprise;

    public Client(String nomEntreprise)
    {
        this.nomEntreprise = nomEntreprise;
    }
}

Si on essaie de compiler le code ci-dessus, on obtiendra une erreur. Et pour cause : la classe Personne possède un constructeur, mais le constructeur de Client n'appelle pas ce dernier. Or, pour créer un classe il faut appeler son constructeur. Il faut donc que le constructeur de Client appelle aussi celui de la classe mère (Personne).

On utilise pour cela la notation super :

public class Client extends Personne
{
    String nomEntreprise;

    public Client(String nom, String prenom, String email, String nomEntreprise)
    {
        super(nom, prenom, email); // constructeur de la classe mère (Personne)

        this.nomEntreprise = nomEntreprise;
    }
}

Idem pour la classe Employe :

public class Employe extends Personne
{
    double salaire;

    public Employe(String nom, String prenom, String email, double salaire)
    {
        super(nom, prenom, email); // constructeur de la classe mère (Personne)

        this.salaire = salaire;
    }
}

Le code final :

public class Personne
{
    String nom;
    String prenom;
    String email;

    public Personne(String nom, String prenom, String email)
    {
        this.nom = nom;
        this.prenom = prenom;
        this.email = email;
    }

    public String getNom()
    {
        return this.nom;
    }

    public String getPrenom()
    {
        return this.prenom;
    }

    public String getEmail()
    {
        return this.email;
    }
}

public class Client extends Personne
{
    String nomEntreprise;

    public Client(String nom, String prenom, String email, String nomEntreprise)
    {
        super(nom, prenom, email); // constructeur de la classe mère (Personne)

        this.nomEntreprise = nomEntreprise;
    }

    public String getNomEntreprise()
    {
        return this.nomEntreprise;
    }
}

public class Employe extends Personne
{
    double salaire;

    public Employe(String nom, String prenom, String email, double salaire)
    {
        super(nom, prenom, email); // constructeur de la classe mère (Personne)

        this.salaire = salaire;
    }

    public double getSalaire()
    {
        return this.salaire;
    }
}

Classes abstraites

Parfois, il arrive qu'on veuille définir une classe mère (comme Personne plus haut) mais qui ne forme qu'un concept abstrait et pas un objet complet à part entière.

Par exemple, on pourrait vouloir définir une classe FigureGeometrique et des classes Carre, Rond, etc. Pour autant, "créer une figure géométrique" n'a pas vraiment de sens, car on doit pour cela connaître la nature de la figure (forme, taille, etc).

Ainsi, la classe FigureGeometrique sera abstraite, c'est-à-dire qu'elle ne peut pas être utilisée seule, il faut obligatoirement utiliser une classe fille.

Sur un schéma UML, un plus (+) devant une ligne signifie que l'élément est public (public).

Sur un schéma UML, un élément (classe ou méthode) abstrait est noté en italique.

En Java, un élément abstrait est marqué abstract.

public abstract class ClasseAbstraite
{
    public String maFonctionPasAbstraite();
    public abstract int maFonctionAbstraite();
}

correspond au schéma suivant :

Essayez de faire en Java les classes FigureGeometrique, Carre et Rond (avec les constructeurs, les getters et les méthodes).

Correction :

public abstract class FigureGeometrique
{
    public abstract double getSurface();
    public abstract double getPerimetre();
}

public class Carre extends FigureGeometrique
{
    double longueurCote;

    public Carre(double cote)
    {
        this.longueurCote = cote;
    }

    public double getLongueurCote()
    {
        return this.longueurCote;
    }

    public double getSurface()
    {
        return this.longueurCote * this.longueurCote;
    }

    public double getPerimetre()
    {
        return this.longueurCote * 4;
    }
}

public class Rond extends FigureGeometrique
{
    double rayon;

    public Rond(double rayon)
    {
        this.rayon = rayon;
    }

    public double getRayon()
    {
        return this.rayon;
    }

    public double getDiametre()
    {
        return 2 * this.rayon;
    }

    public double getSurface()
    {
        return Math.PI * this.rayon * this.rayon;
    }

    public double getPerimetre()
    {
        return 2 * Math.PI * this.rayon;
    }
}

En Java, la classe Math fournit des fonctions et des constantes mathématiques. Math.PI contient par exemple la valeur de π à 15 décimales.


Interfaces

Nous avons vu que l'héritage de classe marque un lien de parenté direct :

  • un Rond est une FigureGeometrique, c'est sa nature profonde, il y a un lien parent-enfant entre les deux classes.
  • idem pour un Client et une Personne.

Les interfaces permettent au contraire de marquer la simple possession d'une caractéristique.

Exemple de cas de figure : vous voulez pouvoir comparer deux objets quelconques. Naturellement, il n'y a pas de moyen prédéfini pour le faire, tout simplement parce que tous les objets ne se comparent pas. Mais prenons le cas d'objets pouvant être comparés. Comment pourrions-nous formaliser ça dans le code ?

Nous avons les classes suivantes :

En Java, <T> signifie que la classe ou l'interface est paramétrée (ou générique), ici par exemple l'interface Comparable prend en paramètre un type, c'est le type avec lequel une classe peut se comparer.
Ici, la classe Rond va implémenter l'interface Comparable<Rond>, cela signifie qu'un rond ne peut être comparé qu'à un autre rond.

Là où l'héritage se note avec extends, l'implémentation se note avec implements.

public interface MonInterface
{
    int maFonction();
}

public class MaClasse implements MonInterface
{
    public int maFonction()
    {
        return 123;
    }
}

correspond au schéma suivant :

Sur un schéma UML, l'implémentation est noté par un trait discontinu terminé par une flèche vide de la classe à l'interface.

En Java, quand on déclare une interface, les méthodes sont par défaut publiques, donc il n'est pas nécessaire de préciser public devant chaque ligne.

Essayez d'écrire l'interface et les classes du schéma.

Pas la peine de réécrire toutes les fonctions des classes Employe et Rond, ne faites que les fonctions liées à l'interface.

Correction :

public interface Comparable<T>
{
    boolean estPlusGrandQue(T autre);
}

public class Employe extends Personne implements Comparable<Employe>
{
    ...

    public boolean estPlusGrandQue(Employe autre)
    {
        return this.salaire > autre.salaire;
    }

    ...
}

public class Rond extends FigureGeometrique implements Comparable<Rond>
{
    ...

    public boolean estPlusGrandQue(Rond autre)
    {
        return this.rayon > autre.rayon;
    }

    ...
}

Pour plus d'informations sur l'UML, consultez l'article complet.

Merci à Kylian pour avoir signalé quelques erreurs