Pourquoi des méthodes par défaut et statiques ont-elles été ajoutées aux interfaces dans Java 8 alors que nous avions déjà des classes abstraites?

99

En Java 8, les interfaces peuvent contenir des méthodes implémentées, des méthodes statiques et les méthodes dites "par défaut" (que les classes d'implémentation n'ont pas besoin de remplacer).

À mon avis (probablement naïf), il n’était pas nécessaire de violer des interfaces comme celle-ci. Les interfaces ont toujours été un contrat que vous devez remplir, et c'est un concept très simple et pur. Maintenant, c'est un mélange de plusieurs choses. À mon avis:

  1. les méthodes statiques n'appartiennent pas aux interfaces. Ils appartiennent à des classes d'utilitaires.
  2. Les méthodes "par défaut" n'auraient absolument pas dû être autorisées dans les interfaces. Vous pouvez toujours utiliser une classe abstraite à cette fin.

En bref:

Avant Java 8:

  • Vous pouvez utiliser des classes abstraites et régulières pour fournir des méthodes statiques et par défaut. Le rôle des interfaces est clair.
  • Toutes les méthodes d'une interface doivent être remplacées par l'implémentation de classes.
  • Vous ne pouvez pas ajouter une nouvelle méthode dans une interface sans modifier toutes les implémentations, mais c'est en fait une bonne chose.

Après Java 8:

  • Il n'y a pratiquement aucune différence entre une interface et une classe abstraite (hormis l'héritage multiple). En fait, vous pouvez émuler une classe normale avec une interface.
  • Lors de la programmation des implémentations, les programmeurs peuvent oublier de remplacer les méthodes par défaut.
  • Il y a une erreur de compilation si une classe tente d'implémenter deux interfaces ou plus ayant une méthode par défaut avec la même signature.
  • En ajoutant une méthode par défaut à une interface, chaque classe d'implémentation hérite automatiquement de ce comportement. Certaines de ces classes n'ont peut-être pas été conçues avec cette nouvelle fonctionnalité, ce qui peut poser problème. Par exemple, si quelqu'un ajoute une nouvelle méthode par défaut default void foo()à une interface Ix, la classe Cximplémentant Ixet disposant d'une foométhode privée avec la même signature ne compile pas.

Quelles sont les principales raisons de ces changements majeurs et quels sont les nouveaux avantages (le cas échéant)?

Monsieur Smith
la source
30
Question bonus: Pourquoi n’ont-ils pas introduit l’héritage multiple pour les classes?
2
les méthodes statiques n'appartiennent pas aux interfaces. Ils appartiennent à des classes d'utilitaires. Non, ils appartiennent à la @Deprecatedcatégorie! Les méthodes statiques sont l'une des constructions les plus maltraitées de Java, à cause de l'ignorance et de la paresse. De nombreuses méthodes statiques impliquent généralement des programmeurs incompétents, augmentent le couplage de plusieurs ordres de grandeur et sont un cauchemar pour les tests unitaires et les refactorisations lorsque vous réalisez pourquoi elles sont une mauvaise idée!
11
@JarrodRoberson Pouvez-vous donner quelques indices supplémentaires (des liens seraient formidables) sur "sont un cauchemar pour les tests unitaires et les refactor lorsque vous réalisez pourquoi ils sont une mauvaise idée!"? Je n'y ai jamais pensé et j'aimerais en savoir plus.
aqueux
11
@Chris L'héritage multiple d'état provoque beaucoup de problèmes, notamment d'allocation de mémoire ( le problème classique des losanges ). Cependant, l'héritage multiple de comportement ne dépend que de la mise en œuvre du contrat déjà défini par l'interface (l'interface peut appeler d'autres méthodes qu'elle déclare et nécessite). Une distinction subtile, mais très intéressante.
ssube
1
vous voulez ajouter une méthode à une interface existante, était lourd ou presque impossible avant java8 maintenant, vous pouvez simplement l’ajouter comme méthode par défaut.
VdeX

Réponses:

59

Un bon exemple de motivation pour les méthodes par défaut est la bibliothèque standard Java, où vous avez maintenant

list.sort(ordering);

au lieu de

Collections.sort(list, ordering);

Je ne pense pas qu'ils auraient pu le faire autrement sans plus d'une implémentation identique de List.sort.

soru
la source
19
C # résout ce problème avec les méthodes d'extension.
Robert Harvey
5
et il permet une liste chaînée d'utiliser un O (1) d' espace et de O (n log n) mergesort, car les listes chaînées peuvent être fusionnées en place, en Java 7 , il dépotoirs à un tableau externe, puis sorte que
monstre à cliquet
5
J'ai trouvé ce papier où Goetz explique le problème. Donc, je vais marquer cette réponse comme solution pour le moment.
Monsieur Smith
1
@RobertHarvey: Créez une liste <Octet> de deux cent millions d'articles, utilisez-la IEnumerable<Byte>.Appendpour les joindre, puis appelez Count-moi, puis dites-moi comment les méthodes d'extension résolvent le problème. Si CountIsKnownet Countétaient membres de IEnumerable<T>, le retour de Appendpourrait annoncer CountIsKnownsi les collections constituantes le faisaient, mais sans de telles méthodes, cela n’est pas possible.
Supercat
6
@supercat: Je n'ai pas la moindre idée de ce dont vous parlez.
Robert Harvey
48

La réponse correcte se trouve en fait dans la documentation Java , qui dit:

Les méthodes par défaut vous permettent d'ajouter de nouvelles fonctionnalités aux interfaces de vos bibliothèques et d'assurer la compatibilité binaire avec le code écrit pour les versions antérieures de ces interfaces.

Cela a longtemps été une source de douleur pour Java, car les interfaces avaient tendance à être impossibles à évoluer une fois rendues publiques. (Le contenu de la documentation est lié au document auquel vous avez lié un commentaire: Evolution de l'interface via des méthodes d'extension virtuelles .) En outre, l'adoption rapide de nouvelles fonctionnalités (par exemple, lambdas et les nouvelles API de flux) ne peut être réalisée qu'en étendant la interfaces de collections existantes et fournissant des implémentations par défaut. Briser la compatibilité binaire ou introduire de nouvelles API signifierait que plusieurs années s'écouleraient avant que les fonctionnalités les plus importantes de Java 8 ne soient couramment utilisées.

La raison pour laquelle les méthodes statiques ont été autorisées dans les interfaces est à nouveau révélée par la documentation: cela vous facilite la tâche d’organiser des méthodes auxiliaires dans vos bibliothèques; vous pouvez conserver des méthodes statiques spécifiques à une interface dans la même interface plutôt que dans une classe séparée. En d'autres termes, les classes d'utilitaires statiques comme java.util.Collectionspeuvent maintenant (enfin) être considérées comme un anti-motif, en général (bien sûr, pas toujours ). Je suppose que l'ajout de la prise en charge de ce comportement était trivial une fois les méthodes d'extension virtuelle mises en œuvre, sinon cela n'aurait probablement pas été fait.

Sur une note similaire, un exemple de la façon dont ces nouvelles fonctionnalités peuvent être utiles est de considérer une classe qui m'a récemment ennuyé, java.util.UUID. Il ne fournit pas vraiment de support pour les types UUID 1, 2 ou 5 et ne peut pas être facilement modifié pour le faire. Il est également bloqué par un générateur aléatoire prédéfini qui ne peut pas être remplacé. L'implémentation de code pour les types d'UUID non pris en charge nécessite soit une dépendance directe à une API tierce plutôt qu'une interface, soit la maintenance du code de conversion et le coût du ramassage des déchets supplémentaire qui va avec. Avec les méthodes statiques, UUIDaurait pu plutôt être défini comme une interface, permettant de véritables implémentations tierces des pièces manquantes. (Si UUIDétaient définis à l'origine comme une interface, nous aurions probablement une sorte de maladroitUuidUtil classe avec des méthodes statiques, ce qui serait affreux aussi.) De nombreuses API de base de Java sont dégradées en ne se basant pas sur des interfaces, mais à partir de Java 8, le nombre d'excuses pour ce mauvais comportement a heureusement diminué.

Il n'est pas correct de dire qu'il n'y a pratiquement aucune différence entre une interface et une classe abstraite , car les classes abstraites peuvent avoir un état (c'est-à-dire, déclarer des champs), contrairement aux interfaces. Il n’est donc pas équivalent à un héritage multiple ni même à un héritage de type mixin. Les mixins appropriés (tels que les traits de Groovy 2.3 ) ont accès à state. (Groovy prend également en charge les méthodes d'extension statique.)

Ce n’est pas non plus une bonne idée de suivre l’exemple de Doval , à mon avis. Une interface est censée définir un contrat, mais elle n'est pas censée faire respecter le contrat. (Pas de toute façon en Java.) La vérification d’une implémentation relève de la responsabilité d’une suite de tests ou d’un autre outil. La définition des contrats pourrait se faire avec des annotations, et OVal est un bon exemple, mais je ne sais pas s'il prend en charge les contraintes définies sur les interfaces. Un tel système est réalisable, même s'il n'en existe pas actuellement. (Les stratégies incluent la personnalisation à la compilation de javacvia le processeur d'annotationAPI et génération de bytecode d’exécution.) Idéalement, les contrats seraient exécutés au moment de la compilation et, dans le pire des cas, au moyen d’une suite de tests, mais j’ai bien compris que l’application à l’exécution est mal vue. Le Checker Framework est un autre outil intéressant pour la programmation de contrats en Java .

vert
la source
1
Pour un suivi ultérieur de mon dernier paragraphe (c'est-à - dire , n'appliquez pas de contrats dans les interfaces ), il est intéressant de souligner que les defaultméthodes ne peuvent pas remplacer equals, hashCodeet toString. Une analyse coûts / avantages très informative expliquant pourquoi cette mesure est interdite peut être consultée ici: mail.openjdk.java.net/pipermail/lambda-dev/2013-March/…
lundi
Il est dommage que Java ne dispose que d’ equalsune seule hashCodeméthode virtuelle et d’une seule méthode, car les collections doivent peut-être tester deux types d’égalité et les éléments qui implémenteraient plusieurs interfaces risquent de se heurter à des exigences contractuelles contradictoires. Il hashMapest utile de pouvoir utiliser des listes qui ne vont pas changer de clé, mais il peut aussi être utile de stocker des collections dans un hashMaprépertoire qui correspond à l'équivalence plutôt qu'à l'état actuel (l'équivalence implique l'appariement de l'état et de l' immutabilité ). .
Supercat
Eh bien, Java propose une solution de contournement avec les interfaces Comparator et Comparable. Mais ceux-là sont un peu moche, je pense.
ngreen
Ces interfaces ne sont prises en charge que par certains types de collection et posent leur propre problème: un comparateur peut lui - même potentiellement encapsuler un état (par exemple, un comparateur de chaînes spécialisé peut ignorer un nombre configurable de caractères au début de chaque chaîne, auquel cas le nombre de caractères). les caractères à ignorer feraient partie de l'état du comparateur), qui ferait à son tour partie de l'état de toute collection triée par celle-ci, mais il n'existe pas de mécanisme défini pour demander à deux comparateurs s'ils sont équivalents.
Supercat
Oh oui, j'ai la douleur des comparateurs. Je travaille sur une structure arborescente qui devrait être simple mais ne l’est pas parce qu’il est si difficile de faire en sorte que le comparateur fonctionne correctement. Je vais probablement écrire une classe d'arbre personnalisée juste pour faire disparaître le problème.
ngreen
44

Parce que vous ne pouvez hériter que d'une classe. Si vous avez deux interfaces dont les implémentations sont suffisamment complexes pour nécessiter une classe de base abstraite, ces deux interfaces s'excluent mutuellement en pratique.

L'alternative consiste à convertir ces classes de base abstraites en un ensemble de méthodes statiques et à transformer tous les champs en arguments. Cela permettrait à tout développeur de l’interface d’appeler les méthodes statiques et d’obtenir la fonctionnalité, mais c’est beaucoup, beaucoup dans un langage déjà trop bavard.


Comme exemple motivant des raisons pour lesquelles il est utile de pouvoir implémenter des interfaces, considérez cette interface Stack:

public interface Stack<T> {
    boolean isEmpty();

    T pop() throws EmptyException;
 }

Il n'y a aucun moyen de garantir que lorsque quelqu'un implémentera l'interface, popune exception sera levée si la pile est vide. Nous pourrions appliquer cette règle en séparant popdeux méthodes: une public finalméthode qui applique le contrat et une protected abstractméthode qui effectue le vidage.

public abstract class Stack<T> {
    public abstract boolean isEmpty();

    protected abstract T pop_implementation();

    public final T pop() throws EmptyException {
        if (isEmpty()) {
            throw new EmptyException();
        else {
            return pop_implementation();
        }
    }
 }

Non seulement nous nous assurons que toutes les implémentations respectent le contrat, nous les avons également libérées de l'obligation de vérifier si la pile est vide et d'exécuter l'exception. C'est une grosse victoire! ... sauf que nous avons dû changer l'interface en une classe abstraite. Dans une langue à héritage unique, cela représente une perte importante de flexibilité. Cela rend vos interfaces possibles mutuellement exclusives. Le fait de pouvoir fournir des implémentations qui ne reposent que sur les méthodes d'interface elles-mêmes résoudrait le problème.

Je ne sais pas si l'approche de Java 8 pour l'ajout de méthodes aux interfaces permet d'ajouter des méthodes finales ou des méthodes abstraites protégées, mais je sais que le langage D le permet et fournit un support natif pour Design by Contract . Il n'y a pas de danger dans cette technique car popc'est final, donc aucune classe d'implémentation ne peut la remplacer.

En ce qui concerne les implémentations par défaut des méthodes remplaçables, je suppose que toute implémentation par défaut ajoutée aux API Java ne repose que sur le contrat de l'interface à laquelle elles ont été ajoutées. Ainsi, toute classe qui implémente correctement l'interface se comportera également correctement avec les implémentations par défaut.

De plus,

Il n'y a pratiquement aucune différence entre une interface et une classe abstraite (hormis l'héritage multiple). En fait, vous pouvez émuler une classe normale avec une interface.

Ce n'est pas tout à fait vrai puisque vous ne pouvez pas déclarer de champs dans une interface. Toute méthode que vous écrivez dans une interface ne peut pas compter sur des détails d'implémentation.


A titre d'exemple en faveur des méthodes statiques dans les interfaces, considérons des classes utilitaires telles que Collections dans l'API Java. Cette classe n'existe que parce que ces méthodes statiques ne peuvent pas être déclarées dans leurs interfaces respectives. Collections.unmodifiableListaurait pu tout aussi bien être déclaré dans l' Listinterface, et il aurait été plus facile à trouver.

Doval
la source
4
Contre-argument: puisque les méthodes statiques, si elles sont écrites correctement, sont autonomes, elles ont plus de sens dans une classe statique distincte où elles peuvent être collectées et classées par nom de classe, et moins de sens dans une interface, où elles sont essentiellement pratiques cela invite à des abus tels que maintenir l'état statique dans l'objet ou causer des effets secondaires, rendant les méthodes statiques impossibles à tester.
Robert Harvey
3
@ RobertHarvey Qu'est-ce qui vous empêche de faire des choses aussi stupides si votre méthode statique est dans une classe? De plus, la méthode dans l'interface peut ne nécessiter aucun état. Vous pourriez simplement essayer de faire respecter un contrat. Supposons que vous ayez une Stackinterface et que vous souhaitiez vous assurer qu'une popexception est lancée quand elle est appelée avec une pile vide. Etant donné les méthodes abstraites boolean isEmpty()et protected T pop_impl(), vous pouvez implémenter final T pop() { isEmpty()) throw PopException(); else return pop_impl(); }Ceci applique le contrat à TOUS les développeurs.
Doval
Attends quoi? Pousser et méthodes Pop sur une pile sont pas vont être static.
Robert Harvey
@ RobertHarvey J'aurais été plus clair sans la limite de caractères dans les commentaires, mais je plaidais pour des implémentations par défaut dans une interface, pas des méthodes statiques.
Doval
8
Je pense que les méthodes d’interface par défaut sont plutôt un hack qui a été introduit pour pouvoir étendre la bibliothèque standard sans avoir à adapter le code existant en fonction de celle-ci.
Giorgio
2

L'intention était peut-être de fournir la possibilité de créer des classes mixin en remplaçant le besoin d'injecter des informations ou des fonctionnalités statiques via une dépendance.

Cette idée semble liée à la manière dont vous pouvez utiliser les méthodes d'extension en C # pour ajouter des fonctionnalités implémentées aux interfaces.

rae1
la source
1
Les méthodes d'extension n'ajoutent pas de fonctionnalités aux interfaces. Les méthodes d'extension ne sont que du sucre syntaxique pour appeler des méthodes statiques sur une classe à l'aide du list.sort(ordering);formulaire approprié .
Robert Harvey
Si vous regardez l' IEnumerableinterface en C #, vous pouvez voir comment l'implémentation de méthodes d'extension à cette interface (comme le LINQ to Objectsfait le fait) ajoute des fonctionnalités à chaque classe implémentée IEnumerable. C'est ce que je voulais dire en ajoutant des fonctionnalités.
Rae1
2
C'est la grande chose à propos des méthodes d'extension. ils donnent l’ illusion que vous bloquez la fonctionnalité sur une classe ou une interface. Il suffit de ne pas confondre cela avec l’ajout de méthodes réelles à une classe; Les méthodes de classe ont accès aux membres privés d'un objet, pas les méthodes d'extension (puisqu'elles ne sont en fait qu'un autre moyen d'appeler des méthodes statiques).
Robert Harvey
2
Exactement, et c'est pourquoi je vois une relation entre avoir des méthodes statiques ou par défaut dans une interface en Java; l'implémentation est basée sur ce qui est disponible pour l'interface et non sur la classe elle-même.
Rae1
1

Les deux objectifs principaux que je vois dans les defaultméthodes (certains cas d'utilisation servent les deux objectifs):

  1. Sucre de syntaxe. Une classe d'utilitaires pourrait servir à cette fin, mais les méthodes d'instance sont plus agréables.
  2. Extension d'une interface existante. L'implémentation est générique mais parfois inefficace.

S'il ne s'agissait que du second objectif, vous ne le verriez pas dans une toute nouvelle interface, telle que Predicate. Toutes les @FunctionalInterfaceinterfaces annotées doivent avoir exactement une méthode abstraite pour qu'un lambda puisse l'implémenter. Ajout des defaultméthodes telles que and, or, ne negatesont que l' utilité, et vous ne sont pas censés les remplacer. Cependant, parfois, les méthodes statiques feraient mieux .

En ce qui concerne l’extension des interfaces existantes - même dans ce cas, certaines nouvelles méthodes ne sont que du sucre syntaxique. Méthodes de Collectioncomme stream, forEach, removeIf- essentiellement, il est tout simplement utilitaire vous n'avez pas besoin de passer outre. Et puis il y a des méthodes comme spliterator. L'implémentation par défaut est sous-optimale, mais bon, au moins le code est compilé. Utilisez-le uniquement si votre interface est déjà publiée et largement utilisée.


En ce qui concerne les staticméthodes, je suppose que les autres le couvrent assez bien: cela permet à l'interface d'être sa propre classe d'utilitaires. Peut-être pourrions-nous nous en débarrasser Collectionsdans le futur de Java? Set.empty()serait rock.

Vlasec
la source