Utilisation de la méthode par défaut Java

13

Pendant des décennies, les interfaces ont été uniquement (uniquement) utilisées pour spécifier les signatures de méthode. On nous a dit que c'était la "bonne façon de faire les choses ™".

Ensuite, Java 8 est sorti et a dit:

Eh bien, euh, maintenant vous pouvez définir des méthodes par défaut. Je dois courir, au revoir.

Je suis curieux de savoir comment cela est digéré par les développeurs Java expérimentés et ceux qui ont commencé récemment (ces dernières années) à le développer. Je me demande également comment cela s'inscrit dans l'orthodoxie et la pratique Java.

Je construis du code expérimental et pendant que je faisais du refactoring, je me suis retrouvé avec une interface qui étend simplement une interface standard (Iterable) et ajoute deux méthodes par défaut. Et je vais être honnête, je me sens très bien à ce sujet.

Je sais que c'est un peu ouvert mais maintenant qu'il y a un certain temps pour que Java 8 soit utilisé dans de vrais projets, y a-t-il encore une orthodoxie autour de l'utilisation des méthodes par défaut? Ce que je vois surtout quand ils sont discutés, c'est comment ajouter de nouvelles méthodes à une interface sans casser les consommateurs existants. Mais qu'en est-il d'utiliser dès le départ comme l'exemple que j'ai donné ci-dessus. Quelqu'un at-il rencontré des problèmes avec la fourniture d'implémentations dans ses interfaces?

JimmyJames
la source
Je serais également intéressé par cette perspective. Je reviens à Java après 6 ans dans le monde .Net. Il me semble que cela pourrait être la réponse de Java aux méthodes d'extension C #, avec un peu d'influence des méthodes de module de Ruby. Je n'ai pas joué avec, donc je ne peux pas en être sûr.
Berin Loritsch
1
Je pense que la raison pour laquelle ils ont ajouté des méthodes par défaut est en grande partie afin qu'ils puissent étendre les interfaces de collecte sans avoir à créer des interfaces entièrement différentes
Justin
1
@Justin: voir java.util.function.Functionpour l'utilisation des méthodes par défaut dans une toute nouvelle interface.
Jörg W Mittag
@Justin Je suppose que c'était le principal moteur. Je devrais vraiment recommencer à prêter attention au processus car ils ont vraiment commencé à faire des changements.
JimmyJames

Réponses:

12

Un excellent cas d'utilisation est ce que j'appelle des interfaces «à levier»: des interfaces qui n'ont qu'un petit nombre de méthodes abstraites (idéalement 1), mais qui fournissent beaucoup de «levier» en ce qu'elles vous offrent beaucoup de fonctionnalités: vous seulement besoin d'implémenter 1 méthode dans votre classe mais obtenez beaucoup d'autres méthodes "gratuitement". Pensez à une interface de collecte, par exemple, avec une seule abstraite foreachméthode et des defaultméthodes comme map, fold, reduce, filter, partition, groupBy, sort, sortBy, etc.

Voici quelques exemples. Commençons par java.util.function.Function<T, R>. Il a une seule méthode abstraite R apply<T>. Et il a deux méthodes par défaut qui vous permettent de composer la fonction avec une autre fonction de deux manières différentes, avant ou après. Ces deux méthodes de composition sont implémentées en utilisant simplementapply :

default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
    return (V v) -> apply(before.apply(v));
}

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    return (T t) -> after.apply(apply(t));
}

Vous pouvez également créer une interface pour des objets comparables, quelque chose comme ceci:

interface MyComparable<T extends MyComparable<T>> {
  int compareTo(T other);

  default boolean lessThanOrEqual(T other) {
    return compareTo(other) <= 0;
  }

  default boolean lessThan(T other) {
    return compareTo(other) < 0;
  }

  default boolean greaterThanOrEqual(T other) {
    return compareTo(other) >= 0;
  }

  default boolean greaterThan(T other) {
    return compareTo(other) > 0;
  }

  default boolean isBetween(T min, T max) {
    return greaterThanOrEqual(min) && lessThanOrEqual(max);
  }

  default T clamp(T min, T max) {
    if (lessThan(   min)) return min;
    if (greaterThan(max)) return max;
                          return (T)this;
  }
}

class CaseInsensitiveString implements MyComparable<CaseInsensitiveString> {
  CaseInsensitiveString(String s) { this.s = s; }
  private String s;

  @Override public int compareTo(CaseInsensitiveString other) {
    return s.toLowerCase().compareTo(other.s.toLowerCase());
  }
}

Ou un framework de collections extrêmement simplifié, où toutes les opérations de collections retournent Collection, quel que soit le type d'origine:

interface MyCollection<T> {
  void forEach(java.util.function.Consumer<? super T> f);

  default <R> java.util.Collection<R> map(java.util.function.Function<? super T, ? extends R> f) {
    java.util.Collection<R> l = new java.util.ArrayList();
    forEach(el -> l.add(f.apply(el)));
    return l;
  }
}

class MyArray<T> implements MyCollection<T> {
  private T[] array;

  MyArray(T[] array) { this.array = array; }

  @Override public void forEach(java.util.function.Consumer<? super T> f) {
    for (T el : array) f.accept(el);
  }

  @Override public String toString() {
    StringBuilder sb = new StringBuilder("(");
    map(el -> el.toString()).forEach(s -> { sb.append(s); sb.append(", "); } );
    sb.replace(sb.length() - 2, sb.length(), ")");
    return sb.toString();
  }

  public static void main(String... args) {
    MyArray<Integer> array = new MyArray<>(new Integer[] {1, 2, 3, 4});
    System.out.println(array);
    // (1, 2, 3, 4)
  }
}

Cela devient très intéressant en combinaison avec lambdas, car une telle interface "levier" peut être implémentée par un lambda (c'est une interface SAM).

C'est le même cas d'utilisation que les méthodes d'extension ont été ajoutées en C♯, mais les méthodes par défaut ont un avantage distinct: ce sont des méthodes d'instance "correctes", ce qui signifie qu'elles ont accès aux détails d'implémentation privée de l'interface ( privateles méthodes d'interface arrivent en Java 9), alors que les méthodes d'extension ne sont que du sucre syntaxique pour les méthodes statiques.

Si Java obtenait une injection d'interface, il permettrait également une correction de singe modulaire sécurisée et de portée. Cela serait très intéressant pour les implémenteurs de langage sur la JVM: pour le moment, par exemple, JRuby hérite ou encapsule des classes Java pour leur fournir une sémantique Ruby supplémentaire, mais idéalement, ils veulent utiliser les mêmes classes. Avec l'Injection d'Interface et les Méthodes par Défaut, ils pourraient injecter par exemple une RubyObjectinterface dans java.lang.Object, de sorte qu'un Java Objectet un Ruby Objectsont exactement la même chose .

Jörg W Mittag
la source
1
Je ne suis pas tout à fait cela. La méthode par défaut sur l'interface doit être définie en termes d'autres méthodes sur l'interface ou les méthodes définies dans Object. Pouvez-vous donner un exemple de la façon dont vous créez une interface de méthode unique significative avec une méthode par défaut? Si vous avez besoin de la syntaxe Java 9 pour le démontrer, c'est bien.
JimmyJames
Par exemple: une Comparableinterface avec un résumé compareTométhode, et par défaut lessThan, lessThanOrEqual, greaterThan, greaterThanOrEqual, isBetweenet les clampméthodes, tous mis en œuvre en termes de compareTo. Ou, regardez java.util.function.Function: il a une applyméthode abstraite et deux méthodes de composition par défaut, toutes deux implémentées en termes de apply. J'ai essayé de donner un exemple d' Collectioninterface, mais il est difficile et trop long de faire en sorte que tout soit sûr pour cette réponse - je vais essayer de donner une version non sécurisée, sans préservation de type. Restez à l'écoute.
Jörg W Mittag
3
Les exemples aident. Merci. J'ai mal compris ce que vous entendiez par interface à méthode unique.
JimmyJames
Les méthodes par défaut signifient qu'une seule interface de méthode abstraite ne doit plus être une interface de méthode unique ;-)
Jörg W Mittag
J'y pensais et il m'est venu à l'esprit que AbstractCollection et AbstractList sont essentiellement ce dont vous parlez ici (méthode 2 au lieu de 1 mais je ne pense pas que ce soit crucial.) Si elles étaient refondues en tant qu'interfaces avec des méthodes par défaut, cela être super simple pour transformer un itérable en collection en ajoutant de la taille et en créant une liste à partir de n'importe quoi est aussi un jeu d'enfant si vous pouvez indexer et connaître la taille.
JimmyJames