L’exemple qui va suivre montre comment les interfaces permettent de définir un contrat commun que différentes classes peuvent implémenter, tout en gardant leur propre comportement car en java, il n’y a pas d’héritage multiple.
Exemple : Les animaux et leurs capacités
Imaginons que nous voulons modéliser différents animaux (chien, oiseau, poisson) et que certains d’entre eux savent nager, voler ou courir. Plutôt que de créer une hiérarchie de classes complexe (avec héritage multiple, impossible en Java), nous allons utiliser des interfaces pour définir ces capacités.
Une interface est un contrat qui définit des méthodes que les classes doivent implémenter.
Voici 3 interfaces pour nos capacités :
//interface pour les animaux qui savent nager public interface Nager { void nager(); }
// interface pour les animaux qui savent voler public interface Voler { void voler(); }
// pour les animaux qui savent courir public interface Courir { void courir(); }
Chaque classe d’animal implémente les interfaces qui correspondent à ses capacités.
public class Chien implements Courir, Nager { private String nom; public Chien(String nom) { this.nom = nom; } @Override public void courir() { System.out.println(nom + " court à quatre pattes !"); } @Override public void nager() { System.out.println(nom + " nage en battant des pattes ! Enfin, on espère"); } }
public class Oiseau implements Voler, Courir { private String nom; public Oiseau(String nom) { this.nom = nom; } @Override public void voler() { System.out.println(nom + " vole dans le ciel !"); } @Override public void courir() { System.out.println(nom + " saute en marchant !"); } }
public class Poisson implements Nager { private String nom; public Poisson(String nom) { this.nom = nom; } @Override public void nager() { System.out.println(nom + " nage en ondulant !"); } } ## Utilisation des interfaces >Dans la classe principale, nous pouvons maintenant traiter tous les animaux de manière polymorphique, en fonction de leurs capacités. ```java public class Main { public static void main(String[] args) { Chien monChien = new Chien("Rex"); Oiseau monOiseau = new Oiseau("Piaf"); Poisson monPoisson = new Poisson("Nemo"); // animaux qui savent courir Courir[] ceuxQuiCourent = {monChien, monOiseau}; for (Courir animal : ceuxQuiCourent) { animal.courir(); } // animaux qui savent nager Nager[] CeuxQuinagent = {monChien, monPoisson}; for (Nager animal : CeuxQuinagent) { animal.nager(); } // animaux qui savent voler Voler[] ceuxQuiVolent = {monOiseau}; for (Voler animal : ceuxQuiVolent) { animal.voler(); } } }
Rex court à quatre pattes ! Piaf saute en marchant ! Rex nage en battant des pattes ! Nemo nage en ondulant ! Piaf vole dans le ciel !
Si on crée une liste générique **ArrayList**, on ne peut pas directement appeler **nager()**, **voler()** ou **courir()** sur les éléments de la liste, car le compilateur ne sait pas si l'objet sait Nager, un Voler ou Courir.
Les solutions :
Vérifier le type de chaque animal avec instanceof et caster l’objet vers l’interface appropriée.
public class Main { public static void main(String[] args) { ArrayList<Animal> animaux = new ArrayList<>(); animaux.add(new Chien("Rex")); animaux.add(new Oiseau("Piaf")); animaux.add(new Poisson("Nemo")); for (Animal animal : animaux) { if (animal instanceof Nager) { ((Nageur) animal).nager(); // cast vers Nager } if (animal instanceof Voler) { ((Voleur) animal).voler(); // Cast vers Voler } if (animal instanceof Courir) { ((Coureur) animal).courir(); // Cast vers Courir } } } }
Avantages :
Inconvénients :
On peut ajouter une méthode dans la classe Animal qui délègue l’appel aux interfaces. Cela évite de faire des instanceof dans le code client. Personnellement, c’est celle que je préfère.
public abstract class Animal { public abstract void faireNager(); // vide par défaut public abstract void faireVoler(); public abstract void faireCourir(); } // Implémentation dans la classe Chien public class Chien extends Animal implements Nager, Courir { @Override public void faireNager() { this.nager(); // appelle la méthode de l'interface Nager } @Override public void faireVoler() { // ne fait rien, un chien ne vole pas quoique un jour peut-être.. avec la génétique } @Override public void faireCourir() { this.courir(); //appelle la méthode de l'interface Courir } @Override public void nager() { System.out.println("Le chien nage !"); } @Override public void courir() { System.out.println("Le chien court !"); } }
public class Main { public static void main(String[] args) { ArrayList<Animal> animaux = new ArrayList<>(); animaux.add(new Chien("Rex")); animaux.add(new Oiseau("Piaf")); animaux.add(new Poisson("Nemo")); // et là, ça marche dans tous les cas ! for (Animal animal : animaux) { animal.faireNager(); animal.faireVoler(); animal.faireCourir(); } } }
Inconvénients : On doit implémenter des méthodes vides pour les capacités non supportées (faireVoler() dans Chien).