web-dev-qa-db-fra.com

Pourquoi Java n'autorise pas l'héritage multiple, mais autorise la conformité à plusieurs interfaces avec des implémentations par défaut

Je ne demande pas ceci -> Pourquoi n'y a-t-il pas d'héritage multiple en Java, mais l'implémentation d'interfaces multiples est autorisée?

En Java, l'héritage multiple n'est pas autorisé, mais après Java 8, les interfaces peuvent avoir des méthodes par défaut (peut implémenter les méthodes elles-mêmes), tout comme les classes abstraites, dans lequel l'héritage multiple devrait également être autorisé.

interface TestInterface 
{ 
    // abstract method 
    public void square(int a); 

    // default method 
    default void show() 
    { 
      System.out.println("Default Method Executed"); 
    } 
} 
47
Asanka

Les choses ne sont pas si simples.
Si une classe implémente plusieurs interfaces définissant des méthodes par défaut avec la même signature, le compilateur vous obligera à remplacer cette méthode pour la classe.

Par exemple avec ces deux interfaces:

public interface Foo {
    default void doThat() {
        // ...
    }
}

public interface Bar {    
    default void doThat() {
        // ...
    }       
}

Il ne compilera pas:

public class FooBar implements Foo, Bar{
}

Vous devez définir/remplacer la méthode pour supprimer l'ambiguïté.
Vous pouvez par exemple déléguer à la mise en oeuvre Bar telle que:

public class FooBar implements Foo, Bar{    
    @Override
    public void doThat() {
        Bar.super.doThat();
    }    
}

ou déléguer à l'implémentation Foo tel que:

public class FooBar implements Foo, Bar {
    @Override
    public void doThat() {
        Foo.super.doThat();
    }
}

ou encore définir un autre comportement:

public class FooBar implements Foo, Bar {
    @Override
    public void doThat() {
        // ... 
    }
}

Cette contrainte montre que Java n'autorise pas l'héritage multiple, même pour les méthodes d'interface par défaut.


Je pense que nous ne pouvons pas appliquer la même logique pour plusieurs héritages, car plusieurs problèmes pourraient se produire, dont les principaux sont:

  • redéfinir/supprimer l'ambiguïté d'une méthode dans les deux classes héritées peut entraîner des effets secondaires et modifier le comportement général des classes héritées si elles s'appuient sur cette méthode en interne. Avec les interfaces par défaut, ce risque existe également, mais il devrait être beaucoup moins rare, car les méthodes par défaut ne sont pas conçues pour introduire des traitements complexes tels que plusieurs invocations internes dans la classe ou pour avoir un état (les interfaces ne peuvent en effet pas être le champ d'instance de l'hôte).
  • comment hériter de plusieurs champs? Et même si la langue le permettait, vous auriez exactement le même problème que celui cité précédemment: effet secondaire dans le comportement de la classe héritée: un champ int foo Défini dans un A et B la classe que vous voulez sous-classer n'a pas la même signification et le même but.
36
davidxxx

Les concepteurs de langage ont déjà pensé à cela, alors ces choses sont appliquées par le compilateur. Donc si vous définissez:

interface First {
    default void go() {
    }
}

interface Second {
    default void go() {
    }
}

Et vous implémentez une classe pour les deux interfaces:

static class Impl implements First, Second {

}

vous obtiendrez une erreur de compilation; et vous auriez besoin de remplacer go pour ne pas créer d'ambiguïté autour de cela.

Mais vous pourriez penser que vous pouvez tromper le compilateur ici, en faisant:

interface First {
    public default void go() {
    }
}

static abstract class Second {
    abstract void go();
}

static class Impl extends Second implements First {
}

Vous pourriez penser que First::go fournit déjà une implémentation pour Second::go et ça devrait aller. Ceci est trop pris en charge, donc cela ne compile pas non plus.

JLS 9.4.1.3: De même, quand un résumé et une méthode par défaut avec des signatures correspondantes sont hérités, nous produisons une erreur. Dans ce cas, il serait possible de donner la priorité à l'une ou à l'autre - nous pourrions peut-être supposer que la méthode par défaut fournit également une implémentation raisonnable pour la méthode abstraite. Mais ceci est risqué car, mis à part le nom et la signature coïncidents, nous n’avons aucune raison de penser que la méthode par défaut se comporte de manière cohérente avec le contrat de la méthode abstraite - la méthode par défaut n’a peut-être même pas existé lorsque la sous-interface a été développée à l’origine.. Dans cette situation, il est plus sûr de demander à l'utilisateur d'affirmer activement que l'implémentation par défaut est appropriée (via une déclaration de substitution).

Le dernier point que je voudrais apporter, pour confirmer que l'héritage multiple n'est pas autorisé, même avec les nouveaux ajouts en Java, est que les méthodes statiques des interfaces ne sont pas héritées. les méthodes statiques sont héritées par défaut:

static class Bug {
    static void printIt() {
        System.out.println("Bug...");
    }
}

static class Spectre extends Bug {
    static void test() {
        printIt(); // this will work just fine
    }
}

Mais si nous changeons cela pour une interface (et vous pouvez implémenter plusieurs interfaces, contrairement aux classes):

interface Bug {
    static void printIt() {
        System.out.println("Bug...");
    }
}

static class Spectre implements Bug {
    static void test() {
        printIt(); // this will not compile
    }
}

Maintenant, ceci est interdit par le compilateur et JLS aussi:

JLS 8.4.8: Une classe n'hérite pas des méthodes statiques de ses superinterfaces.

25
Eugene

Java n'autorise pas l'héritage multiple pour les champs. Cela serait difficile à prendre en charge dans la machine virtuelle Java, car vous ne pouvez avoir que des références au début d'un objet contenant l'en-tête, et non à des emplacements de mémoire arbitraires.

Dans Oracle/Openjdk, les objets ont un en-tête suivi des champs de la plus grande classe, puis de la deuxième classe, etc. Ce serait un changement important de permettre aux champs d'une classe d'apparaître à différents décalages par rapport à l'en-tête. d'un objet pour différentes sous-classes. Les références d'objet les plus probables devraient devenir une référence à l'en-tête d'objet et une référence aux champs pour le prendre en charge.

13
Peter Lawrey

Cela est principalement lié au "problème des diamants", je pense. À l'heure actuelle, si vous implémentez plusieurs interfaces avec la même méthode, le compilateur vous oblige à remplacer la méthode que vous souhaitez implémenter, car elle ne sait pas laquelle utiliser. J'imagine que les créateurs de Java ont voulu supprimer ce problème à l'époque où les interfaces ne pouvaient pas utiliser les méthodes par défaut. Maintenant, ils ont eu une idée: il vous pouvez toujours utiliser celles-ci comme interfaces fonctionnelles dans les flux/expressions lambda et utiliser leurs méthodes par défaut pour le traitement. Vous ne pouvez pas le faire avec des classes, mais le problème de diamant existe toujours. C'est ma conjecture :)

4
Mershel

Les principaux problèmes liés à l'héritage multiple sont l'ordre (pour la substitution et les appels à super), les champs et les constructeurs; les interfaces n'ont pas de champs ni de constructeurs, elles ne causent donc pas de problèmes.

Si vous regardez d'autres langues, elles se divisent généralement en deux grandes catégories:

  1. Langues à héritage multiple plus quelques fonctionnalités pour distinguer les cas particuliers: héritage virtuel [C++], appels directs à tous les superconstructeurs de la classe la plus dérivée [C++], linéarisation des superclasses [Python], règles complexes pour super [Python], etc.

  2. Langues avec un concept différent, généralement appelées interfaces, traits, mixins, modules, etc. qui imposent certaines limitations tels que: pas de constructeurs [Java] ou pas de constructeurs avec paramètres [Scala jusqu’à tout récemment], pas de champs mutables [Java], règles spécifiques de substitution (par exemple, les mixins ont priorité sur les classes de base [Ruby] afin que vous puissiez les inclure quand vous avez besoin un tas de méthodes utilitaires), etc. Java est devenu un langage comme celui-ci.

Pourquoi simplement en refusant les champs et les constructeurs, vous résolvez de nombreux problèmes liés à l'héritage multiple?

  • Vous ne pouvez pas avoir de champs dupliqués dans les classes de base dupliquées.
    • La hiérarchie de classe principale est toujours linéaire.
  • Vous ne pouvez pas construire vos objets de base dans le mauvais sens.
    • Imaginez si Object avait des champs publics/protégés et si toutes les sous-classes avaient des constructeurs définissant ces champs. Lorsque vous héritez de plusieurs classes (toutes dérivées d'Object), laquelle doit définir les champs? Le dernier cours? Ils deviennent des frères et soeurs dans la hiérarchie, ils ne se connaissent donc pas. Devez-vous avoir plusieurs copies de Object pour éviter cela? Toutes les classes interagiraient-elles correctement?
  • N'oubliez pas que les champs dans Java ne sont pas virtuels (ils peuvent être remplacés), ils constituent simplement un stockage de données.
    • Vous pouvez créer un langage dans lequel les champs se comportent comme des méthodes et peuvent être remplacés (le stockage réel sera toujours privé), mais ce serait un changement beaucoup plus important et ne s'appellerait probablement pas Java plus .
  • Les interfaces ne peuvent pas être instanciées par elles-mêmes.
    • Vous devriez toujours les combiner avec une classe concrète. Cela élimine le besoin de constructeurs et clarifie également l'intention du programmeur (c'est-à-dire ce qui est censé être une classe concrète et ce qui constitue une interface/un mélange d'accessoires). Cela fournit également un endroit bien défini pour résoudre toutes les ambiguïtés: la classe concrète.
4
marcus

Les méthodes default dans les interfaces posent un problème qui:

Si les deux interfaces implémentées définissent une méthode par défaut avec la même signature de méthode, la classe d'implémentation ne sait pas quelle méthode par défaut utiliser.

La classe d'implémentation doit définir explicitement la méthode par défaut à utiliser ou définir sa propre méthode.

Ainsi, les méthodes default de Java-8 ne facilitent pas l'héritage multiple. La principale motivation des méthodes par défaut est que si, à un moment donné, nous devons ajouter une méthode à une interface existante, nous pouvons en ajouter une sans modifier les classes d'implémentation existantes. De cette manière, l'interface est toujours compatible avec les anciennes versions. Cependant, nous devrions nous rappeler la motivation d'utiliser les méthodes par défaut et conserver la séparation entre interface et implémentation.

3
S.K.