Статьи Утилиты Telegram YouTube RuTube Отзывы

Функциональные интерфейсы в Java

3 марта 2025

Тэги: Java, Stream API, ООП.

Содержание

  1. Функциональные интерфейсы и лямбда-выражения
  2. Consumer
  3. Supplier
  4. Predicate
  5. Function
  6. UnaryOperator
  7. BiFunction
  8. BinaryOperator
  9. Выводы

В основе Stream API, которое значительно упрощает работу с коллекциями в Java, лежит понятие функциональных интерфейсов.

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

Любой интерфейс можно назвать функциональным, если он содержит один-единственный метод. Такой интерфейс снабжается аннотацией @FunctionalInterface.

Например, мы можем создать обобщённый функциональный интерфейс Convertable, который содержит метод convert(). В качестве параметра метод принимает на вход объект типа A и возвращает объект типа B.

@FunctionalInterface
public interface Convertable<B, A> {
    B convert(A a);
}

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

public void run() {
    doConvert(x -> String.valueOf(x * x));
}

public void doConvert(Convertable<String, Integer> converter) {
    System.out.println(converter.convert(2));
}

Поскольку параметр converter метода doConvert() типизирован типами String и Integer, мы в методе run() можем указать любую лямбду, которая принимает целое число и возвращает строку. В данном случае мы увидим в консоли квадрат числа 2.

Лямбду можно представлять как более краткую запись анонимного класса (хотя с точки зрения компилятора это не совсем так).

public void run() {
    doConvert(new Convertable<String, Integer>() {
        @Override
        public String convert(Integer x) {
            return String.valueOf(x * x);
        }
    });
}

Лямбда может содержать и более одного параметра, но если входной параметр только один и есть метод, удовлетворяющий такой сигнатуре, то можно использовать ещё более краткую запись:

public void run() {
    doConvert(this::quad); // method reference
}

public String quad(int x) {
    return String.valueOf(x * x);
}

Такая форма записи через двойное двоеточие называется «method reference».

Как видите, лямбда позволяет компактно записать реализацию интерфейса. Особенно если эта реализация представляет собой краткую формулу без дополнительных условий. И это очень мощная концепция, которая активно используется в Stream API.

Рассмотрим наиболее употребимые функциональные интерфейсы стандартной библиотеки. Все эти интерфейсы находятся в пакете java.util.function.

Consumer

Функциональный интерфейс Consumer содержит метод accept(). Этот метод принимает объект типа T и ничего не возвращает:

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

Именно такой интерфейс принимает метод Stream.forEach(). Внутри него метод accept() последовательно применяется к каждому элементу стрима.

public void run() {
    Stream.of(1, 2, 3)
            .forEach(System.out::println);
    // или .forEach(e -> System.out.println(e));
}

В данном случае мы передаём метод println() через method reference и выводим все элементы стрима.

Supplier

Обратным к интерфейсу Consumer можно назвать Supplier. Он содержит метод get(), который ничего не принимает, но возвращает результат.

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

В следующем примере через лямбду мы создаём Supplier, который генерирует случайные числа в диапазоне от 0 до 100 (не включая верхнюю границу диапазона). Его передаём в метод Stream.generate() и далее с помощью limit() ограничиваем стрим в 10 элементов, а затем выводим все числа на экран:

public void run() {
    var random = new Random();
    Stream.generate(() -> random.nextInt(100))
            .limit(10)
            .forEach(System.out::println);
}

В консоли мы увидим 10 случайных чисел.

Predicate

Функциональный интерфейс Predicate содержит метод test(). Он принимает на вход объект типа T, проверяет его по некоторому условию и возвращает результат проверки в виде boolean:

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

Этот интерфейс в виде лямбды можно передавать в метод Stream.filter(). Например, вот так можно вывести все чётные числа стрима:

public void run() {
    Predicate<Integer> onlyEven = (i -> i % 2 == 0);
    Stream.of(1, 2, 3, 4, 5, 6)
            .filter(onlyEven)
            .forEach(System.out::println); // 2, 4, 6
}

Помимо filter(), этот же функциональный интерфейс принимают методы allMatch(), anyMatch() и noneMatch().

public void run() {
    Predicate<Integer> onlyPositive = (i -> i > 0);
    System.out.println(
            Stream.of(2, 4, 6, 8)
                    .allMatch(onlyPositive) // true
    );
}

Здесь мы проверяем, являются ли все числа стрима положительными.

Function

Функциональный интерфейс Function является базовым для целого семейства функциональных интерфейсов. Он содержит метод apply(), который принимает на вход объект типа T и преобразует его в объект типа R.

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

По сути делает ровно то же, что и наш интерфейс Convertable, который мы рассмотрели в начале статьи.

Именно Function принимает на вход метод Stream.map():

public void run() {
    Stream.of(1, 2, 3, 4, 5)
            .map(i -> String.valueOf(i * i))
            .forEach(System.out::println);
}

В этом примере мы выводим квадраты исходных чисел в виде строк.

UnaryOperator

Частным случаем интерфейса Function является UnaryOperator. У него совпадают типы входного параметра и результата.

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
}

Модуль числа или квадратный корень можно представить как унарные операторы:

UnaryOperator<Integer> abs = Math::abs; // x -> Math.abs(x)
UnaryOperator<Double> sqrt = Math::sqrt; // x -> Math.sqrt(x)

BiFunction

Функциональный интерфейс, принимающий на вход два параметра разных типов и возвращающий результат, называется BiFunction.

@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
}

BinaryOperator

По аналогии с UnaryOperator, частный случай BiFunction, у которого совпадают типы обоих параметров и выходного значения, называется BinaryOperator:

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
}

Стандартные методы вроде возведения в степень или конкатенация строк могут быть представлены как бинарные операторы:

BinaryOperator<Double> pow = Math::pow; // (a, b) -> Math.pow(a, b);
BinaryOperator<String> concat = String::concat; // (a, b) -> a.concat(b);

Выводы

В этой статье мы познакомились с функциональными интерфейсами в Java и научились записывать их в виде лямбда-выражений. Также мы рассмотрели основные функциональные интерфейсы из стандартной библиотеки, которые активно используются в Stream API. Другие интерфейсы (BooleanSupplier, DoublePredicate, IntFunction, LongConsumer и т.п.) являются частными случаями рассмотренных выше.


См. также


Комментарии

Добавить комментарий

×

devmark.ru