Lambda ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-09-06

Функциональный интерфейс в 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;

Формально требования к функциональному интерфейсу такие:

  1. Ровно один абстрактный метод.
  2. Любое количество default-, static-, private- и private static-методов (последние два — начиная с Java 9).
  3. Абстрактные методы, повторяющие 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. Что чаще всего понимают неправильно

  1. «Функциональный интерфейс — это интерфейс с одним методом». Не с одним методом, а с одним абстрактным методом. default-, static- и private-методов может быть сколько угодно.
  2. «Без @FunctionalInterface лямбду использовать нельзя». Можно: аннотация только включает проверку компилятора. Runnable прекрасно работал с лямбдами и до того, как получил эту аннотацию.
  3. «Лямбда — это сокращённая запись анонимного класса». Нет: разное значение this, разные правила видимости и принципиально разная компиляция (см. таблицу выше).
  4. «Имя метода интерфейса должно совпадать с чем-то в лямбде». Компилятор сверяет только сигнатуру: параметры, возвращаемый тип и throws. Имя абстрактного метода роли не играет.

Часто задаваемые вопросы

Обязательна ли аннотация @FunctionalInterface?

Нет. Любой интерфейс ровно с одним абстрактным методом является функциональным и может быть целевым типом лямбды даже без аннотации. Аннотация нужна как контракт: она заставляет компилятор выдать ошибку, если кто-то добавит в интерфейс второй абстрактный метод.

Можно ли реализовать лямбдой обобщённый (generic) метод?

Нет. Если абстрактный метод объявляет собственные параметры типа, например <T> T convert(T t), лямбда-выражение для него не компилируется: лямбда не может объявлять параметры типа. В таком случае подойдёт анонимный класс или ссылка на метод.

Останется ли интерфейс функциональным, если он наследует другой интерфейс?

Да, если суммарно у него остаётся ровно один абстрактный метод. Наследник, который не добавляет новых абстрактных методов, а только переопределяет существующий или добавляет default-методы, остаётся функциональным. Но аннотацию @FunctionalInterface придётся поставить заново: она не наследуется.

Почему лямбда не даёт изменить локальную переменную?

Лямбда захватывает не саму переменную, а копию её значения, поэтому переменная должна быть final или effectively final. Изменение внутри лямбды не было бы видно снаружи, и компилятор такой код запрещает. Обходные варианты для накопления результата — поле класса, массив из одного элемента или AtomicInteger.

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

Ссылка на метод, например Car::getModel, — это более короткая запись лямбды, которая не делает ничего, кроме вызова одного существующего метода. Обеим конструкциям нужен один и тот же целевой тип — функциональный интерфейс. При этом ссылка на метод, в отличие от лямбды, может реализовать и обобщённый метод.

Видео объяснение

Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.

Комментарии

midavko Oct 21, 2021
"Разрешается доступ к переменным класса и статическим переменным класса." Если лямда-выражение преобразуется в статический метод класса, тогда он может работаться только со статическими переменными/методами класса. Компилятор выдает ошибку, если попытаться обратиться в лямда-выражении к обычным полям класса. Или я что-то путаю?
Oct 22, 2021
Ошибки компиляции не будет, лямбда выражение действительно может обращаться к нестатическим переменным класса. Почитайте раздел "Lambdas and invokedynamic" на сайте https://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood/. Там более подробно описывается как лямбда выражение преобразуется под капотом.

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.