web-dev-qa-db-fra.com

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?

Dans 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'utilité.
  2. Les méthodes "par défaut" ne devraient pas du tout être autorisées dans les interfaces. Vous pouvez toujours utiliser une classe abstraite à cet effet.

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 (autre que l'héritage multiple). En fait, vous pouvez émuler une classe régulière 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 essaie d'implémenter deux ou plusieurs interfaces 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é à l'esprit, ce qui peut entraîner des problèmes. Par exemple, si quelqu'un ajoute une nouvelle méthode par défaut default void foo() à une interface Ix, alors la classe Cx implémentant Ix et ayant un foo avec la même signature ne se compile pas.

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

103
Mister Smith

Un bon exemple de motivation pour les méthodes par défaut est dans 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.

59
soru

La bonne réponse se trouve en fait dans la Documentation Java , qui dit:

[d] Les méthodes efault 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 anciennes versions de ces interfaces.

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

La raison pour autoriser les méthodes statiques dans les interfaces est à nouveau révélée par la documentation: [t] sa facilite l'organisation des méthodes d'assistance dans vos bibliothèques; vous pouvez conserver les méthodes statiques spécifiques à une interface dans la même interface plutôt que dans une classe distincte. En d'autres termes, les classes utilitaires statiques comme Java.util.Collections peut maintenant (enfin) être considéré comme un anti-pattern, 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 que les méthodes d'extension virtuelle ont été implémentées, 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 avantageuses est de considérer une classe qui m'a récemment ennuyé, Java.util.UUID . Il ne prend pas vraiment en charge types UUID 1, 2 ou 5, et il ne peut pas être facilement modifié pour le faire. Il est également bloqué avec un générateur aléatoire prédéfini qui ne peut pas être remplacé. L'implémentation du code pour les types UUID non pris en charge nécessite soit une dépendance directe sur une API tierce plutôt qu'une interface, soit la maintenance du code de conversion et le coût de la récupération de place supplémentaire pour aller avec. Avec des méthodes statiques, UUID aurait pu être défini comme une interface à la place, permettant de réelles implémentations tierces des pièces manquantes. (Si UUID était initialement défini comme une interface, nous aurions probablement une sorte de classe UuidUtil maladroite avec des méthodes statiques, ce qui serait aussi horrible.) De nombreuses API de base de Java sont dégradées par à défaut de se baser 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 que [t] 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 champs) alors que les interfaces ne le peuvent pas. Il n'est donc pas équivalent à l'héritage multiple ou même à l'héritage de style mixin. Les mixins appropriés (tels que Groovy 2.3 traits ) ont accès à l'état. (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 appliquer le contrat. (Pas dans Java de toute façon.) La vérification correcte d'une implémentation est la responsabilité d'une suite de tests ou d'un autre outil. La définition des contrats peut être faite avec des annotations, et OVal est un bon exemple, mais je ne sais pas s'il supporte les contraintes définies sur les interfaces. Un tel système est faisable, même s'il n'en existe pas actuellement. (Les stratégies incluent la personnalisation à la compilation de javac via le - processeur d'annotations génération d'API et de bytecode d'exécution.) Idéalement, les contrats devraient être appliqués au moment de la compilation, et dans le pire des cas en utilisant une suite de tests, mais je crois comprendre que l'application d'exécution est désapprouvée. Un autre outil intéressant qui pourrait aider à la programmation des contrats dans Java est le Checker Framework .

50
ngreen

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

L'alternative est de convertir ces classes de base abstraites en une collection de méthodes statiques et de transformer tous les champs en arguments. Cela permettrait à n'importe quel implémenteur de l'interface d'appeler les méthodes statiques et d'obtenir la fonctionnalité, mais c'est énormément de passe-partout dans un langage qui est déjà beaucoup trop verbeux.


Comme exemple motivant de la raison pour laquelle la possibilité de fournir des implémentations dans les interfaces peut être utile, considérez cette interface de pile:

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

    T pop() throws EmptyException;
 }

Il n'y a aucun moyen de garantir que lorsque quelqu'un implémente l'interface, pop lèvera une exception si la pile est vide. Nous pourrions appliquer cette règle en séparant pop en deux méthodes: a public final méthode qui fait respecter le contrat et une protected abstract méthode qui effectue le popping réel.

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 la nécessité de vérifier si la pile est vide et de lever l'exception. C'est une grande victoire! ... à part le fait que nous devions changer l'interface en une classe abstraite. Dans une langue à héritage unique, c'est une grande perte de flexibilité. Cela rend vos interfaces potentielles mutuellement exclusives. Être capable de fournir des implémentations qui ne dépendent que des méthodes d'interface elles-mêmes résoudrait le problème.

Je ne sais pas si Java 8 pour ajouter des méthodes aux interfaces permet d'ajouter des méthodes finales ou des méthodes abstraites protégées, mais je sais le langage D le permet et fournit natif prise en charge de Design by Contract . Il n'y a aucun danger dans cette technique puisque pop est finale, donc aucune classe d'implémentation ne peut la remplacer.

En ce qui concerne les implémentations par défaut des méthodes substituables, je suppose que toutes les implémentations par défaut ajoutées aux API Java ne dépendent que du contrat de l'interface à laquelle elles ont été ajoutées, donc toute classe qui implémente correctement l'interface sera également se comporter 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 (autre que l'héritage multiple). En fait, vous pouvez émuler une classe régulière avec une interface.

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


À titre d'exemple en faveur des méthodes statiques dans les interfaces, considérez les classes utilitaires comme Collections dans l'API Java. Cette classe existe uniquement parce que ces méthodes statiques ne peuvent pas être déclarées dans leurs interfaces respectives. Collections.unmodifiableList aurait pu tout aussi bien être déclaré dans l'interface List, et il aurait été plus facile à trouver.

44
Doval

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

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

2
rae1

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

  1. Sucre syntaxique. Une classe utilitaire pourrait servir cet objectif, 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 deuxième objectif, vous ne le verriez pas dans une toute nouvelle interface comme Predicate. Toutes les interfaces annotées @FunctionalInterface Doivent avoir exactement une méthode abstraite afin qu'un lambda puisse l'implémenter. Les méthodes default ajoutées comme and, or, negate ne sont que des utilitaires, et vous n'êtes pas censé les remplacer. Cependant, parfois des méthodes statiques feraient mieux .

Quant à l'extension des interfaces existantes - même là, certaines nouvelles méthodes ne sont que du sucre de syntaxe. Méthodes de Collection comme stream, forEach, removeIf - en gros, c'est juste un utilitaire que vous n'avez pas besoin de remplacer. Et puis il y a des méthodes comme spliterator. L'implémentation par défaut n'est pas optimale, mais bon, au moins le code se compile. N'y recourir que si votre interface est déjà publiée et largement utilisée.


Quant aux méthodes static, je suppose que les autres le couvrent assez bien: elles permettent à l'interface d'être sa propre classe utilitaire. Peut-être pourrions-nous nous débarrasser de Collections dans le futur de Java? Set.empty() basculerait.

1
Vlasec