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 .