Функциональные интерфейсы Java
Интерфейсы с одним абстрактным методом в Java, служащие целевыми типами для лямбда-выражений и отмеченные @FunctionalInterface.
Функциональный интерфейс — это интерфейс ровно с одним абстрактным методом. Именно к этому методу компилируется лямбда или ссылка на метод. Runnable, Comparator<T>, Callable<V>, Supplier<T>, Function<T, R>, Predicate<T>, Consumer<T>, ActionListener, FileFilter — всё это функциональные интерфейсы. В JDK их уже десятки, и вы будете создавать собственные, когда ни один из них не подойдёт.
В предыдущей главе были показаны лямбды вида () -> 42 и s -> s.length(), которые «компилируются в любой интерфейс, требуемый контекстом». Эта глава отвечает на вопрос что делает интерфейс допустимым целевым типом — правило единственного абстрактного метода (SAM) — и как аннотация @FunctionalInterface позволяет сказать: «да, это функциональный интерфейс, и я хочу, чтобы компилятор это проверял».
Правило SAM, точная формулировка
Чтобы быть функциональным, интерфейс должен объявлять ровно один метод, требующий реализации. Формулировка важна: не «ровно один метод вообще», а «ровно один абстрактный метод». Три категории методов не засчитываются в это количество:
- Методы
default— у них уже есть тело, поэтому реализатору не нужно его предоставлять. - Методы
static— они принадлежат самому интерфейсу, а не его реализаторам. publicабстрактные методы, переопределяющие методыjava.lang.Object— например,equals,hashCode,toString. Каждый класс уже наследует реализации отObject, поэтому повторное объявление таких методов в интерфейсе не добавляет нового требования.
Третий пункт удивляет людей. Comparator<T> объявляет boolean equals(Object), но всё равно остаётся функциональным, потому что этот метод берётся из Object. Настоящий абстрактный метод — int compare(T, T).
@FunctionalInterface
interface MyComparator<T> {
int compare(T a, T b); // the one SAM
boolean equals(Object other); // Object override — doesn't count
default MyComparator<T> reversed() { // default — doesn't count
return (a, b) -> compare(b, a);
}
static <T extends Comparable<T>> MyComparator<T> natural() { // static — doesn't count
return (a, b) -> a.compareTo(b);
}
}@FunctionalInterface — проверка на этапе компиляции по желанию
Аннотация является необязательной. Интерфейс считается функциональным по своей форме, а не из-за наличия аннотации. Но её использование даёт два преимущества:
- Ошибка компиляции, если интерфейс перестаёт быть функциональным. Случайно добавьте второй абстрактный метод — и компилятор остановит вас немедленно, прямо в интерфейсе, а не на каждом месте вызова, где он используется как целевой тип лямбды.
- Документация. Аннотация сигнализирует: «этот интерфейс предназначен для использования в качестве целевого типа лямбды», что ценно для любого неочевидного случая.
@FunctionalInterface
interface Validator<T> {
boolean isValid(T value);
boolean isInvalid(T value); // <-- compile error: not a functional interface
}Без аннотации второй метод молча превратил бы Validator<T> в нефункциональный интерфейс, и первое место вызова в стиле лямбды, использующее его, не скомпилировалось бы с запутанным сообщением об ошибке, далёким от истинной причины.
Аннотация является также соглашением для собственных функциональных интерфейсов JDK — Function, Predicate, Consumer, Supplier, Runnable, Callable — все они её несут.
Лямбды, ссылки на методы и анонимные классы взаимозаменяемы
Функциональный интерфейс принимает три вида значений, и они свободно взаимозаменяемы:
Predicate<String> blank1 = s -> s.trim().isEmpty(); // lambda
Predicate<String> blank2 = String::isBlank; // method reference (since Java 11)
Predicate<String> blank3 = new Predicate<>() { // anonymous class
@Override public boolean test(String s) { return s.trim().isEmpty(); }
};Все три реализуют один и тот же интерфейс Predicate<String> и дают эквивалентные значения в точке вызова. Лямбда и ссылка на метод значительно короче; анонимный класс оставлен для редких случаев, перечисленных в предыдущей главе (нужно больше одного метода, локальное состояние метода, this ссылается на новый экземпляр).
Обобщённые функциональные интерфейсы
Интерфейс может быть параметризован — именно так одно объявление Function<T, R> может использоваться для любых преобразований:
@FunctionalInterface
interface Mapper<T, R> {
R map(T input);
}
Mapper<String, Integer> length = s -> s.length();
Mapper<Integer, String> hex = n -> Integer.toHexString(n);Параметры могут быть ограниченными, содержать несколько переменных типа и использоваться повторно в разных интерфейсах — стандартная библиотека применяет все эти варианты.
Создание собственного функционального интерфейса
В большинстве случаев следует использовать встроенные интерфейсы из java.util.function — следующая глава подробно их рассматривает. Создавайте собственные, когда:
- Семантика заслуживает имени.
Validator<T>читается лучше в точке вызова, чемFunction<T, ValidationResult>, даже если форма совпадает. - Нужно проверяемое исключение.
Function.applyне объявляет проверяемых исключений; если ваша операция выбрасываетIOException, напишите SAM, который его объявляет. - Нужной формы нет в стандартной библиотеке. Метод с тремя аргументами (трифункция) не имеет встроенного интерфейса — напишите его, когда понадобится.
@FunctionalInterface
interface IOFunction<T, R> {
R apply(T input) throws IOException;
}
IOFunction<Path, String> readAll = Files::readString; // declared exception — built-in Function can'tУдивительно большая часть вопроса «стоит ли писать это?» сводится либо к читаемости, либо к распространению исключений.
Методы default оправдывают себя
Единственный случай, когда вы будете создавать собственный функциональный интерфейс и добавлять к нему методы default — это когда вы хотите позволить вызывающим сторонам компоновать экземпляры:
@FunctionalInterface
interface Filter<T> {
boolean keep(T value);
default Filter<T> and(Filter<T> other) {
return v -> keep(v) && other.keep(v);
}
default Filter<T> negate() {
return v -> !keep(v);
}
}
Filter<Integer> positive = n -> n > 0;
Filter<Integer> even = n -> n % 2 == 0;
Filter<Integer> posOdd = positive.and(even.negate());Именно этот рецепт использует JDK для Predicate.and / or / negate, Function.andThen / compose и Comparator.thenComparing. Единственный абстрактный метод — это поведение; методы default — это алгебра композиции, которая его окружает.
Разбор примера: создание, аннотирование, компоновка
Программа ниже определяет функциональный интерфейс Filter<T> с двумя методами default, демонстрирует правило SAM (второй абстрактный метод не скомпилировался бы) и показывает, как лямбды, ссылки на методы и анонимный класс реализуют один и тот же SAM.
Что вынести из этого примера:
notBlank1(лямбда),notBlank2(цепочка ссылок на методы) иnotBlank3(анонимный класс) — все реализуют один и тот же интерфейсFilter<String>и взаимозаменяемы. Лямбда самая короткая; анонимный класс оставлен для случаев, с которыми лямбды не справляются.positive.and(even.negate())скомпоновал три фильтра в один без каких-либо дополнительных объявлений методов. Методыdefaultandиnegateна интерфейсе — это алгебра композиции, именно поэтому JDK добавляет их кPredicate,FunctionиComparator.SafelyFunctional<T>объявляет иapply(T), иboolean equals(Object), и всё равно скомпилировался с@FunctionalInterface. Переопределениеequalsнаследуется отObject, поэтому не засчитывается в правило единственного абстрактного метода.- Если убрать ключевое слово
defaultвFilter(превратив один default-метод во второй абстрактный), аннотация@FunctionalInterfaceнемедленно вызовет ошибку компиляции прямо в объявлении интерфейса — задолго до того, как любое место вызова лямбды увидит запутанные ошибки вывода типов.
Что дальше
Теперь вы умеете распознавать функциональный интерфейс, создавать его, когда JDK не предоставляет нужного, и позволять компилятору проверять его форму. Почти всегда, однако, правильный ответ — «использовать то, что уже есть». Следующая глава, Встроенные функциональные интерфейсы Java, знакомит с java.util.function — Function, Predicate, Consumer, Supplier, их двухаргументными вариантами и примитивными специализациями, созданными во избежание упаковки.