Перейти к содержимому
Теория · Лямбды и Stream API · урок 3 из 12

Function, Predicate и ссылки на методы

В прошлом уроке для карточки с инструкцией мы завели свой интерфейс Rule. Заводить интерфейс под каждую карточку не придётся.

На фабрике давно напечатаны стандартные бланки карточек:

  • «возьми коробку и отдай вместо неё другую»;
  • «возьми коробку и ответь: да или нет»;
  • «возьми коробку и сделай с ней что-нибудь»;
  • «выдай новую коробку».

Мастеру остаётся вписать в бланк своё правило. В Java такие бланки лежат в пакете java.util.function:

JAVA
import java.util.function.Function;
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Function<String, Integer> len = s -> s.length();
        Predicate<String> isBlank = s -> s.isBlank();
        System.out.println(len.apply("lambda")); // 6
        System.out.println(isBlank.test(""));    // true
    }
}

Давайте разберём программу по строкам:

  1. Function<String, Integer> len — бланк «возьми String, верни Integer». В угловых скобках сначала тип того, что лямбда принимает, потом тип результата.
  2. Predicate<String> isBlank — бланк «возьми String, ответь true или false».
  3. len.apply("lambda") выполняет лямбду и печатает 6: у Function единственный метод называется apply.
  4. isBlank.test("") печатает true: у Predicate метод называется test.

В угловых скобках пишут только ссылочные типы, поэтому вместо int стоит Integer, как в List<Integer>.

Четыре готовых интерфейса

Все четыре функциональные: у каждого один абстрактный метод, и под него пишут лямбду. Их часто перечисляют на собеседовании, поэтому запомните у каждого имя метода. Буквы T и R ниже — места для типов: вместо них пишут String, Integer и любой другой ссылочный тип.

Function

Function<T, R> принимает значение типа T и возвращает значение типа R. Метод — apply. Этот бланк берут, когда одно значение нужно превратить в другое: строку в её длину, цену в цену со скидкой.

Predicate

Predicate<T> принимает значение и возвращает boolean. Метод — test. Это проверка «подходит или нет», и именно её ждёт removeIf из прошлого урока.

Consumer

Consumer<T> принимает значение и ничего не возвращает. Метод — accept. Это действие без результата: напечатать, записать. Его ждёт forEach.

Supplier

Supplier<T> ничего не принимает и возвращает значение. Метод — get. Лямбду для него пишут с пустыми скобками:

JAVA
Consumer<String> shout = s -> System.out.println(s + "!");
Supplier<String> greeting = () -> "hello";
shout.accept(greeting.get()); // hello!

Здесь greeting.get() выдаёт строку "hello", а shout.accept(...) печатает её с восклицательным знаком. Consumer и Supplier импортируют из того же пакета java.util.function, что и Function.

Ссылки на методы

Часто лямбда только вызывает один готовый метод: s -> s.length(). Для такой лямбды есть запись короче — ссылка на метод: String::length. Слева от :: класс или объект, справа имя метода, без скобок и аргументов.

Две формы встречаются чаще всего:

  • String::length вместо s -> s.length() — метод вызывается у самого параметра;
  • System.out::println вместо x -> System.out.println(x) — параметр передаётся в метод объекта System.out.

Поэтому печать всего списка часто записывают как words.forEach(System.out::println). Ссылка на метод — та же карточка, только вместо текста инструкции на ней стоит название готового метода.

Цепочка andThen

У Function, кроме apply, есть default-метод andThen — метод интерфейса с телом, как в теме про интерфейсы. Вызов f.andThen(g) возвращает новую функцию: сначала работает f, а её результат получает g.

JAVA
Function<String, String> strip = String::strip;
Function<String, Integer> size = String::length;
Function<String, Integer> strippedSize = strip.andThen(size);
System.out.println(strippedSize.apply("  hi  ")); // 2

Разберём по строкам:

  1. strip убирает пробелы по краям строки, а size считает её длину. Обе функции записаны ссылками на методы.
  2. strip.andThen(size) строит новую функцию: сначала strip, потом size от результата strip.
  3. apply(" hi ") превращает " hi " в "hi", а "hi" — в 2.

Порядок читается слева направо, как станции на ленте: первой работает функция, у которой вызвали andThen, второй — та, что в скобках. Функциональным Function при этом остаётся: у default-метода уже есть тело, и абстрактный метод в интерфейсе по-прежнему один.

Здесь картинка с бланками неточна. Бумажный бланк можно заполнить как угодно, а лямбду проверяет компилятор. Если лямбда для Predicate вернёт не boolean, программа не скомпилируется.

Частая ошибка

Лямбду из переменной вызвали как обычный метод:

JAVA
Function<String, Integer> len = s -> s.length();
System.out.println(len("lambda")); // ✗
TEXT
error: cannot find symbol

Программа не компилируется: javac ищет метод len в классе Main и не находит. len — переменная, в ней лежит объект, а лямбду вызывают через метод его интерфейса: len.apply("lambda").

Главное

  • Function превращает значение в другое (apply), а Predicate проверяет его (test).
  • Consumer что-то делает со значением и ничего не возвращает (accept), а Supplier выдаёт значение, ничего не принимая (get).
  • Ссылка на метод String::length — короткая запись лямбды, которая только вызывает этот метод.
  • f.andThen(g) выполняет сначала f, потом g с результатом f.