Menggunakan cakupan dan penutupan leksikal
Banyak pengembang memiliki kesalahpahaman ini, percaya bahwa menggunakan ekspresi lambda akan menyebabkan redundansi kode dan mengurangi kualitas kode. Sebaliknya, betapapun rumitnya kode tersebut, kami tidak akan melakukan kompromi apa pun pada kualitas kode demi kesederhanaan, seperti yang akan kita lihat di bawah.
Kita dapat menggunakan kembali ekspresi lambda pada contoh sebelumnya; namun, jika kita mencocokkan huruf lain, masalah redundansi kode akan kembali dengan cepat. Mari kita analisis masalah ini lebih jauh terlebih dahulu, lalu gunakan cakupan leksikal dan penutupan untuk menyelesaikannya.
Redundansi yang disebabkan oleh ekspresi lambda
Mari kita filter huruf-huruf berawalan N atau B itu dari teman-teman. Melanjutkan contoh di atas, kode yang kita tulis mungkin terlihat seperti ini:
Copy kode kodenya sebagai berikut:
Predikat akhir<String> startWithN = nama -> nama.startsWith("N");
Predikat akhir<String> startWithB = nama -> nama.startsWith("B");
hitungan panjang terakhirFriendsStartN =
teman.stream()
.filter(mulaiDenganN).count();
hitungan panjang terakhirFriendsStartB =
teman.stream()
.filter(mulaiDenganB).count();
Predikat pertama menentukan apakah nama dimulai dengan N, dan predikat kedua menentukan apakah nama dimulai dengan B. Kami meneruskan kedua instance ini ke dua panggilan metode filter. Hal ini nampaknya masuk akal, namun kedua predikat tersebut mubazir, hanya perbedaan huruf di cek. Mari kita lihat bagaimana kita dapat menghindari redundansi ini.
Gunakan cakupan leksikal untuk menghindari redundansi
Pada solusi pertama, kita dapat mengekstrak huruf sebagai parameter fungsi dan meneruskan fungsi ini ke metode filter. Ini adalah metode yang bagus, tetapi filter tidak diterima oleh semua fungsi. Ia hanya menerima fungsi dengan satu parameter saja. Parameter tersebut sesuai dengan elemen dalam koleksi dan mengembalikan nilai boolean.
Kami berharap ada tempat di mana surat ini dapat disimpan dalam cache hingga parameter dilewatkan (dalam hal ini, parameter nama). Mari buat fungsi baru seperti ini.
Copy kode kodenya sebagai berikut:
public static Predicate<String> checkIfStartsWith(huruf String terakhir) {
nama kembali -> nama.startsWith(huruf);
}
Kami mendefinisikan fungsi statis checkIfStartsWith, yang menerima parameter String dan mengembalikan objek Predikat, yang dapat diteruskan ke metode filter untuk digunakan nanti. Berbeda dengan fungsi tingkat tinggi yang kita lihat sebelumnya, yang menggunakan fungsi sebagai parameter, metode ini mengembalikan fungsi. Namun ini juga merupakan fungsi tingkat tinggi, yang telah kami sebutkan di Evolusi, bukan perubahan, di halaman 12.
Objek Predikat yang dikembalikan oleh metode checkIfStartsWith agak berbeda dari ekspresi lambda lainnya. Dalam pernyataan return name -> name.startsWith(letter) , kita tahu persis apa namanya, itu adalah parameter yang diteruskan ke ekspresi lambda. Tapi apa sebenarnya huruf variabel itu? Itu di luar domain fungsi anonim. Java menemukan domain tempat ekspresi lambda didefinisikan dan menemukan huruf variabel. Ini disebut lingkup leksikal. Cakupan leksikal adalah hal yang sangat berguna, memungkinkan kita untuk menyimpan variabel dalam satu cakupan untuk kemudian digunakan dalam konteks lain. Karena ekspresi lambda ini menggunakan variabel dalam cakupannya, situasi ini disebut juga penutupan. Mengenai batasan akses cakupan leksikal, dapatkah Anda membaca batasan cakupan leksikal di halaman 31?
Apakah ada batasan pada cakupan leksikal?
Dalam ekspresi lambda, kita hanya dapat mengakses tipe final dalam cakupannya atau variabel lokal sebenarnya dari tipe final.
Ekspresi lambda dapat dipanggil segera, tertunda, atau dari thread yang berbeda. Untuk menghindari konflik ras, variabel lokal di domain yang kita akses tidak boleh diubah setelah diinisialisasi. Operasi modifikasi apa pun akan menyebabkan pengecualian kompilasi.
Menandainya sebagai final memecahkan masalah ini, namun Java tidak memaksa kita untuk menandainya dengan cara ini. Faktanya, Java melihat dua hal. Salah satunya adalah variabel yang diakses harus diinisialisasi dalam metode yang mendefinisikannya, dan sebelum ekspresi lambda didefinisikan. Kedua, nilai variabel-variabel ini tidak dapat diubah - artinya, nilai-nilai tersebut sebenarnya bertipe final, meskipun tidak ditandai seperti itu.
Ekspresi lambda tanpa kewarganegaraan adalah konstanta waktu proses, sedangkan ekspresi yang menggunakan variabel lokal memiliki overhead komputasi tambahan.
Saat memanggil metode filter, kita bisa menggunakan ekspresi lambda yang dikembalikan oleh metode checkIfStartsWith, seperti ini:
Copy kode kodenya sebagai berikut:
hitungan panjang terakhirFriendsStartN =
teman.stream() .filter(checkIfStartsWith("N")).count();
hitungan panjang terakhirFriendsStartB = friends.stream()
.filter(checkIfStartsWith("B")).count();
Sebelum memanggil metode filter, pertama-tama kita memanggil metode checkIfStartsWith() dan memasukkan huruf yang diinginkan. Panggilan ini dengan cepat mengembalikan ekspresi lambda, yang kemudian kita teruskan ke metode filter.
Dengan membuat fungsi tingkat tinggi (dalam kasus ini checkIfStartsWith) dan menggunakan cakupan leksikal, kami berhasil menghilangkan redundansi dari kode. Kita tidak perlu lagi berulang kali menentukan apakah sebuah nama diawali dengan huruf tertentu.
Refactor, kurangi cakupan
Pada contoh sebelumnya kita menggunakan metode statis, tetapi kita tidak ingin menggunakan metode statis untuk menyimpan variabel dalam cache, yang akan mengacaukan kode kita. Yang terbaik adalah mempersempit cakupan fungsi ini ke tempat penggunaannya. Kita dapat menggunakan antarmuka Fungsi untuk mencapai hal ini.
Copy kode kodenya sebagai berikut:
Fungsi akhir<String, Predikat<String>>startWithLetter = (String huruf) -> {
Predikat<String> checkStarts = (Nama string) -> nama.startsWith(huruf);
kembalikan pemeriksaanMulai; };
Ekspresi lambda ini menggantikan metode statis asli. Ekspresi ini dapat ditempatkan dalam suatu fungsi dan didefinisikan sebelum diperlukan. Variabel startWithLetter mengacu pada Fungsi yang parameter masukannya adalah String dan parameter keluarannya adalah Predikat.
Dibandingkan dengan menggunakan metode statis, versi ini jauh lebih sederhana, namun kita dapat terus melakukan refaktorisasi agar lebih ringkas. Dari sudut pandang praktis, fungsi ini sama dengan metode statis sebelumnya; keduanya menerima String dan mengembalikan Predikat. Daripada mendeklarasikan Predikat secara eksplisit, kami menggantinya seluruhnya dengan ekspresi lambda.
Copy kode kodenya sebagai berikut:
Fungsi akhir<String, Predikat<String>>startWithLetter = (String huruf) -> (String nama) -> name.startsWith(letter);
Kita telah menghilangkan kekacauan tersebut, namun kita juga dapat menghapus deklarasi tipe agar lebih ringkas, dan compiler Java akan melakukan pengurangan tipe berdasarkan konteksnya. Mari kita lihat versi yang ditingkatkan.
Copy kode kodenya sebagai berikut:
Fungsi akhir<String, Predikat<String>> dimulaiDenganHuruf =
huruf -> nama -> nama.startsWith(huruf);
Dibutuhkan upaya untuk beradaptasi dengan sintaksis ringkas ini. Jika itu membutakan Anda, carilah di tempat lain terlebih dahulu. Kita telah menyelesaikan pemfaktoran ulang kode dan sekarang dapat menggunakannya untuk menggantikan metode checkIfStartsWith() yang asli, seperti ini:
Copy kode kodenya sebagai berikut:
hitungan panjang terakhirFriendsStartN = friends.stream()
.filter(startsWithLetter.apply("N")).count();
hitungan panjang terakhirFriendsStartB = friends.stream()
.filter(startsWithLetter.apply("B")).count();
Di bagian ini kami menggunakan fungsi tingkat tinggi. Kita telah melihat cara membuat fungsi di dalam fungsi jika kita meneruskan suatu fungsi ke fungsi lain, dan cara mengembalikan fungsi dari suatu fungsi. Semua contoh ini menunjukkan kesederhanaan dan kegunaan kembali yang dibawa oleh ekspresi lambda.
Pada bagian ini kita telah sepenuhnya memanfaatkan fungsi Fungsi dan Predikat, namun mari kita lihat perbedaan di antara keduanya. Predikat menerima parameter tipe T dan mengembalikan nilai boolean untuk mewakili benar atau salah dari kondisi penilaian terkait. Saat kita perlu membuat penilaian bersyarat, kita bisa menggunakan Predicateg untuk menyelesaikannya. Metode seperti filter yang elemen filternya menerima Predikat sebagai parameter. Funciton mewakili fungsi yang parameter inputnya berupa variabel bertipe T dan mengembalikan hasil bertipe R. Ini lebih umum daripada Predikat yang hanya dapat mengembalikan boolean. Selama masukan diubah menjadi keluaran, kita dapat menggunakan Fungsi, sehingga wajar jika peta menggunakan Fungsi sebagai parameter.
Seperti yang Anda lihat, memilih elemen dari koleksi sangatlah sederhana. Di bawah ini kami akan memperkenalkan cara memilih hanya satu elemen dari koleksi.