Function, Predicate и ссылки на методы
В прошлом уроке для карточки с инструкцией мы завели свой интерфейс Rule. Заводить интерфейс под каждую карточку не придётся.
На фабрике давно напечатаны стандартные бланки карточек:
- «возьми коробку и отдай вместо неё другую»;
- «возьми коробку и ответь: да или нет»;
- «возьми коробку и сделай с ней что-нибудь»;
- «выдай новую коробку».
Мастеру остаётся вписать в бланк своё правило. В Java такие бланки лежат в пакете java.util.function:
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
}
}Давайте разберём программу по строкам:
Function<String, Integer> len— бланк «возьмиString, верниInteger». В угловых скобках сначала тип того, что лямбда принимает, потом тип результата.Predicate<String> isBlank— бланк «возьмиString, ответьtrueилиfalse».len.apply("lambda")выполняет лямбду и печатает6: уFunctionединственный метод называетсяapply.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. Лямбду для него пишут с пустыми скобками:
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.
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Разберём по строкам:
stripубирает пробелы по краям строки, аsizeсчитает её длину. Обе функции записаны ссылками на методы.strip.andThen(size)строит новую функцию: сначалаstrip, потомsizeот результатаstrip.apply(" hi ")превращает" hi "в"hi", а"hi"— в2.
Порядок читается слева направо, как станции на ленте: первой работает функция, у которой вызвали andThen, второй — та, что в скобках. Функциональным Function при этом остаётся: у default-метода уже есть тело, и абстрактный метод в интерфейсе по-прежнему один.
Здесь картинка с бланками неточна. Бумажный бланк можно заполнить как угодно, а лямбду проверяет компилятор. Если лямбда для Predicate вернёт не boolean, программа не скомпилируется.
Частая ошибка
Лямбду из переменной вызвали как обычный метод:
Function<String, Integer> len = s -> s.length();
System.out.println(len("lambda")); // ✗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.