Uso del método predeterminado de Java

13

Durante décadas ha sido el caso de que las interfaces eran solamente única (sólo) para especificar firmas de métodos. Nos dijeron que esta era la "forma correcta de hacer las cosas ™".

Entonces salió Java 8 y dijo:

Bueno, eh, ahora puedes definir métodos predeterminados. Tengo que correr, adiós.

Tengo curiosidad por saber cómo los desarrolladores experimentados de Java y los que comenzaron recientemente (los últimos años) lo están digiriendo. También me pregunto cómo encaja esto en la ortodoxia y práctica de Java.

Estoy creando un código experimental y, mientras realizaba algunas refactorizaciones, terminé con una interfaz que simplemente extiende una interfaz estándar (Iterable) y agrega dos métodos predeterminados. Y voy a ser sincero, me siento muy bien por eso.

Sé que esto es un poco abierto, pero ahora que ha pasado algún tiempo para que Java 8 se use en proyectos reales, ¿hay alguna ortodoxia en torno al uso de métodos predeterminados? Lo que más veo cuando se discuten es sobre cómo agregar nuevos métodos a una interfaz sin romper los consumidores existentes. Pero, ¿qué hay de usar esto desde el principio como el ejemplo que di arriba? ¿Alguien ha tenido algún problema al proporcionar implementaciones en sus interfaces?

JimmyJames
fuente
También me interesaría esta perspectiva. Regreso a Java después de 6 años en el mundo .Net. Me parece que esta podría ser la respuesta de Java para los métodos de extensión C #, con un poco de influencia de los métodos del módulo de Ruby. No he jugado con él, así que no puedo estar seguro.
Berin Loritsch
1
Siento que la razón por la que se añaden los métodos por defecto es en gran medida para que pudieran extender las interfaces de la colección sin tener que hacer totalmente diferentes interfaces
Justin
1
@Justin: consulte el java.util.function.Functionuso de métodos predeterminados en una interfaz completamente nueva.
Jörg W Mittag
@Justin Creo que este fue el principal impulsor. Realmente debería comenzar a prestar atención al proceso nuevamente ya que realmente comenzaron a hacer cambios.
JimmyJames

Respuestas:

12

Un gran caso de uso es lo que yo llamo interfaces de "palanca": interfaces que solo tienen una pequeña cantidad de métodos abstractos (idealmente 1), pero proporcionan mucha "influencia" en el sentido de que le brindan mucha funcionalidad: usted solo necesitas implementar 1 método en tu clase pero obtienes muchos otros métodos "gratis". Piense en una interfaz de recogida, por ejemplo, con un solo extracto foreachmétodo y defaultmétodos como map, fold, reduce, filter, partition, groupBy, sort, sortBy, etc.

Aquí hay un par de ejemplos. Vamos a empezar con java.util.function.Function<T, R>. Tiene un solo método abstracto R apply<T>. Y tiene dos métodos predeterminados que le permiten componer la función con otra función de dos maneras diferentes, ya sea antes o después. Ambos métodos de composición se implementan utilizando soloapply :

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));
}

También podría crear una interfaz para objetos comparables, algo como esto:

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());
  }
}

O un marco de colecciones extremadamente simplificado, donde todas las operaciones de colecciones regresan Collection, independientemente del tipo original:

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)
  }
}

Esto se vuelve muy interesante en combinación con lambdas, porque dicha interfaz "palanca" puede ser implementada por un lambda (es una interfaz SAM).

Este es el mismo caso de uso para el que se agregaron los Métodos de extensión en C♯, pero los métodos predeterminados tienen una clara ventaja: son métodos de instancia "adecuados", lo que significa que tienen acceso a detalles de implementación privados de la interfaz ( privateestán llegando los métodos de interfaz en Java 9), mientras que los métodos de extensión son solo azúcar sintáctico para los métodos estáticos.

Si Java recibiera alguna vez una inyección de interfaz, también permitiría parches de mono modulares, seguros y de tipo seguro. Esto sería muy interesante para los implementadores de lenguaje en la JVM: en este momento, por ejemplo, JRuby hereda o ajusta las clases de Java para proporcionarles semánticas de Ruby adicionales, pero idealmente, quieren usar las mismas clases. Con la inyección de interfaz y los métodos predeterminados, podrían inyectar, por ejemplo, una RubyObjectinterfaz java.lang.Object, de modo que un Java Objecty un Ruby Objectson exactamente lo mismo .

Jörg W Mittag
fuente
1
No sigo completamente esto. El método predeterminado en la interfaz debe definirse en términos de otros métodos en la interfaz o métodos definidos en Object. ¿Puedes dar un ejemplo de cómo creas una interfaz significativa de método único con un método predeterminado? Si necesita la sintaxis de Java 9 para demostrar, está bien.
JimmyJames
Por ejemplo: una Comparableinterfaz con un resumen compareTométodo, y por defecto lessThan, lessThanOrEqual, greaterThan, greaterThanOrEqual, isBetween, y clampmétodos, todos implementado en términos de compareTo. O simplemente mire java.util.function.Function: tiene un applymétodo abstracto y dos métodos de composición predeterminados, ambos implementados en términos de apply. Intenté dar un ejemplo de una Collectioninterfaz, pero hacer que todo sea seguro para los tipos es complicado y demasiado largo para esta respuesta: intentaré darle una oportunidad a una versión que no sea segura para los tipos y que no conserve los tipos. Manténganse al tanto.
Jörg W Mittag el
3
Los ejemplos ayudan. Gracias. No entendí lo que querías decir con interfaz de método único.
JimmyJames
Lo que significan los métodos predeterminados es que una única interfaz de método abstracto ya no tiene que ser una sola interfaz de método ;-)
Jörg W Mittag el
Estaba pensando en esto y se me ocurrió que AbstractCollection y AbstractList son básicamente de lo que estás hablando aquí (método 2 en lugar de 1, pero no creo que sea crucial). Si estos se refundieran como interfaces con métodos defualt, sería Ser súper simple para convertir un iterable en una colección agregando tamaño y crear una lista a partir de cualquier cosa también es muy fácil si puede indexar y conocer el tamaño.
JimmyJames