Interfaces et héritage multiple en Java
Dans le langage de programmation Java, une interface n'est pas une classe, mais un ensemble d'exigences pour les classes qui souhaitent s'y conformer.
En règle générale, le fournisseur de certains services déclare: «Si votre classe se conforme à une interface particulière, le service sera exécuté.»
Voyons un exemple concret. 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 { int compareTo(Object other); }
Cela signifie que toute classe qui implémente l'interface Comparable doit avoir une méthode compareTo et que la méthode doit prendre un paramètre Object et renvoyer un entier.
Toutes les méthodes d'une interface sont automatiquement publiques. Pour cette raison, il n'est pas nécessaire de fournir le mot clé public lors de la déclaration d'une méthode dans une interface.
Avant Java 8, les méthodes n'étaient jamais implémentées dans les interfaces. Il est maintenant possible de fournir des méthodes simples dans les interfaces. Bien sûr, ces méthodes ne peuvent pas faire référence à des champs d'instance, les interfaces n'en ont pas.
Fournir des champs d'instance et des méthodes qui fonctionnent sur eux est la tâche des classes qui implémentent l'interface. Vous pouvez considérer une interface comme une classe abstraite sans champs d'instance. Cependant, il existe quelques différences entre ces deux concepts
Pour qu'une classe implémente une interface, vous devez suivre deux étapes:
- Vous déclarez que votre classe a l'intention d'implémenter l'interface donnée.
- Vous fournissez des définitions pour toutes les méthodes de l'interface.
Pour déclarer qu'une classe implémente une interface, utilisez le mot clé implements.
public class Professeur implements Comparable
Bien entendu, la classe Professeur doit désormais fournir la méthode compareTo. Supposons que nous voulions comparer les professeurs par leur salaire. Voici une implémentation de la méthode compareTo
public int compareTo(Object AutreProf) { Professeur p= (Professeur)AutreProf; return Double.compare(salaire, p.salaire); }
Ici, nous utilisons la méthode statique Double.compare qui renvoie une valeur négative si le premier argument est inférieur au deuxième argument, 0 si elles sont égales et une valeur positive sinon.
public class Professeur implements Comparable{ private String nom; private double salaire; public Professeur(String nom, double salaire){ this.nom=nom; this.salaire=salaire; } public int compareTo(Object AutreProf) { Professeur p= (Professeur)AutreProf; return Double.compare(salaire, p.salaire); } }
Nous pouvons faire un peu mieux en fournissant un paramètre de type pour l'interface générique 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; } public int compareTo(Professeur p) { return Double.compare(salaire, p.salaire); } }
Notez que le cast du paramètre object a disparu. (Professeur p= (Professeur)AutreProf;)
Propriétés des interfaces
Les interfaces ne sont pas des classes. En particulier, vous ne pouvez jamais utiliser l'opérateur new pour instancier une interface:
x= new Comparable() // Erreur
Cependant, même si vous ne pouvez pas construire d’objets d’interface, vous pouvez quand même déclarer des variables d’interface.
Comparable x;
Une variable d'interface doit faire référence à un objet d'une classe qui implémente l'interface
// OK à condition que Professeur implémente Comparable Comparable x = new Professeur("Mostafa",7000);
Ensuite, tout comme vous utilisez instanceof pour vérifier si un objet appartient à une classe spécifique, vous pouvez utiliser instanceof pour vérifier si un objet implémente une interface:
if (p instanceof Comparable){ }
Semblables aux classes, les interfaces peuvent hériter de d’autres interfaces.
Exemple 1 :
interface Ligne { // membres de l'interface Ligne } interface Polygone extends Ligne { // membres de l'interface Ligne et Polygone }
Dans l'exemple ci-dessus, l'interface Polygone hérite de l'interface Ligne. Maintenant, si une classe implémente Polygone, elle devrait fournir des implémentations pour toutes les méthodes abstraites de Ligne et Polygone.
Notez qu'une interface peut hériter de plusieurs interfaces similaires à une classe implémentant plusieurs interfaces. Par exemple,
Exemple 2 :
interface A { ... } interface B { ... } Interface C extends A, B { ... }
Interfaces et classes abstraites
Vous vous demandez peut-être pourquoi les concepteurs du langage de programmation Java ont pris la peine d'introduire le concept d'interfaces. Pourquoi l’interface Comparable ne peut-elle pas être simplement une classe abstraite:
abstract class Comparable // pourquoi pas? { public abstract int compareTo(Object autre); }
La classe Professeur alors hériterait simplement cette classe abstraite et fournirait la méthode compareTo
Malheureusement, l’utilisation d’une classe de base abstraite pour exprimer une propriété générique pose un problème majeur. Une classe ne peut hériter que d'une seule classe. Supposons que la classe Professeur hérite déjà d'une classe différente, par exemple, Personne. Ensuite, il ne peut pas hériter d’une deuxième classe.
class Professeur extends Personne, Comparable { // Erreur }
Mais chaque classe peut implémenter autant d'interfaces qu'elle le souhaite:
class Professeur extends Personne implements Comparable { // Ok }
D'autres langages de programmation, en particulier C ++, permettent à une classe d'avoir plusieurs super-classes. Cette fonctionnalité s'appelle l'héritage multiple. Les concepteurs de Java ont choisi de ne pas prendre en charge l'héritage multiple, car cela rend le langage très complexe (comme en C ++) ou moins efficace (comme dans Eiffel).
Au lieu de cela, les interfaces offrent la plupart des avantages d'un héritage multiple tout en évitant les complexités et les inefficacités.
Méthodes statiques et privées
Depuis Java 8, Vous pouvez ajouter des méthodes statiques aux interfaces. Il n'y a jamais eu de raison technique pour interdire cette pratique. Cela semblait simplement contraire à l'esprit des interfaces en tant que SPÉCIFICATIONS abstraites.
Semblable à une classe, nous pouvons accéder aux méthodes statiques d'une interface en utilisant ses références. Par exemple
Polygone.methodeStatic();
De plus, les interfaces prennent en charge les méthodes privées avec la version Java 9. Vous pouvez désormais utiliser des méthodes privées et des méthodes statiques privées dans les interfaces.
Puisque vous ne pouvez pas instancier les interfaces, les méthodes privées sont utilisées comme méthodes d'aide qui fournissent le support à d'autres méthodes dans les interfaces.
Méthodes par défaut dans les interfaces (default)
Depuis Java 8, des méthodes avec implémentation (méthodes par défaut) ont été introduites dans une interface.
Pour déclarer les méthodes par défaut dans les interfaces, nous utilisons le mot clé default.
Exemple 3 :
default void afficher() { // corps de la méthode (implémentation) }
Pourquoi des méthodes par défaut?
Prenons un scénario pour comprendre pourquoi les méthodes par défaut sont introduites en Java.
Supposons que nous devions ajouter une nouvelle méthode dans une interface.
Nous pouvons ajouter facilement la méthode dans notre interface sans implémentation. Cependant, ce n'est pas la fin de l'histoire. Toutes nos classes qui implémentent cette interface doivent fournir une implémentation pour la méthode.
Si un grand nombre de classes implémentaient cette interface, nous devons suivre toutes ces classes et y apporter des modifications. Ce n'est pas seulement fastidieux, mais aussi une source d'erreurs.
Pour résoudre ce problème, Java a introduit les méthodes par défaut. Les méthodes par défaut sont héritées comme les méthodes ordinaires.
Résoudre les conflits de méthodes par défaut
Que se passe-t-il si la même méthode est définie comme méthode par défaut dans une interface, puis à nouveau comme méthode d'une superclasse ou d'une autre interface ?
Des langages tels que Scala et C++ ont des règles complexes pour résoudre de telles ambiguïtés. Heureusement, les règles en Java sont beaucoup plus simples. Les voici :
- Les superclasses gagnent. Si une super-classe fournit une méthode concrète, les méthodes par défaut portant le même nom et les mêmes types de paramètres sont simplement ignorées.
- Conflit d'interfaces. Si une interface fournit une méthode par défaut et qu'une autre interface contient une méthode avec le même nom et les mêmes types de paramètres (par défaut ou non), vous devez résoudre le conflit en surchargeant cette méthode.
Regardons la deuxième règle. Considérons deux interfaces avec une méthode getName:
interface Personne { default String getName() { return ""; }; } interface Employe { default String getName() { return getClass().getName(); } }
Que se passe-t-il si vous formez une classe qui implémente les deux?
class Professeur implements Personne, Employe{ }
La classe hérite deux méthodes getName() incohérentes fournies par les interfaces Personne et Employe. Au lieu de choisir l’une sur l’autre, le compilateur Java signale une erreur et laisse au programmeur le soin de résoudre l’ambiguïté.
Fournissez simplement une méthode getName dans la classe Professeur. Dans cette méthode, vous pouvez choisir l'une des deux méthodes en conflit, comme ceci:
class Professeur implements Personne, Employe{ default String getName() { return Personne.super.getName(); } }
Supposons maintenant que l'interface Employe ne fournit pas d'implémentation par défaut pour getName:
interface Employe { default String getName(); }
La classe Professeur peut-elle hériter de la méthode par défaut de l'interface Personne? Cela peut sembler raisonnable, mais les concepteurs de Java ont opté pour l’uniformité. Peu importe la façon dont deux interfaces sont en conflit. Si au moins une interface fournit une implémentation, le compilateur signale une erreur et le programmeur doit résoudre l'ambiguïté.
Considérons maintenant une classe qui hérite d'une super-classe et implémente une interface, en héritant de la même méthode des deux. Par exemple, supposons que Personne soit une classe et que Professeur soit définie comme :
class Professeur extends Personne implements Employe{ ... }
Dans ce cas, seule la méthode superclass compte et toute méthode par défaut de l'interface est simplement ignorée. Dans notre exemple, Professeur hérite de la méthode getName() de Personne et il est indifférent que l’interface Employe fournisse ou non une implémentation par défaut pour getName(). C'est la règle de la «classe gagne».
La règle «classe gagne» garantit la compatibilité avec Java 7. Si vous ajoutez des méthodes par défaut à une interface, cela n'a aucun effet sur le code qui fonctionnait avant les méthodes par défaut.