Автор оригинала: Ashley Frieze.
1. введение
Java 8 представила программирование в функциональном стиле , позволяющее нам параметризовать методы общего назначения путем передачи функций.
Мы, вероятно, наиболее знакомы с однопараметрическими функциональными интерфейсами Java 8, такими как Функция , Предикат, и Потребитель .
В этом уроке мы рассмотрим функциональные интерфейсы, которые используют два параметра . Такие функции называются двоичными функциями и представлены в Java с помощью функционального интерфейса Bi Function .
2. Однопараметрические Функции
Давайте быстро вспомним, как мы используем однопараметрическую или унарную функцию, как мы это делаем в потоках:
Listmapped = 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 позволяет нам использовать параметры разных типов с возвращаемым значением третьего типа.
Давайте представим, что мы создаем алгоритм для объединения двух списков одинакового размера в третий список, выполнив операцию над каждой парой элементов:
Listlist1 = 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 staticList 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. Вызов обобщенной функции
Наш объединитель-это бифункция , которая позволяет нам вводить алгоритм, независимо от типов ввода и вывода. Давайте попробуем:
Listlist1 = 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");
И мы также можем использовать это для совершенно разных типов входов и выходов.
Давайте введем алгоритм, чтобы определить, больше ли значение в первом списке, чем значение во втором, и получим логический результат:
Listlist1 = 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. Ссылка на метод бифункции
Давайте перепишем приведенный выше код с извлеченным методом и ссылкой на метод:
Listlist1 = 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 на основе функции , чтобы определить, равны ли два списка:
Listlist1 = 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);
Мы действительно можем упростить решение:
Listresult = listCombiner(list1, list2, Float::equals);
Это происходит потому, что функция равна в Float имеет ту же сигнатуру, что и бифункция . Он принимает неявный первый параметр this, объект типа Поплавок . Второй параметр, other , типа Object , является значением для сравнения.
5. Составление бифункций
Что, если бы мы могли использовать ссылки на методы, чтобы сделать то же самое, что и в нашем примере сравнения числовых списков?
Listlist1 = 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 staticBiFunction asBiFunction(BiFunction function) { return function; }
Ссылка на лямбду или метод становится бифункцией только после того, как она была преобразована вызовом метода. Мы можем использовать эту вспомогательную функцию для явного преобразования нашей лямбды в объект BiFunction .
Теперь мы можем использовать , а затем , чтобы добавить поведение поверх первой функции:
Listlist1 = 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 .