1. Pengenalan
Artikel ini adalah panduan untuk pelbagai fungsi yang terdapat di Java 8, kes penggunaan umum dan penggunaannya di pustaka JDK standard.
2. Lambdas di Jawa 8
Java 8 membawa peningkatan sintaksis baru yang kuat dalam bentuk ungkapan lambda. Lambda adalah fungsi tanpa nama yang dapat ditangani sebagai warga bahasa kelas pertama, misalnya diteruskan ke atau dikembalikan dari suatu kaedah.
Sebelum Java 8, anda biasanya membuat kelas untuk setiap kes di mana anda perlu merangkumi satu bahagian fungsi. Ini menyiratkan banyak kod pelat boiler yang tidak perlu untuk menentukan sesuatu yang berfungsi sebagai perwakilan fungsi primitif.
Lambdas, antara muka fungsional dan amalan terbaik bekerja dengannya, secara umum, dijelaskan dalam artikel "Ekspresi Lambda dan Antaramuka Fungsional: Petua dan Amalan Terbaik". Panduan ini memberi tumpuan kepada beberapa antara muka fungsi tertentu yang terdapat dalam pakej fungsi java.util.fungsi .
3. Antara Muka Berfungsi
Semua antara muka berfungsi disyorkan untuk mempunyai anotasi @FunctionalInterface yang bermaklumat . Ini bukan sahaja menyampaikan tujuan antara muka ini dengan jelas, tetapi juga membolehkan penyusun menghasilkan ralat sekiranya antara muka yang diberi anotasi tidak memenuhi syarat.
Sebarang antara muka dengan SAM (Kaedah Abstrak Tunggal) adalah antara muka yang berfungsi , dan pelaksanaannya dapat dianggap sebagai ungkapan lambda.
Perhatikan bahawa kaedah lalai Java 8 tidak abstrak dan tidak dikira: antara muka berfungsi mungkin masih mempunyai banyak kaedah lalai . Anda dapat melihatnya dengan melihat dokumentasi Fungsi .
4. Fungsi
Kes lambda yang paling mudah dan umum adalah antara muka fungsional dengan kaedah yang menerima satu nilai dan mengembalikan nilai yang lain. Fungsi argumen tunggal diwakili oleh antara muka Fungsi yang di parameter oleh jenis argumennya dan nilai kembali:
public interface Function { … }
Salah satu penggunaan jenis Fungsi di perpustakaan standard adalah kaedah Map.computeIfAbsent yang mengembalikan nilai dari peta dengan kunci tetapi mengira nilai jika kunci belum ada dalam peta. Untuk mengira nilai, ia menggunakan pelaksanaan Fungsi lulus:
Map nameMap = new HashMap(); Integer value = nameMap.computeIfAbsent("John", s -> s.length());
Nilai, dalam hal ini, akan dihitung dengan menerapkan fungsi pada kunci, dimasukkan ke dalam peta dan juga dikembalikan dari panggilan kaedah. Ngomong-ngomong , kami boleh mengganti lambda dengan rujukan kaedah yang sesuai dengan jenis nilai lulus dan pulangan .
Ingat bahawa objek yang digunakan kaedah ini, sebenarnya, adalah argumen pertama yang tersirat dari suatu kaedah, yang memungkinkan untuk membuang rujukan panjang kaedah contoh ke antara muka Fungsi :
Integer value = nameMap.computeIfAbsent("John", String::length);
The Fungsi muka juga mempunyai lalai karang kaedah yang membolehkan untuk menggabungkan beberapa fungsi menjadi satu dan melaksanakan mengikut urutan:
Function intToString = Object::toString; Function quote = s -> "'" + s + "'"; Function quoteIntToString = quote.compose(intToString); assertEquals("'5'", quoteIntToString.apply(5));
The quoteIntToString fungsi adalah gabungan daripada quote fungsi digunakan untuk hasil yang intToString fungsi.
5. Pengkhususan Fungsi Primitif
Oleh kerana jenis primitif tidak boleh menjadi argumen jenis generik, terdapat versi antara muka Fungsi untuk jenis primitif yang paling banyak digunakan berganda , int , panjang , dan gabungannya dalam jenis argumen dan pengembalian:
- IntFunction , LongFunction , DoubleFunction: argumen adalah jenis yang ditentukan, jenis pengembalian diparameter
- ToIntFunction , ToLongFunction , ToDoubleFunction: return type adalah jenis yang ditentukan, argumen di parameterkan
- DoubleToIntFunction , DoubleToLongFunction , IntToDoubleFunction , IntToLongFunction , LongToIntFunction , LongToDoubleFunction - mempunyai kedua-dua jenis argumen dan pengembalian yang ditakrifkan sebagai jenis primitif, seperti yang ditentukan oleh namanya
Tidak ada antara muka fungsional di luar kotak untuk, misalnya, fungsi yang memerlukan pendek dan mengembalikan bait , tetapi tidak ada yang menghalang anda daripada menulis sendiri:
@FunctionalInterface public interface ShortToByteFunction { byte applyAsByte(short s); }
Sekarang kita dapat menulis kaedah yang mengubah array pendek menjadi array byte menggunakan aturan yang ditentukan oleh ShortToByteFungsi :
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 kami menggunakannya untuk mengubah susunan seluar pendek ke susunan bait 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. Pengkhususan Fungsi Dua Ariti
Untuk menentukan lambdas dengan dua argumen, kita harus menggunakan antaramuka tambahan yang mengandungi kata kunci "Bi" dalam nama mereka: BiFunction , ToDoubleBiFunction , ToIntBiFunction , dan ToLongBiFunction .
BiFunction mempunyai kedua-dua argumen dan jenis pengembalian yang dihasilkan, sementara ToDoubleBiFunction dan yang lain membolehkan anda mengembalikan nilai primitif.
Salah satu contoh khas penggunaan antara muka ini dalam API standard adalah dalam kaedah Map.replaceAll , yang memungkinkan untuk mengganti semua nilai dalam peta dengan beberapa nilai yang dihitung.
Mari gunakan pelaksanaan BiFungsi yang menerima kunci dan nilai lama untuk mengira 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. Pembekal
The Supplier antara muka fungsi merupakan satu lagi fungsi pengkhususan yang tidak mengambil apa-apa hujah. Ia biasanya digunakan untuk penjanaan nilai yang malas. Sebagai contoh, mari kita tentukan fungsi yang kuasa dua nilai. Ia tidak akan menerima nilai itu sendiri, tetapi Pembekal nilai ini:
public double squareLazy(Supplier lazyValue) { return Math.pow(lazyValue.get(), 2); }
Ini membolehkan kita dengan malas menghasilkan hujah untuk memohon fungsi ini menggunakan pelaksanaan Pembekal . Ini boleh berguna sekiranya penghasilan hujah ini memerlukan banyak masa. Kami akan mensimulasikannya dengan menggunakan kaedah tidur Jambu Batu tanpa gangguan :
Supplier lazyValue = () -> { Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS); return 9d; }; Double valueSquared = squareLazy(lazyValue);
Kes penggunaan lain untuk Pembekal adalah menentukan logik untuk penjujukan urutan. Untuk menunjukkannya, mari gunakan kaedah Stream.generate statik untuk membuat nombor Stream of 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 kaedah Stream.generate menerapkan antara muka fungsional Pembekal . Perhatikan bahawa untuk berguna sebagai penjana, Pembekal biasanya memerlukan semacam keadaan luaran. Dalam kes ini, keadaannya terdiri daripada dua nombor urutan Fibonacci terakhir.
Untuk melaksanakan keadaan ini, kami menggunakan larik dan bukan beberapa pemboleh ubah, kerana semua pemboleh ubah luaran yang digunakan di dalam lambda harus final secara efektif .
Pengkhususan lain antara muka fungsi Pembekal termasuk BooleanSupplier , DoubleSupplier , LongSupplier dan IntSupplier , yang jenis pengembaliannya adalah primitif yang sesuai.
8. Pengguna
Berbanding dengan Pembekal , Pengguna menerima hujah yang baik dan tidak mengembalikan apa-apa. Ini adalah fungsi yang mewakili kesan sampingan.
Sebagai contoh, mari kita sambut semua orang dalam senarai nama dengan mencetak ucapan di konsol. Lambda diteruskan ke kaedah List.forEach menggunakan antara muka fungsi Pengguna :
List names = Arrays.asList("John", "Freddy", "Samuel"); names.forEach(name -> System.out.println("Hello, " + name));
There are also specialized versions of the Consumer — DoubleConsumer, 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 antara muka fungsi muncul di Jawa 8. Banyak muka dari versi terdahulu Java mematuhi kekangan yang FunctionalInterface dan boleh digunakan sebagai lambdas. Contoh yang menonjol adalah antara muka Runnable dan Callable yang digunakan dalam API serentak. Di Java 8 antara muka ini juga ditandai dengan anotasi @FunctionalInterface . Ini membolehkan kita mempermudah kod serentak:
Thread thread = new Thread(() -> System.out.println("Hello From Another Thread")); thread.start();
12. Kesimpulannya
Dalam artikel ini, kami telah menerangkan pelbagai fungsi yang terdapat di Java 8 API yang dapat digunakan sebagai ungkapan lambda. Kod sumber untuk artikel tersebut terdapat di GitHub.