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 ?
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.
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 :
- Déclarer que la classe implémente l'interface (mot-clé
implements) - 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);
}
}
}
Professeur{nom='Amal', salaire=6500.00}
Professeur{nom='Mostafa', salaire=7000.00}
Professeur{nom='Youssef', salaire=7500.00}
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
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
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));
}
}
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
}
}
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);
}
}
É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+
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);
}
}
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 :
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.
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"
}
}
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);
}
}
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));
}
}
}
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
Concevoir un système de paiement extensible utilisant les interfaces Java (avec méthodes default et static).
Travail demandé
- Créer une interface
Payableavec :- 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
- Méthode abstraite :
- Créer deux classes implémentant
Payable:Facture: attributsnumero,montantHT,tva(calcul = montantHT * (1 + tva/100))Abonnement: attributsnomService,tarifMensuel,mois(calcul = tarifMensuel * mois)
- Créer une classe utilitaire
GestionPaiementsavec :- 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
- Méthode statique
- Tester avec un tableau contenant facture(s) et abonnement(s).
=== Factures === Facture #F2025-001 : 1200.00 $ Abonnement Netflix : 47.94 $ Total à payer : 1247.94 $
import java.util.function.Consumer;
/**
* Interface Payable — contrat pour tout élément payable
*/
interface Payable {
// Méthode abstraite — à implémenter par chaque classe
double calculerMontant();
// Méthode par défaut — comportement commun
default void afficherFacture() {
System.out.printf("Montant : %.2f $%n", calculerMontant());
}
// Méthode statique — fabrique simple
static Payable creerPaiementParDefaut(double montant) {
return () -> montant;
}
// Méthode privée (Java 9+) — factorisation
private static String formatterMontant(double montant) {
return String.format("%.2f $", montant);
}
// Méthode statique utilitaire
static double total(Payable[] paiements) {
double somme = 0;
for (Payable p : paiements) {
somme += p.calculerMontant();
}
return somme;
}
}
/**
* Facture — implémentation de Payable
*/
record Facture(String numero, double montantHT, double tva) implements Payable {
public Facture {
if (montantHT <= 0) throw new IllegalArgumentException("Montant HT invalide");
if (tva < 0) throw new IllegalArgumentException("TVA invalide");
}
@Override
public double calculerMontant() {
return montantHT * (1 + tva / 100);
}
@Override
public void afficherFacture() {
System.out.printf("Facture #%s : %.2f $ (HT: %.2f, TVA: %.1f%%)%n",
numero, calculerMontant(), montantHT, tva);
}
}
/**
* Abonnement — implémentation de Payable
*/
record Abonnement(String nomService, double tarifMensuel, int mois) implements Payable {
public Abonnement {
if (tarifMensuel <= 0) throw new IllegalArgumentException("Tarif mensuel invalide");
if (mois <= 0) throw new IllegalArgumentException("Nombre de mois invalide");
}
@Override
public double calculerMontant() {
return tarifMensuel * mois;
}
@Override
public void afficherFacture() {
System.out.printf("Abonnement %s : %.2f $ (%d mois à %.2f $/mois)%n",
nomService, calculerMontant(), mois, tarifMensuel);
}
}
/**
* Interface étendue avec méthode par défaut pour afficher un tableau
*/
interface GestionPaiements {
default void afficherTous(Payable[] paiements) {
System.out.println("=== Liste des paiements ===");
for (Payable p : paiements) {
p.afficherFacture();
}
}
static void afficherAvecStyle(Payable[] paiements, String titre) {
System.out.println("╔══════════════════════════════════════╗");
System.out.printf("║ %-34s ║%n", titre);
System.out.println("╠══════════════════════════════════════╣");
for (Payable p : paiements) {
System.out.printf("║ %-36s ║%n",
String.format("%.2f $", p.calculerMontant()));
}
System.out.println("╚══════════════════════════════════════╝");
}
}
/**
* Classe de test
*/
public class TestPaiement implements GestionPaiements {
public static void main(String[] args) {
// Création des paiements
Payable[] paiements = {
new Facture("F2025-001", 1000.0, 20.0), // 1200 $
new Abonnement("Netflix", 15.98, 3), // 47.94 $
new Abonnement("Spotify", 9.99, 6), // 59.94 $
Payable.creerPaiementParDefaut(50.0) // 50 $ (paiement ponctuel)
};
// Utilisation de l'instance de test pour la méthode default
TestPaiement gestion = new TestPaiement();
gestion.afficherTous(paiements);
// Affichage du total
System.out.printf("%n%s Total à payer : %.2f $ %s%n",
"=".repeat(15), Payable.total(paiements), "=".repeat(15));
// Affichage stylisé
System.out.println();
GestionPaiements.afficherAvecStyle(paiements, "DÉTAIL DES PAIEMENTS");
}
}
=== Liste des paiements === Facture #F2025-001 : 1200.00 $ (HT: 1000.00, TVA: 20.0%) Abonnement Netflix : 47.94 $ (3 mois à 15.98 $/mois) Abonnement Spotify : 59.94 $ (6 mois à 9.99 $/mois) Montant : 50.00 $ =============== Total à payer : 1357.88 $ =============== ╔══════════════════════════════════════╗ ║ DÉTAIL DES PAIEMENTS ║ ╠══════════════════════════════════════╣ ║ 1200.00 $ ║ ║ 47.94 $ ║ ║ 59.94 $ ║ ║ 50.00 $ ║ ╚══════════════════════════════════════╝
- Interface
Payable: définit le contrat avec méthode abstraitecalculerMontant(), méthode par défautafficherFacture(), méthode statiquecreerPaiementParDefaut()et méthode privéeformatterMontant(). recordFacture et Abonnement : implémentations concises et immutables de l'interface (Java 16+).- Méthode statique
total()dans l'interface : utilitaire de calcul directement accessible viaPayable.total(). - Interface
GestionPaiements: démontre qu'une interface peut contenir des méthodes par défaut (afficherTous()) et statiques (afficherAvecStyle()). - Polymorphisme : le tableau
Payable[]contient des instances de types différents — la boucle appelle la bonne implémentation decalculerMontant().
L'essentiel en bref
- 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
extendsplusieurs interfaces ; une classe peutimplementsplusieurs 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 viaInterface.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.
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.
Discussion (0)
Soyez le premier à laisser un commentaire !
Laisser un commentaire
Votre commentaire sera visible après modération.