Функциональные интерфейсы
Определение
Функциональный интерфейс имеет один абстрактный метод (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.
- Перегружать метод разными функциональными интерфейсами с одинаковой формой лямбды.
Вопросы на собеседовании
- Как компилятор определяет функциональный интерфейс?
- Могут ли в нём быть default, static и private-методы?
- Обязательна ли
@FunctionalInterface? - Что такое target typing лямбды?
- Как функциональный интерфейс работает с checked-исключением?
- Считается ли
equals(Object)вторым абстрактным методом?
Связанные заметки
Лямбда выражения · ФИ стандартной библиотеки Java · Функциональное программирование · Интерфейсы Java · Обобщения generics