Функциональные интерфейсы

Определение

Функциональный интерфейс имеет один абстрактный метод (SAM — single abstract method). Его экземпляром может быть лямбда, ссылка на метод или обычный класс.

@FunctionalInterface
interface Validator<T> {
    boolean valid(T value);
}
 
Validator<String> notBlank = value -> !value.isBlank();

@FunctionalInterface необязательна для работы, но фиксирует намерение: компилятор сообщит, если контракт перестал быть функциональным.

Что считается абстрактным методом

Интерфейс может содержать:

  • любое число default-методов;
  • static-методы;
  • private helper-методы;
  • один абстрактный SAM.

Методы, совпадающие с public-методами Object, не добавляют отдельный функциональный дескриптор:

@FunctionalInterface
interface KeyMatcher<T> {
    boolean matches(T value);
 
    boolean equals(Object other);
 
    default KeyMatcher<T> negate() {
        return value -> !matches(value);
    }
}

Абстрактные методы, унаследованные из нескольких родителей, могут схлопнуться в один совместимый контракт. Если их сигнатуры/результаты несовместимы, интерфейс не функциональный.

Target type

У лямбды нет самостоятельного типа: смысл параметров и результата задаёт целевой функциональный интерфейс.

Predicate<String> check = text -> !text.isBlank();
Function<String, Boolean> function = text -> !text.isBlank();

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

Generic SAM

Сам интерфейс может быть обобщён:

@FunctionalInterface
interface Mapper<T, R> {
    R map(T source);
}

Но абстрактный метод, который сам является универсальным для любого T, обычно нельзя реализовать простой мономорфной лямбдой:

interface UniversalFactory {
    <T> T create(Class<T> type);
}

Его контракт требует уметь вернуть любой запрошенный тип.

Checked exceptions

Лямбда может выбросить checked-исключение только если это разрешено SAM:

@FunctionalInterface
interface IoSupplier<T> {
    T get() throws IOException;
}
 
IoSupplier<String> reader = () -> Files.readString(path);

Стандартный Supplier<T> не объявляет IOException, поэтому checked-ошибку приходится обработать, адаптировать или выбрать собственный интерфейс. Безымянное заворачивание всего в RuntimeException теряет смысл ошибки.

Композиция поведения

Функциональный интерфейс может дать доменные комбинаторы:

@FunctionalInterface
interface Rule<T> {
    boolean accepts(T value);
 
    default Rule<T> and(Rule<T> other) {
        return value -> accepts(value) && other.accepts(value);
    }
}

Так правила объединяются без общей mutable-конфигурации.

Ловушки

  • Считать, что у функционального интерфейса должен быть ровно один метод вообще.
  • Забывать @FunctionalInterface на публичном SAM-контракте.
  • Создавать собственный интерфейс, когда стандартный точно выражает роль.
  • Использовать Function<T, Boolean> вместо Predicate<T>.
  • Игнорировать checked-исключения в дизайне SAM.
  • Перегружать метод разными функциональными интерфейсами с одинаковой формой лямбды.

Вопросы на собеседовании

  1. Как компилятор определяет функциональный интерфейс?
  2. Могут ли в нём быть default, static и private-методы?
  3. Обязательна ли @FunctionalInterface?
  4. Что такое target typing лямбды?
  5. Как функциональный интерфейс работает с checked-исключением?
  6. Считается ли equals(Object) вторым абстрактным методом?

Связанные заметки

Лямбда выражения · ФИ стандартной библиотеки Java · Функциональное программирование · Интерфейсы Java · Обобщения generics