Interfaces et héritage multiple en Java

11 Sep 2019 11 Sep 2019 26063 vues ESSADDOUKI Mostafa 20 min de lecture
Introduction
1 Nouveautés de Java 11 2 Différences entre JDK, JRE et JVM 3 Structure d'un programme Java - Hello World 4 Mots clés et conventions de dénomination en Java 5 Types de données intégrés en Java 6 Les variables en Java 7 Classes enveloppe - Number, Integer, Double ... 8 Lire les entrées clavier en Java
Structures de contrôle
9 Les opérateurs en Java 10 Les structures conditionnelles en Java 11 Les boucles en Java 12 Instructions de contrôle de boucle - break, continue
Chaines de caractères
13 Les chaines en Java - API String 14 Les chaines en Java - StringBuffer et StringBuilder 15 Les expressions régulières en Java
Programmation OO
16 Objets et classes en Java 17 Modificateurs d'accès Java - public, private, protected et package 18 Méthodes et surcharge des méthodes en Java 19 les constructeurs en Java 20 L'héritage en Java 21 Classes abstraites en Java 22 Interfaces et héritage multiple en Java 23 Les classes imbriquées en Java 24 Les singletons en Java 25 Classes et méthodes génériques 26 Interface fonctionnelle et expressions Lambda en Java
Tableaux et collections
27 Les tableaux en Java 28 Classe Arrays - java.util.Arrays 29 Les listes dynamiques - java.util.ArrayList 30 Les listes chaînées en Java - java.util.LinkedList 31 HashSet en Java - java.util.HashSet 32 HashMap en Java - java.util.HashMap
Gestion des fichiers
33 Comprendre les fichiers informatiques 34 Utilisation des classes Path et Files en Java 35 Lecture et écriture dans un fichier en Java 36 Fichiers à accès aléatoire en Java
Gestion d'exceptions
37 Gestion d'exceptions en Java 38 Créez vos propres classes d'exception en Java
Programmation concurrente
39 Introduction à la programmation concurrente en Java - Multi-threads 40 classe java.lang.Thread 41 Synchronisation des threads en Java
Cours Java pour les débutants — Étape 22 sur 41

Interfaces en Java

  Prérequis

Maîtriser les classes, les objets et l'héritage simple. Comprendre les modificateurs d'accès et les classes abstraites. Connaître le polymorphisme et la notion de contrat en programmation orientée objet.

  Objectifs

Comprendre le concept d'interface comme contrat de service. Maîtriser la déclaration et l'implémentation d'interfaces. Utiliser les interfaces génériques avec paramètres de type. Appliquer les fonctionnalités modernes (méthodes default, static, private). Résoudre les conflits de méthodes par défaut. Distinguer interfaces et classes abstraites.

1. Qu'est-ce qu'une interface ?

Interface Une interface n'est pas une classe, mais un ensemble d'exigences pour les classes qui souhaitent s'y conformer. C'est un contrat qui spécifie quoi faire sans dire comment le faire.

Le fournisseur d'un service déclare : "Si votre classe se conforme à une interface particulière, le service sera exécuté".

  Exemple — L'interface Comparable

La méthode sort de la classe java.util.Arrays promet de trier un tableau d'objets, mais sous une condition : les objets doivent appartenir à des classes qui implémentent l'interface Comparable.

public interface Comparable<T> {
    int compareTo(T other);
}

Cela signifie que toute classe qui implémente Comparable doit avoir une méthode compareTo.

Modificateurs d'accès

Toutes les méthodes d'une interface sont implicitement public. Il n'est pas nécessaire de fournir le mot-clé public dans la déclaration. En revanche, lors de l'implémentation, vous devez déclarer la méthode public — sinon le compilateur applique l'accès package (plus restrictif) et génère une erreur.

2. Déclarer et implémenter une interface

2.1 Implémentation simple

Pour qu'une classe implémente une interface, deux étapes sont nécessaires :

  1. Déclarer que la classe implémente l'interface (mot-clé implements)
  2. Fournir une implémentation pour toutes les méthodes de l'interface

  Exemple 1 — Implémentation de Comparable

public class Professeur implements Comparable<Professeur> {
    private String nom;
    private double salaire;

    public Professeur(String nom, double salaire) {
        this.nom = nom;
        this.salaire = salaire;
    }

    @Override
    public int compareTo(Professeur p) {
        return Double.compare(this.salaire, p.salaire);
    }

    @Override
    public String toString() {
        return String.format("Professeur{nom='%s', salaire=%.2f}", nom, salaire);
    }
}

public class TestComparable {
    public static void main(String[] args) {
        Professeur[] profs = {
            new Professeur("Mostafa", 7000),
            new Professeur("Amal", 6500),
            new Professeur("Youssef", 7500)
        };

        Arrays.sort(profs);  // Tri utilisant compareTo

        for (Professeur p : profs) {
            System.out.println(p);
        }
    }
}
Sortie
Professeur{nom='Amal', salaire=6500.00}
Professeur{nom='Mostafa', salaire=7000.00}
Professeur{nom='Youssef', salaire=7500.00}
Interface générique Comparable<T>

L'utilisation de Comparable<Professeur> (interface générique) évite le cast explicite du paramètre. La méthode compareTo reçoit directement un Professeur, plus sûr et plus lisible que la version non générique avec Object.

2.2 Instanciation et polymorphisme

Une interface ne s'instancie pas

Vous ne pouvez jamais utiliser l'opérateur new pour instancier une interface :

Comparable x = new Comparable(); // ERREUR de compilation

En revanche, vous pouvez déclarer une variable d'interface qui référence un objet d'une classe implémentant cette interface :

Comparable x = new Professeur("Mostafa", 7000); // OK

L'opérateur instanceof permet de vérifier si un objet implémente une interface :

if (p instanceof Comparable) {
    // p peut être trié
}

3. Hiérarchie des interfaces — Héritage multiple

Héritage entre interfaces

Contrairement aux classes (héritage unique), une interface peut hériter de plusieurs interfaces avec le mot-clé extends.

  Exemple 2 — Héritage multiple d'interfaces

interface Ligne {
    double getLongueur();
    void afficher();
}

interface Dessinable {
    void dessiner();
    void colorier(String couleur);
}

// Une interface peut hériter de plusieurs interfaces
interface Polygone extends Ligne, Dessinable {
    int getNombreCotes();
    double calculerSurface();
}

// Une classe qui implémente Polygone doit implémenter TOUTES les méthodes
// de Polygone, Ligne ET Dessinable
class Triangle implements Polygone {
    private double cote1, cote2, cote3;
    private String couleur;

    public Triangle(double a, double b, double c) {
        this.cote1 = a; this.cote2 = b; this.cote3 = c;
    }

    @Override
    public double getLongueur() { return cote1 + cote2 + cote3; }

    @Override
    public void afficher() { System.out.println("Triangle : " + cote1 + ", " + cote2 + ", " + cote3); }

    @Override
    public void dessiner() { System.out.println("Dessin d'un triangle"); }

    @Override
    public void colorier(String couleur) { this.couleur = couleur; }

    @Override
    public int getNombreCotes() { return 3; }

    @Override
    public double calculerSurface() {
        double s = getLongueur() / 2;
        return Math.sqrt(s * (s - cote1) * (s - cote2) * (s - cote3));
    }
}
Pourquoi des interfaces plutôt qu'une classe abstraite ?

Une classe ne peut hériter que d'une seule classe (héritage unique). Si Comparable était une classe abstraite, une classe comme Professeur qui hérite déjà de Personne ne pourrait pas étendre Comparable. Les interfaces permettent d'implémenter plusieurs contrats (héritage multiple) tout en évitant les complexités du C++ (diamond problem, ambiguïtés).

4. Méthodes statiques dans les interfaces (Java 8+)

Depuis Java 8, vous pouvez ajouter des méthodes statiques aux interfaces. Elles sont accessibles via le nom de l'interface, comme pour une classe.

  Exemple 3 — Méthodes statiques

interface MathUtils {
    // Méthode statique
    static double puissance(double base, int exposant) {
        double resultat = 1;
        for (int i = 0; i < exposant; i++) resultat *= base;
        return resultat;
    }

    static int factoriel(int n) {
        if (n <= 1) return 1;
        return n * factoriel(n - 1);
    }
}

public class TestStatic {
    public static void main(String[] args) {
        // Appel via l'interface (pas besoin d'implémentation)
        System.out.println(MathUtils.puissance(2, 5));  // 32.0
        System.out.println(MathUtils.factoriel(6));     // 720
    }
}
Sortie
32.0
720

5. Méthodes privées dans les interfaces (Java 9+)

Java 9 a introduit les méthodes privées (statiques ou non) dans les interfaces. Elles servent de méthodes d'aide partagées entre méthodes default ou static, sans exposer ces détails d'implémentation aux classes utilisatrices.

  Exemple 4 — Méthodes privées (Java 9+)

interface Validation {

    // Méthode privée non statique — partagée entre méthodes default
    private boolean estVide(String s) {
        return s == null || s.trim().isEmpty();
    }

    // Méthode privée statique
    private static boolean estEmailValide(String email) {
        return email != null && email.contains("@") && email.indexOf('@') > 0;
    }

    // Méthodes default utilisant les méthodes privées
    default boolean validerNom(String nom) {
        if (estVide(nom)) {
            System.out.println("Erreur : le nom ne peut pas être vide");
            return false;
        }
        return true;
    }

    default boolean validerEmail(String email) {
        if (!Validation.estEmailValide(email)) {
            System.out.println("Erreur : email invalide");
            return false;
        }
        return true;
    }
}

class Utilisateur implements Validation {
    private String nom;
    private String email;

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

    public boolean valider() {
        return validerNom(nom) && validerEmail(email);
    }
}
Avantage des méthodes privées

Éviter la duplication de code entre méthodes default. Ces méthodes ne sont pas héritées et restent invisibles pour les classes implémentant l'interface.

6. Méthodes par défaut (default) — Java 8+

Méthode par défaut

Une méthode default est une méthode d'interface avec implémentation. Les classes qui implémentent l'interface peuvent hériter cette implémentation ou la redéfinir.

6.1 Pourquoi des méthodes par défaut ?

Avant Java 8, ajouter une nouvelle méthode dans une interface obligeait à modifier toutes les classes implémentant cette interface — une opération lourde et source d'erreurs. Les méthodes par défaut permettent d'étendre une interface rétrocompatiblement.

  Exemple 5 — Ajout rétrocompatible

interface Collection<T> {
    // ... méthodes existantes ...

    // Nouvelle méthode ajoutée avec une implémentation par défaut
    default void forEach(Consumer<? super T> action) {
        for (T element : this) {  // this est la collection
            action.accept(element);
        }
    }
}

Toutes les classes existantes implémentant Collection héritent automatiquement de forEach sans aucune modification.

6.2 Exemple concret — Interface Personne

interface Personne {
    String getNom();
    int getAge();

    // Méthode par défaut — calcul automatique de la catégorie d'âge
    default String getCategorieAge() {
        int age = getAge();
        if (age < 18) return "Mineur";
        if (age < 65) return "Adulte";
        return "Senior";
    }

    // Méthode par défaut — affichage standard
    default void afficher() {
        System.out.printf("Nom : %s, Age : %d ans, Catégorie : %s%n",
            getNom(), getAge(), getCategorieAge());
    }
}

class Professeur implements Personne {
    private String nom;
    private int age;
    private double salaire;

    public Professeur(String nom, int age, double salaire) {
        this.nom = nom;
        this.age = age;
        this.salaire = salaire;
    }

    @Override
    public String getNom() { return nom; }

    @Override
    public int getAge() { return age; }

    // La méthode getCategorieAge() est héritée de l'interface

    @Override
    public void afficher() {
        super.afficher();  // Appel à la méthode default
        System.out.println("Salaire : " + salaire);
    }
}
Sortie
Nom : Mostafa, Age : 42 ans, Catégorie : Adulte
Salaire : 7000.0

7. Résolution des conflits de méthodes par défaut

Le compilateur Java applique des règles strictes pour résoudre les conflits :

Règle n°1 — La classe gagne (Rule of Class Wins)

Si une superclasse fournit une méthode concrète, toute méthode par défaut d'une interface (même nom, mêmes paramètres) est ignorée. La compatibilité ascendante est préservée.

Règle n°2 — Conflit d'interfaces

Si deux interfaces (ou plus) fournissent une méthode par défaut avec la même signature, le compilateur signale une erreur. La classe doit redéfinir explicitement la méthode conflictuelle.

  Exemple 6 — Conflit entre interfaces

interface Personne {
    default String getIdentite() { return "Personne"; }
}

interface Employe {
    default String getIdentite() { return "Employé"; }
}

// ERREUR : Professeur hérite deux implémentations par défaut de getIdentite()
class Professeur implements Personne, Employe {
    // Obligation de redéfinir la méthode conflictuelle
    @Override
    public String getIdentite() {
        // Choix explicite : on peut appeler l'une des deux versions
        return Personne.super.getIdentite() + " / " + Employe.super.getIdentite();
    }
}

7.1 Règle de la classe gagne

  Exemple 7 — La superclasse l'emporte

class Animal {
    public String parler() {
        return "L'animal fait un bruit";
    }
}

interface Vocal {
    default String parler() {
        return "L'interface dit quelque chose";
    }
}

// La méthode de la classe Animal l'emporte sur la méthode default de Vocal
class Chien extends Animal implements Vocal {
    // Pas besoin de redéfinir — la version de Animal est utilisée
}

public class TestClasseGagne {
    public static void main(String[] args) {
        Chien chien = new Chien();
        System.out.println(chien.parler());  // "L'animal fait un bruit"
    }
}
Restrictions importantes

Vous ne pouvez jamais créer une méthode par défaut qui redéfinit une méthode de Object (toString(), equals(), hashCode()). En conséquence de la règle "la classe gagne", Object est toujours la superclasse ultime — sa méthode l'emporte toujours.

8. Interfaces vs Classes abstraites — Tableau comparatif

Critère Interface (Java 8+) Classe abstraite
Instanciation (new)}, Non}, Non (sauf sous-classe concrète)},
Attributs d'instance}, Uniquement static final (constantes)}, Oui (avec modificateurs d'accès)},
Constructeurs}, Non}, Oui},
Héritage multiple}, Oui (une classe peut implémenter plusieurs interfaces)}, Non (une classe ne peut étendre qu'une seule classe)},
Méthodes abstraites}, Oui (abstract implicite)}, Oui (abstract explicite)},
Méthodes concrètes (avant Java 8)}, Non}, Oui},
Méthodes default (Java 8+)}, Oui (avec implémentation)}, Non applicable (classe abstraite accepte toutes méthodes concrètes)},
Méthodes static (Java 8+)}, Oui}, Oui},
Méthodes private (Java 9+)}, Oui (partage de code entre méthodes default/static)}, Oui},
État partagé entre sous-classes}, Non (pas de champs d'instance)}, Oui (champs protected ou private avec accesseurs)},

9. Exemple complet — Interface moderne (Java 21)

  Exemple 8 — Interface complète avec toutes les fonctionnalités modernes

import java.util.*;
import java.util.function.*;

/**
 * Interface moderne illustrant toutes les fonctionnalités Java 8-21
 */
interface Bibliotheque<T> {

    // ─────────────────────────────────────────────────────────────────
    // 1. Méthodes abstraites — le contrat essentiel
    // ─────────────────────────────────────────────────────────────────
    void ajouter(T element);
    boolean supprimer(T element);
    List<T> rechercher(Predicate<T> critere);
    int taille();

    // ─────────────────────────────────────────────────────────────────
    // 2. Méthodes par défaut (default) — comportements communs
    // ─────────────────────────────────────────────────────────────────
    default boolean estVide() {
        return taille() == 0;
    }

    default void afficherTous() {
        System.out.println("=== Contenu de la bibliothèque ===");
        rechercher(e -> true).forEach(this::afficherElement);
    }

    default void afficherParCriteres(Predicate<T>... criteres) {
        Predicate<T> combine = Arrays.stream(criteres)
            .reduce(Predicate::and)
            .orElse(e -> true);
        rechercher(combine).forEach(this::afficherElement);
    }

    // ─────────────────────────────────────────────────────────────────
    // 3. Méthodes statiques — utilitaires
    // ─────────────────────────────────────────────────────────────────
    static <U> Bibliotheque<U> creerBibliothequeVide() {
        return new BibliothequeConcrete<>();
    }

    static <U extends Comparable<U>> void trierEtAfficher(List<U> elements) {
        elements.sort(Comparator.naturalOrder());
        elements.forEach(System.out::println);
    }

    // ─────────────────────────────────────────────────────────────────
    // 4. Méthodes privées (Java 9+) — factorisation interne
    // ─────────────────────────────────────────────────────────────────
    private void afficherElement(T element) {
        System.out.println("  • " + element);
    }

    private static <U> boolean estValide(U element) {
        return element != null;
    }

    // ─────────────────────────────────────────────────────────────────
    // 5. Méthode privée statique (Java 9+)
    // ─────────────────────────────────────────────────────────────────
    private static String formaterMessage(String titre) {
        return "=== " + titre.toUpperCase() + " ===";
    }
}

/**
 * Implémentation concrète de l'interface
 */
class BibliothequeConcrete<T> implements Bibliotheque<T> {
    private final List<T> elements = new ArrayList<>();

    @Override
    public void ajouter(T element) {
        if (element != null) {
            elements.add(element);
        }
    }

    @Override
    public boolean supprimer(T element) {
        return elements.remove(element);
    }

    @Override
    public List<T> rechercher(Predicate<T> critere) {
        return elements.stream().filter(critere).toList();
    }

    @Override
    public int taille() {
        return elements.size();
    }
}

/**
 * Classe de test
 */
public class TestBibliotheque {
    public static void main(String[] args) {
        // Utilisation des méthodes statiques
        Bibliotheque<String> biblio = Bibliotheque.creerBibliothequeVide();

        // Ajout d'éléments
        biblio.ajouter("Java moderne");
        biblio.ajouter("Programmation fonctionnelle");
        biblio.ajouter("Design Patterns");
        biblio.ajouter("Clean Code");

        // Utilisation des méthodes par défaut
        System.out.println("Est vide ? " + biblio.estVide());
        biblio.afficherTous();

        // Recherche avec critère
        List<String> resultats = biblio.rechercher(
            livre -> livre.contains("Java") || livre.contains("Pattern")
        );
        System.out.println("\nRecherche : " + resultats);

        // Méthode statique utilitaire
        List<Integer> nombres = Arrays.asList(5, 2, 8, 1, 9);
        System.out.println("\nTri de nombres :");
        Bibliotheque.trierEtAfficher(nombres);
    }
}
Sortie
Est vide ? false
=== Contenu de la bibliothèque ===
  • Java moderne
  • Programmation fonctionnelle
  • Design Patterns
  • Clean Code

Recherche : [Java moderne, Design Patterns]

Tri de nombres :
1
2
5
8
9

10. Pattern Matching avec les interfaces (Java 21)

  Complément — Pattern Matching pour instanceof et switch (Java 21)

Java 21 permet d'utiliser le pattern matching avec les interfaces, rendant le code plus concis et plus sûr.

interface Forme {
    double surface();
}

record Cercle(double rayon) implements Forme {
    @Override public double surface() { return Math.PI * rayon * rayon; }
}

record Rectangle(double longueur, double largeur) implements Forme {
    @Override public double surface() { return longueur * largeur; }
}

public class TestPatternMatching {
    static String decrire(Forme f) {
        // Pattern matching avec switch (Java 21)
        return switch (f) {
            case Cercle c    -> String.format("Cercle de rayon %.2f (surface : %.4f)", c.rayon(), c.surface());
            case Rectangle r -> String.format("Rectangle %.2f x %.2f (surface : %.4f)", r.longueur(), r.largeur(), r.surface());
            default          -> "Forme inconnue";
        };
    }

    public static void main(String[] args) {
        List<Forme> formes = List.of(new Cercle(5.0), new Rectangle(3.0, 4.0));
        for (Forme f : formes) {
            System.out.println(decrire(f));
        }
    }
}
Sortie
Cercle de rayon 5.00 (surface : 78.5398)
Rectangle 3.00 x 4.00 (surface : 12.0000)

11. Exercice

Système de paiement avec interfaces

Niveau : Intermédiaire

Concevoir un système de paiement extensible utilisant les interfaces Java (avec méthodes default et static).

Travail demandé

  1. Créer une interface Payable avec :
    • Méthode abstraite : double calculerMontant()
    • Méthode par défaut : void afficherFacture() — affiche le montant formaté
    • Méthode statique : Payable creerPaiementParDefaut(double montant) — retourne une instance par défaut
  2. Créer deux classes implémentant Payable :
    • Facture : attributs numero, montantHT, tva (calcul = montantHT * (1 + tva/100))
    • Abonnement : attributs nomService, tarifMensuel, mois (calcul = tarifMensuel * mois)
  3. Créer une classe utilitaire GestionPaiements avec :
    • Méthode statique double totalAPayer(Payable[] paiements)
    • Méthode par défaut (via une interface dédiée) ou statique pour afficher toutes les factures
  4. Tester avec un tableau contenant facture(s) et abonnement(s).
Sortie attendue
Sortie
=== Factures ===
Facture #F2025-001 : 1200.00 $
Abonnement Netflix : 47.94 $
Total à payer : 1247.94 $

  L'essentiel en bref

Synthèse — Interfaces en Java moderne (Java 21)
  • Une interface est un contrat qui spécifie quoi faire sans imposer comment — idéale pour le découplage et la réutilisabilité.
  • Héritage multiple : une interface peut extends plusieurs interfaces ; une classe peut implements plusieurs interfaces.
  • Méthodes abstraites : signature uniquement (implémentation obligatoire dans les classes concrètes).
  • Méthodes par défaut (default) (Java 8+) : fournissent une implémentation par défaut, évitent de casser le code existant lors de l'extension d'une interface.
  • Méthodes statiques (static) (Java 8+) : utilitaires associés à l'interface, appelés via Interface.nomMethode().
  • Méthodes privées (private) (Java 9+) : factorisation de code interne, invisibles pour les classes implémentant l'interface.
  • Interfaces fonctionnelles : interfaces avec une seule méthode abstraite — peuvent être utilisées avec des expressions lambda (ex: Predicate, Function, Consumer).
  • Règle de résolution des conflits : "la classe gagne" (la méthode d'une superclasse l'emporte sur une méthode default). En cas de conflit entre interfaces, la classe doit redéfinir explicitement la méthode.
Un peu d'histoire

Les interfaces existent en Java depuis la version 1.0 (1995). Java 8 (2014) a révolutionné les interfaces avec l'ajout des méthodes default et static, permettant l'évolution des API (comme l'ajout de stream() dans Collection) sans casser le code existant. Java 9 (2017) a ajouté les méthodes private pour le partage de code interne. Les interfaces fonctionnelles et les expressions lambda (Java 8) ont transformé Java en un langage supportant la programmation fonctionnelle. Le pattern matching (Java 21) simplifie le travail avec les hiérarchies d'interfaces en éliminant les casts explicites.

Sortie
// La sortie apparaîtra ici…
Prêt · Ctrl+Entrée pour exécuter

Discussion (0)

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Votre commentaire sera visible après modération.