Код
Java
#База знаний

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

Что это такое, зачем нужно и как работает.

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

Передача простых параметров в виде примитивов или объектов труда обычно не составляет. Но порой в метод требуется передавать не просто переменную, а исполняемый код.

Например, нам нужен метод, который работает с элементами массива, причём только с теми, что соответствуют некоторому условию. А само условие во время написания метода мы можем не знать (или оно будет меняться).

Как поступить? Передавать реализующий условие код с помощью параметра метода! Да, в Java начиная с восьмой версии можно подобное делать. И сейчас вы узнаете как.


Мария помазкина

Хлебом не корми — дай кому-нибудь про Java рассказать.


Пример

Напишем методы, возвращающие сумму и произведение двух чисел:

private int sum(int a, int b) {
    return a+b;
}

private int mult(int a, int b) {
    return a*b;
}

А теперь объединим их в один — processTwoNumbers. Он будет принимать два параметра-числа и код, который их обрабатывает.

private int processTwoNumbers (int a, int b, [сюда передаётся код])

Для выполнения метода sum третий параметр примет в качестве аргумента действие a+b, а для выполнения метода mult — a*b.

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

Значит, надо как-то сообщить об этом компилятору — запретить будущим разработчикам передавать неподходящий код (вроде a+b+c).

Поможет в этом сигнатура метода. Она станет третьим параметром в нашем методе processTwoNumbers:

private int processTwoNumbers (int a, int b, [сигнатура метода])

Но как записать третий параметр, чтобы сигнатура самого метода processTwoNumbers не разрослась до нечитабельности? Этот вопрос разработчики Java решили изящно. Они придумали функциональные интерфейсы.

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

Функциональный интерфейс — это интерфейс, который содержит ровно один абстрактный метод, то есть описание метода без тела. Статические методы и методы по умолчанию при этом не в счёт, их в функциональном интерфейсе может быть сколько угодно.

Когда параметром метода является функциональный интерфейс, при вызове этого метода одним из аргументов должен быть блок кода.

Передаваемый блок кода должен удовлетворять следующему условию: его сигнатура должна совпадать с сигнатурой единственного абстрактного метода функционального интерфейса.

Звучит непросто, поясним на примере:

@FunctionalInterface
public interface ToIntBiFunction<T, U> {

   /**
    * Applies this function to the given arguments.
    *
    * @param t the first function argument
    * @param u the second function argument
    * @return the function result
    */
   int applyAsInt(T t, U u);
}

Важно. В Java есть несколько готовых функциональных интерфейсов с разным числом и типами входных-выходных параметров. (Как раз из таких ToIntBiFunction выше.) А если мы создаём новый функциональный интерфейс, то важно не забыть аннотацию @FunctionalInterface. Увидев её, компилятор проверит, что интерфейс и правда является функциональным.

Функциональный интерфейс ToIntBiFunction<T, U> подходит к тому примеру, с которого мы начинали. Это значит, что мы можем передать в него аргументом код, который:

  1. принимает на вход два параметра (T t, U u). T и U указывают на то, что аргументы могут быть разных типов. Например, Long и String. Для нас это даже избыточно, у нас они одного типа — int;
  2. возвращает значение типа int.

Вот что получится:

public void processTwoNumbers(int a, int b, ToIntBiFunction<Integer, Integer> function){
//code
}

Кусочек ToIntBiFunction<Integer, Integer> говорит: передавай сюда метод с такой же сигнатурой, как у метода внутри меня.

Чтобы внутри метода processTwoNumbers выполнить переданный код, нужно вызвать метод из функционального интерфейса:

public void processTwoNumbers(int a, int b, ToIntBiFunction<Integer, Integer> function){

function.applyAsInt(a, b);
}

Вот мы и добрались до лямбда-выражений.

Что такое лямбда-выражения

Это компактный синтаксис, заимствованный из λ-исчисления, для передачи кода в качестве параметра в другой код.

По сути, это анонимный (без имени) класс или метод. Так как всё в Java (за исключением примитивных типов) — это объекты, лямбды тоже должны быть связаны с конкретным объектным типом. Как вы догадались, он называется функциональным интерфейсом.

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

Не будь лямбд, вызывать метод processTwoNumbers каждый раз приходилось бы так:

ToIntBiFunction<Integer, Integer> biFunction = new ToIntBiFunction<>() {
    @Override
    public int applyAsInt(Integer a, Integer b) {
        return a + b;
    }
};

processTwoNumbers(1, 2, biFunction);

Примечание. biFunction в примере создана с использованием анонимных классов. Без этого нам пришлось бы создавать класс, реализующий интерфейс ToIntBiFunction, и объявлять в этом классе метод applyAsInt. А с анонимным классом мы всё это сделали на лету.

В примере выше всё, кроме одной строчки, избыточно. За содержательную часть (логику работы) отвечает только одно выражение return a + b, а всё остальное — техническая шелуха. И её пришлось написать многовато, даже чтобы просто передать методу код сложения двух чисел.

Здесь и вступают в игру лямбды. С ними можно сократить создание biFunction всего до десяти символов!

Лямбда строится так: (параметры) -> (код метода)

А наша лямбда будет такой:

ToIntBiFunction<Integer, Integer> biFunction = (a, b) -> a + b;

И всё! Этот блок из 10 символов можно передавать как аргумент методу, ожидающему функциональный интерфейс в качестве параметра. Причём чаще всего обходятся без промежуточной переменной — передают напрямую лямбду:

processTwoNumbers(1, 2, (a, b) -> a+b);

Компилятор проверит, что лямбда подходит функциональному интерфейсу — принимает нужное число параметров нужного типа. Напомню, в нашем примере задействован функциональный интерфейс ToIntBiFunction. Сигнатура его единственного абстрактного метода содержит два параметра (Integer a, Integer b).

Например, такой вот вызов метода не скомпилируется, потому что передан всего один параметр:

processTwoNumbers(5, 6, (a) -> 5 * a);

Лямбды записывают по-разному. Мы рассмотрели только один вариант.

Где применяют лямбды?

Много где. Довольно частый случай — обход элементов в цикле:

List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
integers.forEach(item -> System.out.println(item));

Ещё лямбды работают в компараторах при сортировке. Допустим, нужно отсортировать коллекцию по последней букве каждого слова:

List<String> colors = Arrays.asList("Black", "White", "Red");
Collections.sort(colors, (o1, o2) -> {
   String o1LastLetter = o1.substring(o1.length() - 1);
   String o2LastLetter = o2.substring(o2.length() - 1);
   return o1LastLetter.compareTo(o2LastLetter);
});

Редко обходятся без лямбд при работе с коллекциями вместе со Stream API. В следующем примере фильтруем стрим по значению (filter), меняем каждый элемент (map) и собираем в список (collect):

final double currencyUsdRub = 80;
List<Double> pricesRub = Arrays.asList(25d, 50d , 60d, 12d, 45d, 89d);

List<Double> pricesUsdGreater50Rub = pricesRub.stream()
   .filter(d -> d > 50) // используем функциональный интерфейс Predicate<T>
   .map(d -> d / currencyUsdRub) // используем функциональный интерфейс Function<T, R>
   .collect(Collectors.toList()); 

Подытожим

Функциональные интерфейсы в Java 8 избавили разработчиков от чудовищно громоздкого синтаксиса с анонимными классами (когда требовалось передавать некую функциональность в метод) и позволили использовать компактные лямбда-выражения и ссылки на методы.

До восьмой версии Java разработчики обходились без лямбда-выражений. Лямбды стали для них очередным синтаксическим сахаром.

Сперва синтаксическим сахаром были функциональные интерфейсы, они позволили оперировать блоком кода, который выполняется когда нужно, но реализации были слишком громоздкими. А с лямбдами функциональные интерфейсы стали записываться короче. Так что лямбды — не просто синтаксический сахар, а синтаксический сахар синтаксического сахара.

Теперь на Java можно писать программы в стиле функциональных языков программирования (это когда программа записывается как последовательное применение функций к некоторым значениям и другим функциям, а не как сложная структура из циклов, условных операторов и перекладывания значений туда-сюда). Удивительно, как легко превратить массивные структуры кода в изящные цепочки вызовов, и всё это благодаря лямбдам и функциональным интерфейсам.

Официальная документация по лямбдам здесь.


Курс

Профессия Java-разработчик PRO

Вы с нуля освоите востребованный язык программирования, научитесь создавать качественные приложения под разные платформы и станете ценным Java-специалистом уровня middle.

Узнать про курс
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована