Функциональный интерфейс в Java
У интерфейса java.util.Comparator объявлено два абстрактных метода: compare(T, T) и equals(Object). Тем не менее строка Comparator<Car> byCost = (a, b) -> a.getCostUSD() - b.getCostUSD(); прекрасно компилируется, а сама аннотация @FunctionalInterface висит на Comparator совершенно законно. Понимание того, почему второй абстрактный метод «не считается», и есть ключ ко всей теме.
Функциональный интерфейс (functional interface) в Java — это интерфейс ровно с одним абстрактным методом. Такой интерфейс может быть целевым типом лямбда-выражения или ссылки на метод. Количество default-, static- и private-методов при этом не ограничено.
1. Что такое функциональный интерфейс
Лямбда-выражение не существует само по себе: ему всегда нужен тип. Этим типом и служит функциональный интерфейс — интерфейс с единственным абстрактным методом (SAM, Single Abstract Method).
public interface Searchable {
boolean test(Car car);
} Теперь интерфейс можно реализовать лямбда-выражением:
Searchable s = (Car c) -> c.getCostUSD() > 20000;
// или короче, тип параметра выводится компилятором:
Searchable s2 = c -> c.getCostUSD() > 20000; Формально требования к функциональному интерфейсу такие:
- Ровно один абстрактный метод.
- Любое количество
default-,static-,private- иprivate static-методов (последние два — начиная с Java 9). - Абстрактные методы, повторяющие
public-методы классаObject(equals,hashCode,toString), при подсчёте не учитываются.
Пример интерфейса с default-методом — он остаётся функциональным:
interface A {
default int defaultMethod() {
return 0;
}
void method(); // единственный абстрактный метод
} default-методов может быть сколько угодно:
interface B {
default int defaultMethod() {
return 0;
}
default int anotherDefaultMethod() {
return 0;
}
private int helper() { // Java 9+
return 42;
}
void method();
} И тот самый случай с методами Object — интерфейс тоже функциональный, потому что абстрактным считается только method():
interface C {
boolean equals(Object o);
int hashCode();
String toString();
void method(); // фактически единственный абстрактный метод
} Почему equals не считается
Любой класс, реализующий интерфейс, автоматически наследует реализации equals, hashCode и toString от Object. Значит, реализовывать их лямбде не нужно — компилятор исключает такие объявления из подсчёта абстрактных методов. Именно поэтому Comparator с его compare и equals остаётся функциональным интерфейсом.
Примеры функциональных интерфейсов из стандартной библиотеки: java.lang.Runnable, java.util.Comparator, java.util.concurrent.Callable, java.lang.Iterable. У Iterable, например, есть два default-метода (forEach и spliterator), но абстрактный по-прежнему один — iterator(). Проверять интерфейс всегда нужно по количеству именно абстрактных методов, а не всех объявленных.
2. Целевой тип: откуда лямбда узнаёт свой интерфейс
Само лямбда-выражение не содержит информации о том, какой интерфейс оно реализует. Тип выводится из контекста, в котором лямбда используется, и называется целевым типом (target type).
Целевой тип берётся из:
- объявления переменной (
Searchable s = c -> ...;); - параметра метода (
list.removeIf(c -> ...)); - возвращаемого значения метода;
- приведения типа (
(Searchable) c -> ...); - тернарного оператора и инициализатора массива.
Отсюда следствие: одно и то же лямбда-выражение подходит разным интерфейсам, если сигнатуры их абстрактных методов совместимы.
interface Searchable {
boolean test(Car car);
}
interface Saleable {
boolean approve(Car car);
}
//...
Searchable s1 = c -> c.getCostUSD() > 20000;
Saleable s2 = c -> c.getCostUSD() > 20000; Обратите внимание: имена методов (test и approve) не совпадают — это неважно. Значение имеют только список параметров, тип возвращаемого значения и объявленные исключения.
По той же причине не компилируется код, в котором целевого типа нет:
// Ошибка компиляции: целевой тип неизвестен
var f = () -> System.out.println("hi");
// Ошибка компиляции: Object не функциональный интерфейс
Object o = c -> c.getCostUSD() > 20000; 3. Какие методы разрешены в функциональном интерфейсе
| Вид метода | Сколько допустимо | Учитывается как абстрактный | С какой версии |
|---|---|---|---|
| Абстрактный | Ровно один | Да | Java 1.0 |
default | Сколько угодно | Нет | Java 8 |
static | Сколько угодно | Нет | Java 8 |
private / private static | Сколько угодно | Нет | Java 9 |
Абстрактный, повторяющий public-метод Object | Сколько угодно | Нет | Java 8 |
4. Аннотация @FunctionalInterface
В Java 8 появилась аннотация @FunctionalInterface. Она ничего не меняет в поведении программы, но заставляет компилятор проверить, что у интерфейса действительно ровно один абстрактный метод. Если это не так — сборка падает.
// Не скомпилируется: два абстрактных метода
@FunctionalInterface
interface A {
void m(int i);
void m(long l);
} Аннотация необязательна: интерфейс с одним абстрактным методом является функциональным и без неё, лямбду ему присвоить можно. Но ставить её стоит — она защищает интерфейс от того, что кто-то добавит второй абстрактный метод и молча сломает весь код, где использовались лямбды.
Важно
Аннотацию @FunctionalInterface нельзя поставить на класс, перечисление или аннотацию — только на интерфейс. И она не наследуется: интерфейс-наследник придётся аннотировать заново.
5. Встроенные функциональные интерфейсы java.util.function
Писать свой интерфейс под каждую лямбду не нужно: в Java 8 добавлен пакет java.util.function с готовыми интерфейсами на все типовые сигнатуры.
| Интерфейс | Абстрактный метод | Что делает | Пример лямбды |
|---|---|---|---|
| Predicate<T> | boolean test(T t) | Проверяет условие | c -> c.getCostUSD() > 20000 |
| Consumer<T> | void accept(T t) | Принимает значение, ничего не возвращает | c -> System.out.println(c) |
| Function<T, R> | R apply(T t) | Преобразует одно значение в другое | c -> c.getModel() |
| Supplier<T> | T get() | Поставляет значение без аргументов | () -> new Car() |
| UnaryOperator<T> | T apply(T t) | Function, у которой аргумент и результат одного типа | s -> s.trim() |
| BinaryOperator<T> | T apply(T a, T b) | Сворачивает два значения одного типа в одно | (a, b) -> a + b |
| BiFunction<T, U, R> | R apply(T t, U u) | Функция от двух аргументов | (c, k) -> c.getCostUSD() * k |
| BiPredicate<T, U> | boolean test(T t, U u) | Условие от двух аргументов | (c, max) -> c.getCostUSD() < max |
| BiConsumer<T, U> | void accept(T t, U u) | Действие над парой значений | (k, v) -> map.put(k, v) |
Для примитивов существуют специализации без упаковки: IntPredicate, IntFunction<R>, ToIntFunction<T>, IntUnaryOperator, IntBinaryOperator, IntSupplier, IntConsumer и их аналоги для long и double. Они избавляют от автоупаковки в коде, который выполняется часто.
Функциональные интерфейсы есть и вне этого пакета: Runnable (void run()), Callable<V> (V call()), Comparator<T> (int compare(T, T)), Iterable<T>, AutoCloseable.
6. Что чаще всего понимают неправильно
- «Функциональный интерфейс — это интерфейс с одним методом». Не с одним методом, а с одним абстрактным методом.
default-,static- иprivate-методов может быть сколько угодно. - «Без @FunctionalInterface лямбду использовать нельзя». Можно: аннотация только включает проверку компилятора.
Runnableпрекрасно работал с лямбдами и до того, как получил эту аннотацию. - «Лямбда — это сокращённая запись анонимного класса». Нет: разное значение
this, разные правила видимости и принципиально разная компиляция (см. таблицу выше). - «Имя метода интерфейса должно совпадать с чем-то в лямбде». Компилятор сверяет только сигнатуру: параметры, возвращаемый тип и
throws. Имя абстрактного метода роли не играет.
Часто задаваемые вопросы
Обязательна ли аннотация @FunctionalInterface?
Нет. Любой интерфейс ровно с одним абстрактным методом является функциональным и может быть целевым типом лямбды даже без аннотации. Аннотация нужна как контракт: она заставляет компилятор выдать ошибку, если кто-то добавит в интерфейс второй абстрактный метод.
Можно ли реализовать лямбдой обобщённый (generic) метод?
Нет. Если абстрактный метод объявляет собственные параметры типа, например <T> T convert(T t), лямбда-выражение для него не компилируется: лямбда не может объявлять параметры типа. В таком случае подойдёт анонимный класс или ссылка на метод.
Останется ли интерфейс функциональным, если он наследует другой интерфейс?
Да, если суммарно у него остаётся ровно один абстрактный метод. Наследник, который не добавляет новых абстрактных методов, а только переопределяет существующий или добавляет default-методы, остаётся функциональным. Но аннотацию @FunctionalInterface придётся поставить заново: она не наследуется.
Почему лямбда не даёт изменить локальную переменную?
Лямбда захватывает не саму переменную, а копию её значения, поэтому переменная должна быть final или effectively final. Изменение внутри лямбды не было бы видно снаружи, и компилятор такой код запрещает. Обходные варианты для накопления результата — поле класса, массив из одного элемента или AtomicInteger.
Чем ссылка на метод отличается от лямбды?
Ссылка на метод, например Car::getModel, — это более короткая запись лямбды, которая не делает ничего, кроме вызова одного существующего метода. Обеим конструкциям нужен один и тот же целевой тип — функциональный интерфейс. При этом ссылка на метод, в отличие от лямбды, может реализовать и обобщённый метод.
Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии