Domyślne użycie metody Java

13

Przez dziesięciolecia to było tak, że interfejsy były tylko tylko (tylko) dla określenia podpisy metod. Powiedziano nam, że to „właściwy sposób na robienie rzeczy”.

Potem pojawiła się Java 8 i powiedziała:

Teraz możesz zdefiniować domyślne metody. Muszę uciekać, pa.

Jestem ciekawy, w jaki sposób jest to trawione zarówno przez doświadczonych programistów Java, jak i tych, którzy zaczęli go ostatnio (w ostatnich latach) rozwijać. Zastanawiam się także, jak to pasuje do ortodoksji i praktyki Java.

Buduję trochę kodu eksperymentalnego i kiedy robiłem trochę refaktoryzacji, skończyłem z interfejsem, który po prostu rozszerza standardowy interfejs (Iterable) i dodaje dwie domyślne metody. I będę szczera, czuję się z tym cholernie dobrze.

Wiem, że jest to trochę otwarte, ale teraz, gdy było już trochę czasu, aby Java 8 mogła być używana w prawdziwych projektach, czy istnieje ortodoksja dotycząca korzystania z domyślnych metod? To, co najczęściej widzę, kiedy są omawiane, dotyczy sposobu dodawania nowych metod do interfejsu bez niszczenia istniejących konsumentów. Ale co z użyciem tego od samego początku, jak w przykładzie podanym powyżej. Czy ktoś napotkał jakiekolwiek problemy z implementacją w swoich interfejsach?

JimmyJames
źródło
Byłbym również zainteresowany tą perspektywą. Wracam do Javy po 6 latach w świecie .Net. Wydaje mi się, że może to być odpowiedź Javy na metody rozszerzenia C #, przy odrobinie wpływu metod modułowych Ruby. Nie grałem z tym, więc nie jestem pewien.
Berin Loritsch
1
Wydaje mi się, że powodem dodania domyślnych metod jest w dużej mierze umożliwienie rozszerzenia interfejsów kolekcji bez konieczności tworzenia zupełnie innych interfejsów
Justin
1
@ Justin: sprawdź java.util.function.Functionużycie metod domyślnych w zupełnie nowym interfejsie.
Jörg W Mittag,
@Justin Domyślam się, że był to główny sterownik. Naprawdę powinienem znów zacząć zwracać uwagę na ten proces, ponieważ oni naprawdę wprowadzili zmiany.
JimmyJames,

Odpowiedzi:

12

Świetnym przykładem użycia są interfejsy „dźwigniowe”: interfejsy, które mają tylko niewielką liczbę abstrakcyjnych metod (najlepiej 1), ale zapewniają wiele „dźwigni”, ponieważ zapewniają wiele funkcji: tylko Ty trzeba zaimplementować 1 metodę w swojej klasie, ale uzyskać wiele innych metod „za darmo”. Pomyśl o interfejsie zbiórki, na przykład, za pomocą jednego abstrakcyjnego foreachsposobu i defaultmetod, takich jak map, fold, reduce, filter, partition, groupBy, sort, sortBy, itd.

Oto kilka przykładów. Zacznijmyjava.util.function.Function<T, R> . Ma jedną abstrakcyjną metodę R apply<T>. I ma dwie domyślne metody, które pozwalają komponować funkcję z inną funkcją na dwa różne sposoby, przed lub po. Obie te metody kompozycji są implementowane przy użyciuapply :

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

Możesz także utworzyć interfejs dla porównywalnych obiektów, coś takiego:

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

Lub wyjątkowo uproszczona struktura kolekcji, w której zwracane są wszystkie operacje kolekcji Collection, niezależnie od tego, jaki był oryginalny typ:

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

Staje się to bardzo interesujące w połączeniu z lambdas, ponieważ taki interfejs „dźwigniowy” może być zaimplementowany przez lambda (jest to interfejs SAM).

Jest to ten sam przypadek użycia, dla którego metody C zostały dodane w C♯, ale metody domyślne mają jedną wyraźną zaletę: są to „właściwe” metody instancji, co oznacza, że ​​mają dostęp do prywatnych szczegółów implementacyjnych interfejsu ( privatenadchodzą metody interfejsu w Javie 9), podczas gdy metody rozszerzeń są jedynie cukrem syntaktycznym dla metod statycznych.

Gdyby Java kiedykolwiek otrzymała interfejs wstrzykiwania, pozwoliłaby również na bezpieczne, modułowe, łatanie małp w poprawnym typie. Byłoby to bardzo interesujące dla implementatorów języka na JVM: na przykład JRuby dziedziczy lub otacza klasy Java, aby zapewnić im dodatkową semantykę Ruby, ale idealnie chcą używać tych samych klas. Dzięki interfejsowi wstrzykiwania i metodom domyślnym mogą wstrzykiwać np. RubyObjectInterfejs java.lang.Object, tak aby Java Objecti Ruby Objectbyły dokładnie tym samym .

Jörg W Mittag
źródło
1
Nie do końca to śledzę. Domyślna metoda interfejsu musi być zdefiniowana w kategoriach innych metod interfejsu lub metod zdefiniowanych w Object. Czy możesz podać przykład tworzenia sensownego interfejsu pojedynczej metody za pomocą metody domyślnej? Jeśli potrzebujesz demonstracji składni Java 9, nie ma sprawy.
JimmyJames
Na przykład: Comparableinterfejs z abstrakcyjnej compareTometody i domyślnie lessThan, lessThanOrEqual, greaterThan, greaterThanOrEqual, isBetween, oraz clampsposoby, wszystko realizowane w kategoriach compareTo. Lub spójrz tylko java.util.function.Function: ma applymetodę abstrakcyjną i dwie domyślne metody kompozycji, obie zaimplementowane pod względem apply. Próbowałem podać przykład Collectioninterfejsu, ale uzyskanie bezpieczeństwa typu jest trudne i zbyt długie, aby odpowiedzieć na tę odpowiedź - postaram się dać szansę wersji nie bezpiecznej i nie zachowującej typu. Bądźcie czujni.
Jörg W Mittag,
3
Przykłady pomagają. Dzięki. Źle zrozumiałem, co masz na myśli przez interfejs jednej metody.
JimmyJames,
Domyślne metody oznaczają, że pojedynczy interfejs metody abstrakcyjnej nie musi już być interfejsem jednej metody ;-)
Jörg W Mittag
Myślałem o tym i przyszło mi do głowy, że AbstractCollection i AbstractList są w zasadzie tym, o czym tu mówisz (metoda 2 zamiast 1, ale nie sądzę, żeby to miało kluczowe znaczenie). Gdyby były one przekształcone jako interfejsy z defualt metodami, byłoby bardzo łatwo jest przekształcić iterowalną kolekcję, dodając rozmiar i tworząc listę z czegokolwiek, co jest również bardzo proste, jeśli możesz indeksować i znać rozmiar.
JimmyJames 28.04.17