Рубрики
Без рубрики

Руководство по интерфейсу бифункциональности Java

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

Автор оригинала: Ashley Frieze.

1. введение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 позволяет нам использовать параметры разных типов с возвращаемым значением третьего типа.

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

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

List 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. Обобщите функцию

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

private static  List listCombiner(
  List list1, List list2, BiFunction combiner) {
    List 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 для любого типа, возвращаемого функцией комбинации.

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

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

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

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

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

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

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

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

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

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

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

4.3. Ссылка на метод бифункции

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

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

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

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

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

Следует отметить, что это немного облегчает чтение кода, так как метод first Больше, чем Второй описывает алгоритм, введенный в качестве ссылки на метод.

4.4. Ссылки на Метод функции Bi С использованием этого

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

private static  BiFunction asBiFunction(BiFunction function) {
    return function;
}

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

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

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

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

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

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

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

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

Как всегда, полные образцы кода доступны на GitHub .