W3docs

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

Чтобы быть функциональным, интерфейс должен объявлять ровно один метод, требующий реализации. Формулировка важна: не «ровно один метод вообще», а «ровно один абстрактный метод». Три категории методов не засчитываются в это количество:

  1. Методы default — у них уже есть тело, поэтому реализатору не нужно его предоставлять.
  2. Методы static — они принадлежат самому интерфейсу, а не его реализаторам.
  3. 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 — проверка на этапе компиляции по желанию

Аннотация является необязательной. Интерфейс считается функциональным по своей форме, а не из-за наличия аннотации. Но её использование даёт два преимущества:

  1. Ошибка компиляции, если интерфейс перестаёт быть функциональным. Случайно добавьте второй абстрактный метод — и компилятор остановит вас немедленно, прямо в интерфейсе, а не на каждом месте вызова, где он используется как целевой тип лямбды.
  2. Документация. Аннотация сигнализирует: «этот интерфейс предназначен для использования в качестве целевого типа лямбды», что ценно для любого неочевидного случая.
@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.

java— editable, runs on the server

Что вынести из этого примера:

  • notBlank1 (лямбда), notBlank2 (цепочка ссылок на методы) и notBlank3 (анонимный класс) — все реализуют один и тот же интерфейс Filter<String> и взаимозаменяемы. Лямбда самая короткая; анонимный класс оставлен для случаев, с которыми лямбды не справляются.
  • positive.and(even.negate()) скомпоновал три фильтра в один без каких-либо дополнительных объявлений методов. Методы default and и negate на интерфейсе — это алгебра композиции, именно поэтому JDK добавляет их к Predicate, Function и Comparator.
  • SafelyFunctional<T> объявляет и apply(T), и boolean equals(Object), и всё равно скомпилировался с @FunctionalInterface. Переопределение equals наследуется от Object, поэтому не засчитывается в правило единственного абстрактного метода.
  • Если убрать ключевое слово default в Filter (превратив один default-метод во второй абстрактный), аннотация @FunctionalInterface немедленно вызовет ошибку компиляции прямо в объявлении интерфейса — задолго до того, как любое место вызова лямбды увидит запутанные ошибки вывода типов.

Что дальше

Теперь вы умеете распознавать функциональный интерфейс, создавать его, когда JDK не предоставляет нужного, и позволять компилятору проверять его форму. Почти всегда, однако, правильный ответ — «использовать то, что уже есть». Следующая глава, Встроенные функциональные интерфейсы Java, знакомит с java.util.functionFunction, Predicate, Consumer, Supplier, их двухаргументными вариантами и примитивными специализациями, созданными во избежание упаковки.

Практика

Практика
Интерфейс объявляет три метода: один абстрактный метод, один метод `default` и `boolean equals(Object)`, переобъявленный из `Object`. Является ли он допустимым `@FunctionalInterface`?
Интерфейс объявляет три метода: один абстрактный метод, один метод `default` и `boolean equals(Object)`, переобъявленный из `Object`. Является ли он допустимым `@FunctionalInterface`?
Was this page helpful?