1. Въведение
Java 8 въведе програмиране на функционален стил, което ни позволява да параметризираме методите с общо предназначение чрез предаване на функции.
Вероятно сме най-добре запознати с еднопараметричните функционални интерфейси Java 8 като Function , Predicate и Consumer .
В този урок ще разгледаме функционалните интерфейси, които използват два параметъра . Такива функции се наричат двоични функции и са представени в Java с функционалния интерфейс BiFunction .
2. Функции с един параметър
Нека да обобщим бързо как използваме единичен параметър или унарна функция, както правим в потоци:
List mapped = Stream.of("hello", "world") .map(word -> word + "!") .collect(Collectors.toList()); assertThat(mapped).containsExactly("hello!", "world!");
Както виждаме, картата използва Function , която приема един параметър и ни позволява да извършим операция върху тази стойност, връщайки нова стойност.
3. Двупараметрични операции
Библиотеката Java Stream ни предоставя функция за намаляване , която ни позволява да комбинираме елементите на поток . Трябва да изразим как стойностите, които сме натрупали досега, се трансформират чрез добавяне на следващия елемент.
Функцията за намаляване използва функционалния интерфейс BinaryOperator , който приема два обекта от същия тип като своите входове.
Нека си представим, че искаме да се присъединим към всички елементи в нашия поток, като поставим новите отпред с разделител на тирета. Ще разгледаме няколко начина за прилагане на това в следващите раздели.
3.1. Използване на ламбда
Изпълнението на ламбда за BiFunction е с префикс от два параметъра, заобиколени от скоби:
String result = Stream.of("hello", "world") .reduce("", (a, b) -> b + "-" + a); assertThat(result).isEqualTo("world-hello-");
Както виждаме, двете стойности, a и b, са Strings . Написахме ламбда, която ги комбинира, за да направи желания изход, като първият е вторият и тирето между тях.
Трябва да отбележим, че намаляване използва начална стойност - в този случай празния низ. По този начин завършваме с последващо тире с горния код, тъй като първата стойност от нашия поток е свързана с него.
Също така трябва да отбележим, че изводът за типа на 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 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. Генерализирайте функцията
Можем да обобщим тази специализирана функция, използвайки BiFunction като комбинатор:
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 за какъвто и да е тип, който функцията за комбиниране връща.
Използваме BiFunction, предоставен на тази функция, като извикваме метода й apply , за да получим резултата.
4.2. Извикване на генерализирана функция
Нашият комбинатор е BiFunction , който ни позволява да инжектираме алгоритъм, независимо от типовете вход и изход. Нека го изпробваме:
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. А BiFunction Метод Референтен
Нека пренапишем горния код с извлечен метод и справка за метод:
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; }
We should note that this makes the code a little easier to read, as the method firstIsGreaterThanSecond describes the algorithm injected as a method reference.
4.4. BiFunction Method References Using this
Let's imagine we want to use the above BiFunction-based algorithm to determine if two lists are equal:
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);
We can actually simplify the solution:
List result = listCombiner(list1, list2, Float::equals);
This is because the equals function in Float has the same signature as a BiFunction. It takes an implicit first parameter of this, an object of type Float. The second parameter, other, of type Object, is the value to compare.
5. Composing BiFunctions
What if we could use method references to do the same thing as our numeric list comparison example?
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);
This is close to our example but returns an Integer, rather than the original Boolean. This is because the compareTo method in Double returns Integer.
We can add the extra behavior we need to achieve our original by using andThen to compose a function. This produces a BiFunction that first does one thing with the two inputs and then performs another operation.
Next, let's create a function to coerce our method reference Double::compareTo into a BiFunction:
private static BiFunction asBiFunction(BiFunction function) { return function; }
A lambda or method reference only becomes a BiFunction after it has been converted by a method invocation. We can use this helper function to convert our lambda into the BiFunction object explicitly.
Now, we can use andThen to add behavior on top of the first function:
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. Conclusion
В този урок разгледахме BiFunction и BinaryOperator по отношение на предоставената библиотека Java Streams и нашите собствени потребителски функции. Разгледахме как да предадем BiFunctions, използвайки ламбда и референции на методи, и видяхме как да съставяме функции.
Библиотеките на Java предоставят само едно- и двупараметрични функционални интерфейси. За ситуации, които изискват повече параметри, вижте нашата статия за кариране за повече идеи.
Както винаги, пълните примерни кодове са достъпни в GitHub.