Uso del método predeterminado de Java

14

Durante décadas, las interfaces fueron solo solo (solo) para especificar firmas de métodos. Nos dijeron que esta era la "manera correcta de hacer las cosas ™".

Luego salió Java 8 y dijo:

  

Bueno, er, uh, ahora puede definir los métodos predeterminados. Tengo que correr, adiós.

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

Estoy creando un código experimental y mientras estaba haciendo una refactorización, terminé con una interfaz que simplemente extiende una interfaz estándar (iterable) y agrega dos métodos predeterminados. Y seré honesto, me siento muy bien por ello.

Sé que esto es un poco abierto, pero ahora que Java 8 se ha utilizado en proyectos reales, ¿existe una ortodoxia sobre el uso de los 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 he dado anteriormente? ¿Alguien se ha topado con algún problema al proporcionar implementaciones en sus interfaces?

    
pregunta JimmyJames 24.04.2017 - 19:27

1 respuesta

12

Un gran caso de uso es lo que yo llamo interfaces de "palanca": interfaces que solo tienen un pequeño número de métodos abstractos (idealmente 1), pero proporcionan un gran "apalancamiento" en el sentido de que le brindan mucha funcionalidad. : solo necesitas implementar 1 método en tu clase pero obtienes muchos otros métodos "gratis". Piense en una interfaz de colección, por ejemplo, con un solo método abstracto foreach y métodos default como map , fold , reduce , filter , partition , groupBy , sort , sortBy , etc.

Aquí hay un par de ejemplos. Comencemos 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, antes o después. Ambos métodos de composición se implementan utilizando solo apply :

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 puedes crear una interfaz para objetos similares, 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 devuelven Collection , independientemente de cuál era el 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 las lambdas, porque dicha interfaz "palanca" puede ser implementada por una 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 ventaja distinta: son métodos de instancia "adecuados", lo que significa que tienen acceso a los detalles de implementación privada de la interfaz (% Los métodos de interfaz private vienen en Java 9), mientras que los métodos de extensión son solo azúcar sintáctica para métodos estáticos.

Si Java alguna vez recibiera la inyección de interfaz, también permitiría la instalación de parches modulares de tipo seguro, de ámbito y de tipo. Esto sería muy interesante para los implementadores de lenguaje en la JVM: en este momento, por ejemplo, JRuby hereda o envuelve las clases de Java para proporcionarles semántica de Ruby adicional, pero lo ideal es que quieran usar las mismas clases. Con la inyección de interfaz y los métodos predeterminados, se podrían inyectar, por ejemplo. una interfaz RubyObject en java.lang.Object , de modo que un Java Object y un Ruby Object son exactamente lo mismo .

    
respondido por el Jörg W Mittag 24.04.2017 - 22:03

Lea otras preguntas en las etiquetas