«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\u003cT\u003e, который принимает на вход два объекта одного типа.

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

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 Использование этого

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);

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

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

Мы можем упростить решение:

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

5. Составление BiFunctions

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, которая сначала делает одно действие с двумя входами, а затем выполняет другую операцию.

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

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

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

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);

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

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

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

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