Статьи

Дизайнеры API, будьте осторожны

Lean Functional API Design

С Java 8 дизайн API стал намного интереснее, но и немного сложнее. Как успешному разработчику API больше не будет достаточно думать о всевозможных объектно-ориентированных аспектах вашего API, теперь вам также нужно будет рассмотреть его функциональные аспекты. Другими словами, вместо простого предоставления таких методов, как:

void performAction(Parameter parameter);

// Call the above:
object.performAction(new Parameter(...));

… Теперь вы должны подумать о том, лучше ли моделировать аргументы вашего метода как функции для отложенной оценки

// Keep the existing method for convenience
// and for backwards compatibility
void performAction(Parameter parameter);

// Overload the existing method with the new
// functional one:
void performAction(Supplier<Parameter> parameter);

// Call the above:
object.performAction(() -> new Parameter(...));

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

JDK зависимость

В приведенном выше примере используется Supplier тип JDK 8  . Этот тип недоступен до JDK 8, поэтому, если вы используете его, вы ограничите использование своих API JDK 8. Если вы хотите продолжить поддерживать более старые версии Java, вам придется свернуть свой собственный поставщик, или, возможно, использовать  Callable, который был доступен с Java 5:

// Overload the existing method with the new
// functional one:
void performAction(Callable<Parameter> parameter);

// Call the above:
object.performAction(() -> new Parameter(...));

Одним из преимуществ использования  Callable является тот факт, что ваши лямбда-выражения (или «классические»  Callable реализации, или вложенные / внутренние классы) могут генерировать проверенные исключения. Мы сообщили о другой возможности обойти это ограничение здесь .

перегрузка

Хотя (возможно) вполне нормально перегружать эти два метода

void performAction(Parameter parameter);
void performAction(Supplier<Parameter> parameter);

… Вы должны быть осторожны при перегрузке «более похожих» методов, таких как эти:

void performAction(Supplier<Parameter> parameter);
void performAction(Callable<Parameter> parameter);

Если вы создадите вышеупомянутый API, клиентский код вашего API не сможет использовать лямбда-выражения, поскольку нет способа избавиться от двусмысленности лямбда-выражения от лямбда-выражения  Supplier a  CallableМы также упоминали об этом в предыдущем сообщении в блоге .

«Void-совместимый» против «совместимый по стоимости»

Я недавно (повторно) обнаружил  эту интересную раннюю ошибку компилятора JDK 8 , когда компилятор не смог устранить неоднозначность следующего:

void run(Consumer<Integer> consumer);
void run(Function<Integer, Integer> function);

// Remember, the above types are roughly:
interface Consumer<T> {
    void accept(T t);
//  ^^^^ void-compatible
}

interface Function<T, R> {
    R apply(T t);
//  ^ value-compatible
}

Термины «void-совместимый» и «совместимый по значению» определены в  JLS § 15.27.2  для лямбда-выражений. Согласно JLS, следующие два вызова не являются   неоднозначными:

// Only run(Consumer) is applicable
run(i -> {});

// Only run(Function) is applicable
run(i -> 1);

Другими словами, безопасно перегрузить метод для получения двух «похожих» типов аргументов, таких как  Consumer и  Function, поскольку лямбда-выражения, используемые для выражения аргументов метода, не будут неоднозначными.

Это очень полезно, потому что иметь  необязательное  возвращаемое значение очень элегантно, когда вы используете лямбда-выражения. Рассмотрим предстоящий API транзакций jOOQ 3.4 , который в  общих чертах представлен следующим образом:

// This uses a "void-compatible" lambda
ctx.transaction(c -> {
    DSL.using(c).insertInto(...).execute();
    DSL.using(c).update(...).execute();
});

// This uses a "value-compatible" lambda
Integer result =
ctx.transaction(c -> {
    DSL.using(c).update(...).execute();
    DSL.using(c).delete(...).execute();

    return 42;
});

В приведенном выше примере первый вызов разрешается в  TransactionalRunnableто время как второй вызов разрешается,  TransactionalCallable чей API похожи на эти:

interface TransactionalRunnable {
    void run(Configuration c) throws Exception;
}

interface TransactionalCallable<T> {
    T run(Configuration c) throws Exception;
}

Обратите внимание, что, начиная с JDK 1.8.0_05 и Eclipse Kepler ( с патчем поддержки Java 8 ), это разрешение неоднозначности еще не работает из-за следующих ошибок:

Таким образом, чтобы оставаться в безопасности, возможно, вы могли бы просто избежать перегрузки.

Универсальные методы не являются SAM

Обратите внимание, что интерфейсы «SAM», содержащие один абстрактный  обобщенный  метод, НЕ являются   SAM в том смысле, что они могут быть использованы в качестве целей лямбда-выражений. Следующий тип никогда не будет образовывать лямбда-выражения:

interface NotASAM {
    <T> void run(T t);
}

Это указано в  JLS §15.27.3

Лямбда-выражение совпадает с типом функции, если выполняются все следующие условия:

  • Тип функции не имеет параметров типа.
  • […]

Что ты должен сделать сейчас?

Если вы дизайнер API, вам следует начать писать модульные тесты / интеграционные тесты также в Java 8. Почему? По той простой причине, что если вы этого не сделаете, вы неправильно поймете свой API для тех пользователей, которые фактически используют его с Java 8.  Эти вещи чрезвычайно тонки . Чтобы понять их правильно, требуется немного практики и много регрессионных тестов. Как вы думаете, вы хотите перегрузить метод? Убедитесь, что вы не нарушаете API-интерфейс клиента, который вызывает оригинальный метод с помощью лямбды.

Вот и все на сегодня. Оставайтесь с нами для  более удивительного контента Java 8 в этом блоге .