Utilizzo del metodo di default Java

14

Per decenni è stato il caso che le interfacce erano solo solo (solo) per specificare le firme del metodo. Ci è stato detto che questo era il "modo giusto per fare cose ™".

Quindi Java 8 è uscito e ha detto:

Well, er, uh, now you can define default methods. Gotta run, bye.

Sono curioso di sapere come questo venga digerito dagli sviluppatori Java esperti e da coloro che hanno iniziato a svilupparsi recentemente (negli ultimi anni). Mi sto anche chiedendo come questo si inserisca nell'ortodossia e nella pratica di Java.

Sto costruendo del codice sperimentale e mentre eseguivo alcuni refactoring, ho finito con un'interfaccia che estende semplicemente un'interfaccia standard (Iterable) e aggiunge due metodi predefiniti. E sarò onesto, mi sento dannatamente bene.

So che questo è un po 'aperto, ma ora che Java 8 è stato usato in progetti reali, c'è ancora un'ortodossia sull'uso dei metodi predefiniti? Quello che vedo per lo più quando vengono discussi riguarda come aggiungere nuovi metodi a un'interfaccia senza rompere i consumatori esistenti. Ma per quanto riguarda l'utilizzo di questo fin dall'inizio come nell'esempio che ho dato sopra. Qualcuno ha avuto problemi con la fornitura di implementazioni nelle loro interfacce?

    
posta JimmyJames 24.04.2017 - 19:27
fonte

1 risposta

12

Un ottimo caso d'uso sono quelle che io chiamo interfacce "leva": interfacce che hanno solo un piccolo numero di metodi astratti (idealmente 1), ma forniscono un sacco di "effetto leva" in quanto forniscono molte funzionalità : devi solo implementare 1 metodo nella tua classe, ma ottenere molti altri metodi "gratuitamente". Pensa ad un'interfaccia di raccolta, ad esempio, con un singolo metodo foreach astratto e metodi default come map , fold , reduce , filter , partition , groupBy , sort , sortBy , ecc.

Ecco un paio di esempi. Iniziamo con java.util.function.Function<T, R> . Ha un solo metodo astratto R apply<T> . E ha due metodi predefiniti che consentono di comporre la funzione con un'altra funzione in due modi diversi, prima o dopo. Entrambi i metodi di composizione sono implementati utilizzando 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));
}

Potresti anche creare un'interfaccia per oggetti comparabili, qualcosa del genere:

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 framework di raccolte estremamente semplificato, in cui tutte le operazioni delle collezioni restituiscono Collection , indipendentemente dal tipo originale:

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

Questo diventa molto interessante in combinazione con lambda, perché tale interfaccia "leva" può essere implementata da un lambda (è un'interfaccia SAM).

Questo è lo stesso caso d'uso in cui sono stati aggiunti i metodi di estensione in C♯, ma i metodi predefiniti hanno un vantaggio distinto: sono metodi di istanza "appropriati", il che significa che hanno accesso ai dettagli di implementazione privati dell'interfaccia (% i metodi di interfaccia diprivate sono in arrivo in Java 9), mentre i metodi di estensione sono solo zucchero sintattico per i metodi statici.

Se Java dovesse mai avere Interface Injection, permetterebbe anche il patch-patching sicuro, con scope, modulare. Ciò sarebbe molto interessante per gli implementatori linguistici sulla JVM: al momento, ad esempio, JRuby eredita o avvolge le classi Java per fornire loro semantica aggiuntiva Ruby, ma idealmente, vogliono usare le stesse classi. Con l'iniezione di interfaccia e i metodi predefiniti, potrebbero iniettarsi per es. un'interfaccia RubyObject in java.lang.Object , in modo che una% di Java% co_de e una% di% di Ruby siano la stessa cosa esatta .

    
risposta data 24.04.2017 - 22:03
fonte

Leggi altre domande sui tag