Antarmuka Fungsional di Java 8

1. Perkenalan

Artikel ini adalah panduan untuk antarmuka fungsional berbeda yang ada di Java 8, kasus penggunaan umum, dan penggunaannya di pustaka JDK standar.

2. Lambdas di Jawa 8

Java 8 membawa peningkatan sintaksis baru yang kuat dalam bentuk ekspresi lambda. Lambda adalah fungsi anonim yang bisa ditangani sebagai warga bahasa kelas satu, misalnya diteruskan ke atau dikembalikan dari metode.

Sebelum Java 8, Anda biasanya akan membuat kelas untuk setiap kasus di mana Anda perlu merangkum satu fungsi. Ini menyiratkan banyak kode boilerplate yang tidak perlu untuk mendefinisikan sesuatu yang berfungsi sebagai representasi fungsi primitif.

Lambda, antarmuka fungsional dan praktik terbaik untuk bekerja dengannya, secara umum, dijelaskan dalam artikel "Ekspresi Lambda dan Antarmuka Fungsional: Kiat dan Praktik Terbaik". Panduan ini berfokus pada beberapa antarmuka fungsional tertentu yang ada dalam paket java.util.function .

3. Antarmuka Fungsional

Semua antarmuka fungsional direkomendasikan untuk memiliki anotasi @FunctionalInterface yang informatif . Ini tidak hanya mengkomunikasikan dengan jelas tujuan dari antarmuka ini, tetapi juga memungkinkan kompiler untuk menghasilkan kesalahan jika antarmuka yang dianotasi tidak memenuhi syarat.

Setiap antarmuka dengan SAM (Single Abstract Method) adalah antarmuka fungsional , dan implementasinya dapat diperlakukan sebagai ekspresi lambda.

Perhatikan bahwa metode default Java 8 tidak abstrak dan tidak dihitung: antarmuka fungsional mungkin masih memiliki beberapa metode default . Anda dapat mengamati ini dengan melihat dokumentasi Function .

4. Fungsi

Kasus lambda yang paling sederhana dan umum adalah antarmuka fungsional dengan metode yang menerima satu nilai dan mengembalikan nilai lain. Fungsi argumen tunggal ini diwakili oleh antarmuka Fungsi yang diparameterisasi oleh jenis argumennya dan nilai yang dikembalikan:

public interface Function { … }

Salah satu penggunaan tipe Fungsi di pustaka standar adalah metode Map.computeIfAbsent yang mengembalikan nilai dari peta dengan kunci tetapi menghitung nilai jika kunci belum ada di peta. Untuk menghitung nilai, ini menggunakan implementasi Fungsi yang diteruskan:

Map nameMap = new HashMap(); Integer value = nameMap.computeIfAbsent("John", s -> s.length());

Nilai, dalam hal ini, akan dihitung dengan menerapkan fungsi ke kunci, dimasukkan ke dalam peta, dan juga dikembalikan dari pemanggilan metode. Ngomong-ngomong, kita bisa mengganti lambda dengan referensi metode yang cocok dengan tipe nilai yang diteruskan dan dikembalikan .

Ingatlah bahwa objek tempat metode dipanggil sebenarnya adalah argumen pertama implisit dari suatu metode, yang memungkinkan mentransmisikan referensi panjang metode instance ke antarmuka Fungsi :

Integer value = nameMap.computeIfAbsent("John", String::length);

The Fungsi antarmuka juga memiliki standar compose metode yang memungkinkan untuk menggabungkan beberapa fungsi menjadi satu dan mengeksekusi mereka secara berurutan:

Function intToString = Object::toString; Function quote = s -> "'" + s + "'"; Function quoteIntToString = quote.compose(intToString); assertEquals("'5'", quoteIntToString.apply(5));

Fungsi quoteIntToString adalah kombinasi dari fungsi kutipan yang diterapkan ke hasil dari fungsi intToString .

5. Spesialisasi Fungsi Primitif

Karena tipe primitif tidak bisa menjadi argumen tipe generik, ada versi antarmuka Fungsi untuk tipe primitif yang paling banyak digunakan double , int , long , dan kombinasinya dalam tipe argumen dan kembalian:

  • IntFunction , LongFunction , DoubleFunction: argumen memiliki tipe yang ditentukan, tipe kembalian diberi parameter
  • ToIntFunction , ToLongFunction , ToDoubleFunction: tipe kembalian adalah tipe yang ditentukan, argumen diparameterisasi
  • DoubleToIntFunction , DoubleToLongFunction , IntToDoubleFunction , IntToLongFunction , LongToIntFunction , LongToDoubleFunction - memiliki argumen dan tipe kembalian yang didefinisikan sebagai tipe primitif, seperti yang ditentukan oleh namanya

Tidak ada antarmuka fungsional out-of-the-box untuk, katakanlah, fungsi yang membutuhkan waktu singkat dan mengembalikan satu byte , tetapi tidak ada yang menghentikan Anda untuk menulis milik Anda sendiri:

@FunctionalInterface public interface ShortToByteFunction { byte applyAsByte(short s); }

Sekarang kita dapat menulis metode yang mengubah array pendek menjadi array byte menggunakan aturan yang ditentukan oleh ShortToByteFunction :

public byte[] transformArray(short[] array, ShortToByteFunction function) { byte[] transformedArray = new byte[array.length]; for (int i = 0; i < array.length; i++) { transformedArray[i] = function.applyAsByte(array[i]); } return transformedArray; }

Inilah cara kita menggunakannya untuk mengubah array short menjadi array byte dikalikan dengan 2:

short[] array = {(short) 1, (short) 2, (short) 3}; byte[] transformedArray = transformArray(array, s -> (byte) (s * 2)); byte[] expectedArray = {(byte) 2, (byte) 4, (byte) 6}; assertArrayEquals(expectedArray, transformedArray);

6. Spesialisasi Fungsi Dua Aritas

Untuk mendefinisikan lambda dengan dua argumen, kita harus menggunakan antarmuka tambahan yang berisi kata kunci "Bi" dalam namanya: BiFunction , ToDoubleBiFunction , ToIntBiFunction , dan ToLongBiFunction .

BiFunction memiliki argumen dan tipe kembalian yang dihasilkan, sementara ToDoubleBiFunction dan lainnya memungkinkan Anda mengembalikan nilai primitif.

Salah satu contoh umum penggunaan antarmuka ini dalam API standar adalah dalam metode Map.replaceAll , yang memungkinkan penggantian semua nilai dalam peta dengan beberapa nilai yang dihitung.

Mari gunakan implementasi BiFunction yang menerima kunci dan nilai lama untuk menghitung nilai baru untuk gaji dan mengembalikannya.

Map salaries = new HashMap(); salaries.put("John", 40000); salaries.put("Freddy", 30000); salaries.put("Samuel", 50000); salaries.replaceAll((name, oldValue) -> name.equals("Freddy") ? oldValue : oldValue + 10000);

7. Pemasok

The Pemasok antarmuka fungsional adalah satu lagi Fungsi spesialisasi yang tidak mengambil argumen. Ini biasanya digunakan untuk pembuatan nilai yang lambat. Misalnya, mari tentukan fungsi yang mengkuadratkan nilai ganda . Itu tidak akan menerima nilai itu sendiri, tetapi Pemasok dengan nilai ini:

public double squareLazy(Supplier lazyValue) { return Math.pow(lazyValue.get(), 2); }

Ini memungkinkan kita untuk dengan malas menghasilkan argumen untuk pemanggilan fungsi ini menggunakan implementasi Pemasok . Ini dapat berguna jika pembuatan argumen ini membutuhkan banyak waktu. Kami akan mensimulasikannya menggunakan metode sleepUninterruptibly Guava :

Supplier lazyValue = () -> { Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS); return 9d; }; Double valueSquared = squareLazy(lazyValue);

Kasus penggunaan lain untuk Pemasok menentukan logika untuk pembuatan urutan. Untuk mendemonstrasikannya, mari gunakan metode Stream.generate statis untuk membuat Arus bilangan Fibonacci:

int[] fibs = {0, 1}; Stream fibonacci = Stream.generate(() -> { int result = fibs[1]; int fib3 = fibs[0] + fibs[1]; fibs[0] = fibs[1]; fibs[1] = fib3; return result; });

Fungsi yang diteruskan ke metode Stream.generate mengimplementasikan antarmuka fungsional Pemasok . Perhatikan bahwa agar berguna sebagai generator, Pemasok biasanya membutuhkan semacam status eksternal. Dalam hal ini, statusnya terdiri dari dua nomor deret Fibonacci terakhir.

Untuk mengimplementasikan status ini, kami menggunakan array, bukan beberapa variabel, karena semua variabel eksternal yang digunakan di dalam lambda harus final secara efektif .

Spesialisasi lain dari antarmuka fungsional Pemasok termasuk BooleanSupplier , DoubleSupplier , LongSupplier dan IntSupplier , yang jenis kembaliannya adalah primitif yang sesuai.

8. Konsumen

Berbeda dengan Pemasok , Konsumen menerima argumen yang dibuat dan tidak mengembalikan apa pun. Ini adalah fungsi yang mewakili efek samping.

Misalnya, mari menyapa semua orang dalam daftar nama dengan mencetak salam di konsol. Lambda yang diteruskan ke metode List.forEach mengimplementasikan antarmuka fungsional Konsumen :

List names = Arrays.asList("John", "Freddy", "Samuel"); names.forEach(name -> System.out.println("Hello, " + name));

There are also specialized versions of the ConsumerDoubleConsumer, IntConsumer and LongConsumer — that receive primitive values as arguments. More interesting is the BiConsumer interface. One of its use cases is iterating through the entries of a map:

Map ages = new HashMap(); ages.put("John", 25); ages.put("Freddy", 24); ages.put("Samuel", 30); ages.forEach((name, age) -> System.out.println(name + " is " + age + " years old"));

Another set of specialized BiConsumer versions is comprised of ObjDoubleConsumer, ObjIntConsumer, and ObjLongConsumer which receive two arguments one of which is generified, and another is a primitive type.

9. Predicates

In mathematical logic, a predicate is a function that receives a value and returns a boolean value.

The Predicate functional interface is a specialization of a Function that receives a generified value and returns a boolean. A typical use case of the Predicate lambda is to filter a collection of values:

List names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David"); List namesWithA = names.stream() .filter(name -> name.startsWith("A")) .collect(Collectors.toList());

In the code above we filter a list using the Stream API and keep only names that start with the letter “A”. The filtering logic is encapsulated in the Predicate implementation.

As in all previous examples, there are IntPredicate, DoublePredicate and LongPredicate versions of this function that receive primitive values.

10. Operators

Operator interfaces are special cases of a function that receive and return the same value type. The UnaryOperator interface receives a single argument. One of its use cases in the Collections API is to replace all values in a list with some computed values of the same type:

List names = Arrays.asList("bob", "josh", "megan"); names.replaceAll(name -> name.toUpperCase());

The List.replaceAll function returns void, as it replaces the values in place. To fit the purpose, the lambda used to transform the values of a list has to return the same result type as it receives. This is why the UnaryOperator is useful here.

Of course, instead of name -> name.toUpperCase(), you can simply use a method reference:

names.replaceAll(String::toUpperCase);

One of the most interesting use cases of a BinaryOperator is a reduction operation. Suppose we want to aggregate a collection of integers in a sum of all values. With Stream API, we could do this using a collector, but a more generic way to do it would be to use the reduce method:

List values = Arrays.asList(3, 5, 8, 9, 12); int sum = values.stream() .reduce(0, (i1, i2) -> i1 + i2); 

The reduce method receives an initial accumulator value and a BinaryOperator function. The arguments of this function are a pair of values of the same type, and a function itself contains a logic for joining them in a single value of the same type. Passed function must be associative, which means that the order of value aggregation does not matter, i.e. the following condition should hold:

op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)

The associative property of a BinaryOperator operator function allows to easily parallelize the reduction process.

Of course, there are also specializations of UnaryOperator and BinaryOperator that can be used with primitive values, namely DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator, and LongBinaryOperator.

11. Legacy Functional Interfaces

Tidak semua antarmuka fungsional muncul di Java 8. Banyak antarmuka dari versi Java sebelumnya sesuai dengan batasan Fungsional Antarmuka dan dapat digunakan sebagai lambda. Contoh yang menonjol adalah antarmuka Runnable dan Callable yang digunakan dalam API konkurensi. Di Java 8 antarmuka ini juga ditandai dengan anotasi @FunctionalInterface . Ini memungkinkan kami untuk sangat menyederhanakan kode konkurensi:

Thread thread = new Thread(() -> System.out.println("Hello From Another Thread")); thread.start();

12. Kesimpulan

Di artikel ini, kami telah menjelaskan berbagai antarmuka fungsional yang ada di Java 8 API yang dapat digunakan sebagai ekspresi lambda. Kode sumber artikel tersedia di GitHub.