Untuk siapa artikel ini?
- Ini untuk orang yang membaca bagian pertama artikel ini;
- Ini untuk orang-orang yang merasa sudah mengenal Java Core dengan baik, tetapi tidak tahu tentang ekspresi lambda di Java. Atau mungkin mereka pernah mendengar sesuatu tentang ekspresi lambda, tetapi detailnya kurang.
- Ini untuk orang-orang yang memiliki pemahaman tertentu tentang ekspresi lambda, tetapi masih takut dan tidak terbiasa menggunakannya.
Akses ke variabel eksternal
Apakah kode ini dikompilasi dengan kelas anonim?
int counter = 0;
Runnable r = new Runnable() {
@Override
public void run() {
counter++;
}
};
Tidak. counter
Variabel harus final
. Atau jika tidak final
, maka setidaknya tidak dapat mengubah nilainya. Prinsip yang sama berlaku dalam ekspresi lambda. Mereka dapat mengakses semua variabel yang dapat mereka "lihat" dari tempat mereka dideklarasikan. Tetapi lambda tidak boleh mengubahnya (berikan nilai baru padanya). Namun, ada cara untuk melewati batasan ini di kelas anonim. Cukup buat variabel referensi dan ubah status internal objek. Dengan demikian, variabel itu sendiri tidak berubah (menunjuk ke objek yang sama) dan dapat dengan aman ditandai sebagai final
.
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() {
@Override
public void run() {
counter.incrementAndGet();
}
};
Di sini variabel kita counter
adalah referensi ke suatu AtomicInteger
objek. Dan incrementAndGet()
metode ini digunakan untuk mengubah keadaan objek ini. Nilai variabel itu sendiri tidak berubah saat program sedang berjalan. Itu selalu menunjuk ke objek yang sama, yang memungkinkan kita mendeklarasikan variabel dengan kata kunci final. Berikut adalah contoh yang sama, tetapi dengan ekspresi lambda:
int counter = 0;
Runnable r = () -> counter++;
Ini tidak dapat dikompilasi karena alasan yang sama dengan versi dengan kelas anonim: counter
tidak boleh berubah saat program sedang berjalan. Tapi semuanya baik-baik saja jika kita melakukannya seperti ini:
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = () -> counter.incrementAndGet();
Ini juga berlaku untuk metode pemanggilan. Dalam ekspresi lambda, Anda tidak hanya dapat mengakses semua variabel yang "terlihat", tetapi juga memanggil metode apa pun yang dapat diakses.
public class Main {
public static void main(String[] args) {
Runnable runnable = () -> staticMethod();
new Thread(runnable).start();
}
private static void staticMethod() {
System.out.println("I'm staticMethod(), and someone just called me!");
}
}
Meskipun staticMethod()
bersifat pribadi, ini dapat diakses di dalam main()
metode, sehingga juga dapat dipanggil dari dalam lambda yang dibuat di dalam main
metode.
Kapan ekspresi lambda dieksekusi?
Anda mungkin menemukan pertanyaan berikut terlalu sederhana, tetapi Anda harus menanyakannya dengan cara yang sama: kapan kode di dalam ekspresi lambda akan dieksekusi? Kapan itu dibuat? Atau kapan disebut (yang belum diketahui)? Ini cukup mudah untuk diperiksa.
System.out.println("Program start");
// All sorts of code here
// ...
System.out.println("Before lambda declaration");
Runnable runnable = () -> System.out.println("I'm a lambda!");
System.out.println("After lambda declaration");
// All sorts of other code here
// ...
System.out.println("Before passing the lambda to the thread");
new Thread(runnable).start();
Keluaran layar:
Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
Anda dapat melihat bahwa ekspresi lambda dieksekusi di bagian paling akhir, setelah utas dibuat dan hanya ketika eksekusi program mencapai metode run()
. Tentu saja tidak saat diumumkan. Dengan mendeklarasikan ekspresi lambda, kami hanya membuat Runnable
objek dan menjelaskan bagaimana run()
metodenya berperilaku. Metode itu sendiri dieksekusi lama kemudian.
Referensi metode?
Referensi metode tidak terkait langsung dengan lambda, tetapi menurut saya masuk akal untuk mengatakan beberapa patah kata tentangnya di artikel ini. Misalkan kita memiliki ekspresi lambda yang tidak melakukan sesuatu yang istimewa, tetapi hanya memanggil metode.
x -> System.out.println(x)
Ia menerima beberapa x
dan hanya panggilan System.out.println()
, lewat x
. Dalam hal ini, kita dapat menggantinya dengan referensi metode yang diinginkan. Seperti ini:
System.out::println
Itu benar — tidak ada tanda kurung di akhir! Berikut contoh yang lebih lengkap:
List<String> strings = new LinkedList<>();
strings.add("Dota");
strings.add("GTA5");
strings.add("Halo");
strings.forEach(x -> System.out.println(x));
Di baris terakhir, kami menggunakan forEach()
metode yang mengambil objek yang mengimplementasikan Consumer
antarmuka. Sekali lagi, ini adalah antarmuka fungsional, hanya memiliki satu void accept(T t)
metode. Oleh karena itu, kami menulis ekspresi lambda yang memiliki satu parameter (karena diketik di antarmuka itu sendiri, kami tidak menentukan tipe parameter; kami hanya menunjukkan bahwa kami akan memanggilnya x
). Di badan ekspresi lambda, kami menulis kode yang akan dieksekusi saat metode accept()
dipanggil. Di sini kami hanya menampilkan apa yang berakhir di x
variabel. Metode yang sama ini forEach()
mengiterasi semua elemen dalam koleksi dan memanggil accept()
metode pada implementasiConsumer
antarmuka (lambda kami), meneruskan setiap item dalam koleksi. Seperti yang saya katakan, kita dapat mengganti ekspresi lambda (yang hanya mengklasifikasikan metode yang berbeda) dengan referensi ke metode yang diinginkan. Maka kode kita akan terlihat seperti ini:
List<String> strings = new LinkedList<>();
strings.add("Dota");
strings.add("GTA5");
strings.add("Halo");
strings.forEach(System.out::println);
Hal utama adalah bahwa parameter dari metode println()
dan accept()
cocok. Karena println()
metode dapat menerima apa saja (itu kelebihan beban untuk semua tipe primitif dan semua objek), alih-alih ekspresi lambda, kita cukup meneruskan referensi ke metode println()
ke forEach()
. Kemudian forEach()
akan mengambil setiap elemen dalam koleksi dan meneruskannya langsung ke println()
metode. Bagi siapa pun yang mengalami ini untuk pertama kalinya, harap perhatikan bahwa kami tidak memanggil System.out.println()
(dengan titik di antara kata dan dengan tanda kurung di akhir). Sebagai gantinya, kami memberikan referensi ke metode ini. Jika kita menulis ini
strings.forEach(System.out.println());
kita akan mengalami kesalahan kompilasi. Sebelum pemanggilan ke forEach()
, Java melihat apa System.out.println()
yang dipanggil, sehingga ia memahami bahwa nilai yang dikembalikan adalah void
dan akan mencoba meneruskan void
ke forEach()
, yang sebaliknya mengharapkan sebuah Consumer
objek.
Sintaks untuk referensi metode
Ini cukup sederhana:-
Kami meneruskan referensi ke metode statis seperti ini:
ClassName::staticMethodName
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("Dota"); strings.add("GTA5"); strings.add("Halo"); strings.forEach(Main::staticMethod); } private static void staticMethod(String s) { // Do something } }
-
Kami meneruskan referensi ke metode non-statis menggunakan objek yang sudah ada, seperti ini:
objectName::instanceMethodName
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("Dota"); strings.add("GTA5"); strings.add("Halo"); Main instance = new Main(); strings.forEach(instance::nonStaticMethod); } private void nonStaticMethod(String s) { // Do something } }
-
Kami meneruskan referensi ke metode non-statis menggunakan kelas yang mengimplementasikannya sebagai berikut:
ClassName::methodName
public class Main { public static void main(String[] args) { List<User> users = new LinkedList<>(); users.add (new User("John")); users.add(new User("Paul")); users.add(new User("George")); users.forEach(User::print); } private static class User { private String name; private User(String name) { this.name = name; } private void print() { System.out.println(name); } } }
-
Kami meneruskan referensi ke konstruktor seperti ini:
ClassName::new
Referensi metode sangat nyaman ketika Anda sudah memiliki metode yang akan bekerja dengan sempurna sebagai panggilan balik. Dalam hal ini, alih-alih menulis ekspresi lambda yang berisi kode metode, atau menulis ekspresi lambda yang hanya memanggil metode, kami cukup meneruskan referensi ke metode tersebut. Dan itu saja.
Perbedaan yang menarik antara kelas anonim dan ekspresi lambda
Di kelas anonim,this
kata kunci menunjuk ke objek kelas anonim. Tetapi jika kita menggunakan ini di dalam lambda, kita mendapatkan akses ke objek dari kelas yang memuatnya. Di mana kami benar-benar menulis ekspresi lambda. Ini terjadi karena ekspresi lambda dikompilasi menjadi metode privat dari kelas tempat mereka ditulis. Saya tidak akan merekomendasikan penggunaan "fitur" ini, karena memiliki efek samping dan bertentangan dengan prinsip pemrograman fungsional. Yang mengatakan, pendekatan ini sepenuhnya konsisten dengan OOP. ;)
Dari mana saya mendapatkan informasi saya dan apa lagi yang harus Anda baca?
- Tutorial di situs web resmi Oracle. Banyak informasi rinci, termasuk contoh.
- Bab tentang referensi metode dalam tutorial Oracle yang sama.
- Tersedot ke Wikipedia jika Anda benar-benar penasaran.
GO TO FULL VERSION