Panduan untuk Antarmuka BiFungsi Java

1. Perkenalan

Java 8 memperkenalkan pemrograman gaya fungsional, memungkinkan kita untuk membuat parameter metode tujuan umum dengan meneruskan fungsi.

Kami mungkin paling akrab dengan antarmuka fungsional Java 8 parameter tunggal seperti Fungsi , Predikat, dan Konsumen .

Dalam tutorial ini, kita akan melihat antarmuka fungsional yang menggunakan dua parameter . Fungsi tersebut disebut fungsi biner dan direpresentasikan di Jawa dengan antarmuka fungsional BiFunction .

2. Fungsi Parameter Tunggal

Mari cepat kita rekap bagaimana kita menggunakan parameter tunggal atau fungsi unary, seperti yang kita lakukan di aliran:

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

Seperti yang bisa kita lihat, peta menggunakan Fungsi , yang mengambil satu parameter dan memungkinkan kita melakukan operasi pada nilai itu, mengembalikan nilai baru.

3. Operasi Dua Parameter

Pustaka Java Stream memberi kita fungsi pengurangan yang memungkinkan kita menggabungkan elemen-elemen aliran . Kita perlu mengungkapkan bagaimana nilai yang telah kita kumpulkan sejauh ini diubah dengan menambahkan item berikutnya.

Fungsi pengurangan menggunakan antarmuka fungsional BinaryOperator , yang mengambil dua objek dengan tipe yang sama sebagai inputnya.

Bayangkan kita ingin menggabungkan semua item di aliran kita dengan meletakkan yang baru di depan dengan pemisah tanda hubung. Kami akan melihat beberapa cara untuk menerapkan ini di bagian berikut.

3.1. Menggunakan Lambda

Implementasi lambda untuk BiFunction diawali oleh dua parameter, diapit oleh tanda kurung:

String result = Stream.of("hello", "world") .reduce("", (a, b) -> b + "-" + a); assertThat(result).isEqualTo("world-hello-");

Seperti yang bisa kita lihat, dua nilai, a dan b adalah String . Kami telah menulis lambda yang menggabungkannya untuk membuat keluaran yang diinginkan, dengan yang kedua terlebih dahulu, dan tanda hubung di antaranya.

Kita harus mencatat bahwa reduce menggunakan nilai awal - dalam hal ini, string kosong. Jadi, kami berakhir dengan tanda hubung dengan kode di atas, karena nilai pertama dari aliran kami digabungkan dengannya.

Juga, kita harus mencatat bahwa inferensi tipe Java memungkinkan kita untuk menghilangkan tipe parameter kita hampir sepanjang waktu. Dalam situasi di mana tipe lambda tidak jelas dari konteksnya, kita dapat menggunakan tipe untuk parameter kita:

String result = Stream.of("hello", "world") .reduce("", (String a, String b) -> b + "-" + a);

3.2. Menggunakan Fungsi

Bagaimana jika kita ingin agar algoritma di atas tidak menaruh tanda hubung di ujungnya? Kami dapat menulis lebih banyak kode di lambda kami, tetapi itu mungkin menjadi berantakan. Mari mengekstrak fungsi sebagai gantinya:

private String combineWithoutTrailingDash(String a, String b) { if (a.isEmpty()) { return b; } return b + "-" + a; }

Dan kemudian menyebutnya:

String result = Stream.of("hello", "world") .reduce("", (a, b) -> combineWithoutTrailingDash(a, b)); assertThat(result).isEqualTo("world-hello");

Seperti yang bisa kita lihat, lambda memanggil fungsi kita, yang lebih mudah dibaca daripada meletakkan implementasi yang lebih kompleks sebaris.

3.3. Menggunakan Referensi Metode

Beberapa IDE akan secara otomatis meminta kita untuk mengubah lambda di atas menjadi referensi metode, karena seringkali lebih jelas untuk dibaca.

Mari tulis ulang kode kita untuk menggunakan referensi metode:

String result = Stream.of("hello", "world") .reduce("", this::combineWithoutTrailingDash); assertThat(result).isEqualTo("world-hello");

Referensi metode sering kali membuat kode fungsional lebih jelas.

4. Menggunakan BiFunction

Sejauh ini, kami telah mendemonstrasikan cara menggunakan fungsi di mana kedua parameter memiliki tipe yang sama. The BiFunction antarmuka memungkinkan kita untuk menggunakan parameter dari berbagai jenis , dengan nilai kembali dari tipe ketiga.

Mari kita bayangkan bahwa kita membuat algoritma untuk menggabungkan dua daftar dengan ukuran yang sama ke dalam daftar ketiga dengan melakukan operasi pada setiap pasangan elemen:

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. Menggeneralisasi Fungsi

Kita dapat menggeneralisasi fungsi khusus ini menggunakan BiFungsi sebagai penggabung:

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

Mari kita lihat apa yang terjadi di sini. Ada tiga tipe parameter: T untuk tipe item di daftar pertama, U untuk tipe di daftar kedua, lalu R untuk tipe apa pun yang dikembalikan fungsi kombinasi.

Kami menggunakan BiFunction disediakan untuk fungsi ini dengan memanggil berlaku metode untuk mendapatkan hasilnya.

4.2. Memanggil Fungsi Umum

Penggabung kami adalah BiFungsi , yang memungkinkan kami memasukkan algoritme, apa pun jenis input dan outputnya. Mari kita coba:

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

Dan kita dapat menggunakan ini untuk jenis input dan output yang sama sekali berbeda.

Mari masukkan algoritme untuk menentukan apakah nilai di daftar pertama lebih besar dari nilai di daftar kedua dan menghasilkan hasil boolean :

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. Sebuah BiFunction Metode Referensi

Mari kita tulis ulang kode di atas dengan metode yang diekstrak dan referensi metode:

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

Dalam tutorial ini, kami telah menjelajahi BiFunction dan BinaryOperator dalam hal pustaka Java Streams yang disediakan dan fungsi kustom kami sendiri. Kami telah melihat cara meneruskan BiFunctions menggunakan lambda dan referensi metode, dan kami telah melihat cara membuat fungsi.

Pustaka Java hanya menyediakan antarmuka fungsional satu dan dua parameter. Untuk situasi yang membutuhkan lebih banyak parameter, lihat artikel kami tentang kari untuk mendapatkan lebih banyak ide.

Seperti biasa, contoh kode lengkap tersedia di GitHub.