Перейти к основному содержимому

Руководство по интерфейсу Java BiFunction

· 7 мин. чтения

1. Введение

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

Вероятно, мы лучше всего знакомы с однопараметрическими функциональными интерфейсами Java 8, такими как Function , Predicate и Consumer .

В этом уроке мы рассмотрим функциональные интерфейсы, использующие два параметра . Такие функции называются бинарными функциями и представлены в Java функциональным интерфейсом BiFunction .

2. Однопараметрические функции

Давайте быстро вспомним, как мы используем однопараметрическую или унарную функцию, как мы это делаем в потоках :

List<String> mapped = Stream.of("hello", "world")
.map(word -> word + "!")
.collect(Collectors.toList());

assertThat(mapped).containsExactly("hello!", "world!");

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

3. Двухпараметрические операции

Библиотека Java Stream предоставляет нам функцию сокращения , которая позволяет нам комбинировать элементы потока . Нам нужно выразить, как значения, которые мы накопили до сих пор, преобразуются путем добавления следующего элемента.

Функция сокращения использует функциональный интерфейс BinaryOperator<T> , который принимает в качестве входных данных два объекта одного типа.

Давайте представим, что мы хотим объединить все элементы в нашем потоке, поместив новые на передний план с разделителем тире. Мы рассмотрим несколько способов реализации этого в следующих разделах.

3.1. Использование лямбды

Реализация лямбды для BiFunction начинается с двух параметров, заключенных в квадратные скобки:

String result = Stream.of("hello", "world")
.reduce("", (a, b) -> b + "-" + a);

assertThat(result).isEqualTo("world-hello-");

Как мы видим, два значения, a и b , являются строками . Мы написали лямбду, которая объединяет их для получения желаемого результата, со вторым первым и тире между ними.

Следует отметить, что reduce использует начальное значение — в данном случае пустую строку. Таким образом, мы получаем завершающий тире с приведенным выше кодом, так как первое значение из нашего потока соединяется с ним.

Кроме того, мы должны отметить, что вывод типов в Java позволяет нам в большинстве случаев опускать типы наших параметров. В ситуациях, когда тип лямбды не ясен из контекста, мы можем использовать типы для наших параметров:

String result = Stream.of("hello", "world")
.reduce("", (String a, String b) -> b + "-" + a);

3.2. Использование функции

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

private String combineWithoutTrailingDash(String a, String b) {
if (a.isEmpty()) {
return b;
}
return b + "-" + a;
}

А затем назовите это:

String result = Stream.of("hello", "world") 
.reduce("", (a, b) -> combineWithoutTrailingDash(a, b));

assertThat(result).isEqualTo("world-hello");

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

3.3. Использование ссылки на метод

Некоторые IDE автоматически предлагают нам преобразовать приведенную выше лямбду в ссылку на метод, так как ее часто легче читать.

Давайте перепишем наш код, чтобы использовать ссылку на метод:

String result = Stream.of("hello", "world")
.reduce("", this::combineWithoutTrailingDash);

assertThat(result).isEqualTo("world-hello");

Ссылки на методы часто делают функциональный код более понятным.

4. Использование BiFunction

До сих пор мы демонстрировали, как использовать функции, в которых оба параметра относятся к одному типу. Интерфейс BiFunction позволяет нам использовать параметры разных типов с возвращаемым значением третьего типа.

Давайте представим, что мы создаем алгоритм для объединения двух списков одинакового размера в третий список, выполняя операцию над каждой парой элементов:

List<String> list1 = Arrays.asList("a", "b", "c");
List<Integer> list2 = Arrays.asList(1, 2, 3);

List<String> result = new ArrayList<>();
for (int i=0; i < list1.size(); i++) {
result.add(list1.get(i) + list2.get(i));
}

assertThat(result).containsExactly("a1", "b2", "c3");

4.1. Обобщить функцию

Мы можем обобщить эту специализированную функцию, используя BiFunction в качестве объединителя:

private static <T, U, R> List<R> listCombiner(
List<T> list1, List<U> list2, BiFunction<T, U, R> combiner) {
List<R> result = new ArrayList<>();
for (int i = 0; i < list1.size(); i++) {
result.add(combiner.apply(list1.get(i), list2.get(i)));
}
return result;
}

Давайте посмотрим, что здесь происходит. Существует три типа параметров: T для типа элемента в первом списке, U для типа во втором списке, а затем R для любого типа, возвращаемого комбинационной функцией.

Мы используем BiFunction , предоставленную этой функции, вызывая ее метод apply , чтобы получить результат.

4.2. Вызов обобщенной функции

Наш объединитель — это BiFunction , который позволяет нам внедрить алгоритм независимо от типов ввода и вывода. Давайте попробуем:

List<String> list1 = Arrays.asList("a", "b", "c");
List<Integer> list2 = Arrays.asList(1, 2, 3);

List<String> result = listCombiner(list1, list2, (a, b) -> a + b);

assertThat(result).containsExactly("a1", "b2", "c3");

И мы можем использовать это для совершенно разных типов входов и выходов.

Давайте внедрим алгоритм, чтобы определить, больше ли значение в первом списке, чем значение во втором, и получить логический результат:

List<Double> list1 = Arrays.asList(1.0d, 2.1d, 3.3d);
List<Float> list2 = Arrays.asList(0.1f, 0.2f, 4f);

List<Boolean> result = listCombiner(list1, list2, (a, b) -> a > b);

assertThat(result).containsExactly(true, true, false);

4.3. Справочник по методу BiFunction

Давайте перепишем приведенный выше код с извлеченным методом и ссылкой на метод:

List<Double> list1 = Arrays.asList(1.0d, 2.1d, 3.3d);
List<Float> list2 = Arrays.asList(0.1f, 0.2f, 4f);

List<Boolean> result = listCombiner(list1, list2, this::firstIsGreaterThanSecond);

assertThat(result).containsExactly(true, true, false);

private boolean firstIsGreaterThanSecond(Double a, Float b) {
return a > b;
}

Следует отметить, что это делает код немного легче для чтения, так как метод firstIsGreaterThanSecond описывает алгоритм, введенный как ссылка на метод.

4.4. Ссылки на методы BiFunction с использованием этого

Давайте представим, что мы хотим использовать описанный выше алгоритм на основе BiFunction , чтобы определить, равны ли два списка:

List<Float> list1 = Arrays.asList(0.1f, 0.2f, 4f);
List<Float> list2 = Arrays.asList(0.1f, 0.2f, 4f);

List<Boolean> result = listCombiner(list1, list2, (a, b) -> a.equals(b));

assertThat(result).containsExactly(true, true, true);

На самом деле мы можем упростить решение:

List<Boolean> result = listCombiner(list1, list2, Float::equals);

Это связано с тем, что функция equals в Float имеет ту же сигнатуру, что и BiFunction . Он принимает неявный первый параметр this, объект типа Float . Второй параметр, other типа Object , представляет собой значение для сравнения.

5. Составление бифункций

Что, если бы мы могли использовать ссылки на методы, чтобы делать то же самое, что и в нашем примере сравнения числовых списков?

List<Double> list1 = Arrays.asList(1.0d, 2.1d, 3.3d);
List<Double> list2 = Arrays.asList(0.1d, 0.2d, 4d);

List<Integer> result = listCombiner(list1, list2, Double::compareTo);

assertThat(result).containsExactly(1, 1, -1);

Это близко к нашему примеру, но возвращает целое число , а не исходное логическое значение . Это связано с тем, что метод compareTo в Double возвращает Integer .

Мы можем добавить дополнительное поведение, необходимое для достижения нашего оригинала, используя andThen для создания функции . Это создает BiFunction , которая сначала делает одно действие с двумя входами, а затем выполняет другую операцию.

Далее, давайте создадим функцию для преобразования нашей ссылки на метод Double::compareTo в BiFunction :

private static <T, U, R> BiFunction<T, U, R> asBiFunction(BiFunction<T, U, R> function) {
return function;
}

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

Теперь мы можем использовать andThen , чтобы добавить поведение поверх первой функции:

List<Double> list1 = Arrays.asList(1.0d, 2.1d, 3.3d);
List<Double> list2 = Arrays.asList(0.1d, 0.2d, 4d);

List<Boolean> result = listCombiner(list1, list2,
asBiFunction(Double::compareTo).andThen(i -> i > 0));

assertThat(result).containsExactly(true, true, false);

6. Заключение

В этом руководстве мы рассмотрели BiFunction и BinaryOperator с точки зрения предоставленной библиотеки Java Streams и наших собственных пользовательских функций. Мы рассмотрели, как передавать BiFunctions с помощью лямбда-выражений и ссылок на методы, и увидели, как составлять функции.

Библиотеки Java предоставляют только функциональные интерфейсы с одним и двумя параметрами. Дополнительные идеи для ситуаций, требующих дополнительных параметров, см. в нашей статье о каррировании .

Как всегда, полные примеры кода доступны на GitHub .