Статьи

Вы будете сожалеть о применении перегрузки с Lambdas!

Писать хорошие API сложно. Очень трудно. Вы должны думать о невероятном количестве вещей, если хотите, чтобы ваши пользователи любили ваш API. Вы должны найти правильный баланс между:

  1. Полезность
  2. Удобство использования
  3. Обратная совместимость
  4. Прямая совместимость

Мы уже писали об этой теме в нашей статье: Как разработать хороший, регулярный API . Сегодня мы рассмотрим, как …

Java 8 меняет правила

ryNBGfQ

Да!

Перегрузка – хороший инструмент для обеспечения удобства в двух измерениях:

  • Предоставляя альтернативы типа аргумента
  • Предоставляя значения аргумента по умолчанию

Примеры вышеупомянутого от JDK включают в себя:

01
02
03
04
05
06
07
08
09
10
11
12
public class Arrays {
 
    // Argument type alternatives
    public static void sort(int[] a) { ... }
    public static void sort(long[] a) { ... }
 
    // Argument default values
    public static IntStream stream(int[] array) { ... }
    public static IntStream stream(int[] array,
        int startInclusive,
        int endExclusive) { ... }
}

JOOQ API явно полон такого удобства. Поскольку jOOQ является DSL для SQL , мы можем даже немного злоупотреблять:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public interface DSLContext {
    <T1> SelectSelectStep<Record1<T1>>
        select(SelectField<T1> field1);
 
    <T1, T2> SelectSelectStep<Record2<T1, T2>>
        select(SelectField<T1> field1,
               SelectField<T2> field2);
 
    <T1, T2, T3> SelectSelectStep<Record3<T1, T2, T3>> s
        select(SelectField<T1> field1,
               SelectField<T2> field2,
               SelectField<T3> field3);
 
    <T1, T2, T3, T4> SelectSelectStep<Record4<T1, T2, T3, T4>>
        select(SelectField<T1> field1,
               SelectField<T2> field2,
               SelectField<T3> field3,
               SelectField<T4> field4);
 
    // and so on...
}

Такие языки, как Ceylon, развивают эту идею удобства на один шаг вперед, утверждая, что вышеизложенное является единственной разумной причиной, по которой перегрузка используется в Java. И, таким образом, создатели Ceylon полностью удалили перегрузку из своего языка, заменив вышеприведенные типы объединения и фактические значения по умолчанию для аргументов. Например

1
2
3
4
5
6
7
// Union types
void sort(int[]|long[] a) { ... }
 
// Default argument values
IntStream stream(int[] array,
    int startInclusive = 0,
    int endInclusive = array.length) { ... }

Прочитайте «10 лучших возможностей языка Цейлона, которые я хотел бы иметь на Java» для получения дополнительной информации о Цейлоне.

К сожалению, в Java мы не можем использовать типы объединения или значения по умолчанию для аргументов. Поэтому мы должны использовать перегрузку, чтобы предоставить нашим пользователям API удобные методы.

Однако если аргумент вашего метода является функциональным интерфейсом , между Java 7 и Java 8 ситуация резко изменилась в связи с перегрузкой метода. Пример приведен здесь из JavaFX.

«Недружественный» список ObservableList от JavaFX

JavaFX расширяет типы коллекций JDK, делая их «наблюдаемыми». Не следует путать с Observable , типом динозавров из JDK 1.0 и дней, предшествовавших колебанию.

Собственный JavaFX Observable выглядит следующим образом:

1
2
3
4
public interface Observable {
  void addListener(InvalidationListener listener);
  void removeListener(InvalidationListener listener);
}

И, к счастью, этот InvalidationListener является функциональным интерфейсом:

1
2
3
4
@FunctionalInterface
public interface InvalidationListener {
  void invalidated(Observable observable);
}

Это здорово, потому что мы можем делать такие вещи, как:

1
2
3
Observable awesome =
    FXCollections.observableArrayList();
awesome.addListener(fantastic -> splendid.cheer());

(обратите внимание, как я заменил foo / bar / baz на более веселые термины. Мы все должны это сделать. Foo и bar так 1970 )

К сожалению, вещи становятся более пугающими, когда мы делаем то, что мы, вероятно, сделаем вместо этого. Т.е. вместо того, чтобы объявлять Observable , мы бы хотели, чтобы это был гораздо более полезный ObservableList :

1
2
3
ObservableList<String> awesome =
    FXCollections.observableArrayList();
awesome.addListener(fantastic -> splendid.cheer());

Но теперь мы получаем ошибку компиляции во второй строке:

1
2
3
4
awesome.addListener(fantastic -> splendid.cheer());
//      ^^^^^^^^^^^
// The method addListener(ListChangeListener<? super String>)
// is ambiguous for the type ObservableList<String>

Потому что, по сути …

1
2
3
4
public interface ObservableList<E>
extends List<E>, Observable {
    void addListener(ListChangeListener<? super E> listener);
}

и…

1
2
3
4
@FunctionalInterface
public interface ListChangeListener<E> {
    void onChanged(Change<? extends E> c);
}

Теперь снова, до Java 8, два типа слушателей были совершенно однозначно различимы, и они все еще остаются. Вы можете легко вызвать их, передав именованный тип. Наш оригинальный код все еще работал бы, если бы мы написали:

1
2
3
4
5
ObservableList<String> awesome =
    FXCollections.observableArrayList();
InvalidationListener hearYe =
    fantastic -> splendid.cheer();
awesome.addListener(hearYe);

Или же…

1
2
3
4
ObservableList<String> awesome =
    FXCollections.observableArrayList();
awesome.addListener((InvalidationListener)
    fantastic -> splendid.cheer());

Или даже…

1
2
3
4
ObservableList<String> awesome =
    FXCollections.observableArrayList();
awesome.addListener((Observable fantastic) ->
    splendid.cheer());

Все эти меры устранят двусмысленность. Но, откровенно говоря, лямбды – это вдвое хуже, если вам нужно явно ввести лямбда или типы аргументов. У нас есть современные IDE, которые могут выполнять автозаполнение и выводить типы так же, как и сам компилятор.

Представьте, что мы действительно хотим вызвать другой addListener() , тот, который принимает ListChangeListener. Мы должны написать любой из

1
2
3
4
5
6
7
ObservableList<String> awesome =
    FXCollections.observableArrayList();
 
// Agh. Remember that we have to repeat "String" here
ListChangeListener<String> hearYe =
    fantastic -> splendid.cheer();
awesome.addListener(hearYe);

Или же…

1
2
3
4
5
6
ObservableList<String> awesome =
    FXCollections.observableArrayList();
 
// Agh. Remember that we have to repeat "String" here
awesome.addListener((ListChangeListener<String>)
    fantastic -> splendid.cheer());

Или даже…

1
2
3
4
5
6
ObservableList<String> awesome =
    FXCollections.observableArrayList();
 
// WTF... "extends" String?? But that's what this thing needs...
awesome.addListener((Change<? extends String> fantastic) ->
    splendid.cheer());

Перегрузка у тебя не будет. Будьте осторожны, вы должны.

API дизайн сложен. Раньше было тяжело, сейчас стало сложнее. В Java 8, если какой-либо из аргументов ваших методов API является функциональным интерфейсом, подумайте дважды о перегрузке этого метода API. И как только вы решили продолжить перегрузку, подумайте еще раз, в третий раз, действительно ли это хорошая идея.

Не убежден? Внимательно посмотрите на JDK. Например, тип java.util.stream.Stream . Как вы видите, сколько перегруженных методов имеют одинаковое количество аргументов функционального интерфейса, которые снова принимают такое же количество аргументов метода (как в нашем предыдущем addListener() )?

Нуль.

Существуют перегрузки, в которых номера аргументов перегрузки отличаются. Например:

1
2
3
4
5
<R> R collect(Supplier<R> supplier,
              BiConsumer<R, ? super T> accumulator,
              BiConsumer<R, R> combiner);
 
<R, A> R collect(Collector<? super T, A, R> collector);

Вы никогда не будете иметь никакой двусмысленности при вызове collect() .

Но когда номера аргументов не различаются, и при этом номера аргументов собственных методов не различаются, имена методов различаются. Например:

1
2
3
4
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
IntStream mapToInt(ToIntFunction<? super T> mapper);
LongStream mapToLong(ToLongFunction<? super T> mapper);
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);

Теперь это очень раздражает на сайте вызовов, потому что вы должны заранее подумать, какой метод использовать, основываясь на множестве задействованных типов.

Но это действительно единственное решение этой дилеммы. Итак, помните: вы будете сожалеть, применяя перегрузку с лямбдами!