Untuk siapa artikel ini?
- Bagi yang merasa sudah mengetahui Java Core dengan baik, namun belum mengetahui tentang ekspresi lambda di Java. Atau, mungkin, Anda pernah mendengar sesuatu tentang lambda, tetapi tanpa detailnya.
- bagi mereka yang sudah memahami ekspresi lambda, tetapi masih takut dan tidak biasa menggunakannya.
Jika Anda tidak termasuk dalam salah satu kategori ini, Anda mungkin menganggap artikel ini membosankan, tidak benar, dan secara umum “tidak keren”. Dalam hal ini, silakan lewati, atau, jika Anda berpengalaman dalam topik tersebut, sarankan di komentar bagaimana saya dapat meningkatkan atau melengkapi artikel tersebut. Materinya tidak menuntut nilai akademis apa pun, apalagi kebaruan. Sebaliknya, sebaliknya: di dalamnya saya akan mencoba menggambarkan hal-hal yang kompleks (untuk beberapa) sesederhana mungkin. Saya terinspirasi untuk menulis oleh permintaan untuk menjelaskan stream api. Saya memikirkannya dan memutuskan bahwa tanpa memahami ekspresi lambda, beberapa contoh saya tentang “aliran” tidak akan dapat dipahami. Jadi mari kita mulai dengan lambda.
Pengetahuan apa yang diperlukan untuk memahami artikel ini:
- Pengertian Pemrograman Berorientasi Objek (selanjutnya disebut OOP), yaitu :
- pengetahuan tentang apa itu kelas dan objek, apa perbedaan di antara keduanya;
- pengetahuan tentang apa itu antarmuka, perbedaannya dengan kelas, apa hubungan di antara keduanya (antarmuka dan kelas);
- pengetahuan tentang apa itu metode, bagaimana cara memanggilnya, apa itu metode abstrak (atau metode tanpa implementasi), apa parameter/argumen suatu metode, bagaimana meneruskannya ke sana;
- pengubah akses, metode/variabel statis, metode/variabel akhir;
- pewarisan (kelas, antarmuka, pewarisan berganda antarmuka).
- Pengetahuan tentang Java Core: generik, koleksi (daftar), utas.
Baiklah, mari kita mulai.
Sedikit sejarah
Ekspresi Lambda datang ke Java dari pemrograman fungsional, dan dari matematika. Pada pertengahan abad ke-20 di Amerika, ada Gereja Alonzo yang bekerja di Universitas Princeton, yang sangat menyukai matematika dan segala macam abstraksi. Gereja Alonzo-lah yang menemukan kalkulus lambda, yang pada awalnya merupakan sekumpulan ide abstrak dan tidak ada hubungannya dengan pemrograman. Pada saat yang sama, ahli matematika seperti Alan Turing dan John von Neumann bekerja di Universitas Princeton yang sama. Semuanya bersatu: Church menciptakan sistem kalkulus lambda, Turing mengembangkan mesin komputasi abstraknya, yang sekarang dikenal sebagai “mesin Turing.” Nah, von Neumann mengusulkan diagram arsitektur komputer, yang menjadi dasar komputer modern (dan sekarang disebut “arsitektur von Neumann”). Pada saat itu, gagasan Gereja Alonzo tidak sepopuler karya rekan-rekannya (kecuali di bidang matematika “murni”). Namun, beberapa saat kemudian, John McCarthy (juga lulusan Universitas Princeton, pada saat cerita ini dibuat - seorang karyawan Institut Teknologi Massachusetts) menjadi tertarik dengan ide-ide Gereja. Berdasarkan mereka, pada tahun 1958 ia menciptakan bahasa pemrograman fungsional pertama, Lisp. Dan 58 tahun kemudian, ide pemrograman fungsional bocor ke Java sebagai nomor 8. Belum genap 70 tahun berlalu... Sebenarnya, ini bukanlah jangka waktu terlama untuk menerapkan ide matematika dalam praktik.
Intinya
Ekspresi lambda adalah fungsi seperti itu. Anda dapat menganggap ini sebagai metode reguler di Java, satu-satunya perbedaan adalah metode ini dapat diteruskan ke metode lain sebagai argumen. Ya, tidak hanya angka, string, dan kucing yang dapat diteruskan ke metode, tetapi juga metode lain! Kapan kita membutuhkan ini? Misalnya, jika kita ingin meneruskan beberapa panggilan balik. Kita memerlukan metode yang kita panggil agar dapat memanggil metode lain yang kita teruskan ke sana. Artinya, agar kita mempunyai kesempatan untuk mengirimkan satu panggilan balik dalam beberapa kasus, dan panggilan balik lainnya dalam kasus lain. Dan metode kami, yang akan menerima panggilan balik kami, akan memanggil mereka. Contoh sederhananya adalah pengurutan. Katakanlah kita menulis semacam penyortiran rumit yang terlihat seperti ini:
public void mySuperSort() {
if(compare(obj1, obj2) > 0)
}
Dimana,
if
kita memanggil metode tersebut
compare()
, meneruskan ke sana dua objek yang kita bandingkan, dan kita ingin mengetahui objek mana yang “lebih besar”. Kita akan menempatkan yang “lebih besar” sebelum yang “lebih kecil”. Saya menulis "lebih banyak" dalam tanda kutip karena kita sedang menulis metode universal yang akan dapat mengurutkan tidak hanya dalam urutan menaik tetapi juga dalam urutan menurun (dalam hal ini, "lebih" akan menjadi objek yang pada dasarnya lebih kecil, dan sebaliknya) . Untuk menetapkan aturan bagaimana kita ingin mengurutkan, kita perlu meneruskannya ke
mySuperSort()
. Dalam hal ini, kita akan dapat “mengendalikan” metode kita saat sedang dipanggil. Tentu saja, Anda dapat menulis dua metode terpisah
mySuperSortAsc()
untuk
mySuperSortDesc()
mengurutkan dalam urutan menaik dan menurun. Atau berikan beberapa parameter di dalam metode (misalnya,
boolean
if
true
, urutkan dalam urutan menaik, dan if
false
dalam urutan menurun). Tetapi bagaimana jika kita ingin mengurutkan bukan struktur sederhana, tetapi, misalnya, daftar array string? Bagaimana metode kita
mySuperSort()
mengetahui cara mengurutkan array string ini? Untuk ukuran? Berdasarkan total panjang kata? Mungkin berdasarkan abjad, bergantung pada baris pertama dalam array? Namun bagaimana jika, dalam beberapa kasus, kita perlu mengurutkan daftar array berdasarkan ukuran array, dan dalam kasus lain, berdasarkan total panjang kata dalam array? Saya pikir Anda sudah mendengar tentang pembanding dan dalam kasus seperti itu kita cukup meneruskan objek pembanding ke metode pengurutan kita, di mana kita menjelaskan aturan yang ingin kita gunakan untuk mengurutkan. Karena metode standar
sort()
diterapkan dengan prinsip yang sama seperti ,
mySuperSort()
dalam contoh saya akan menggunakan metode standar
sort()
.
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};
List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);
Comparator<String[]> sortByLength = new Comparator<String[]>() {
@Override
public int compare(String[] o1, String[] o2) {
return o1.length - o2.length;
}
};
Comparator<String[]> sortByWordsLength = new Comparator<String[]>() {
@Override
public int compare(String[] o1, String[] o2) {
int length1 = 0;
int length2 = 0;
for (String s : o1) {
length1 += s.length();
}
for (String s : o2) {
length2 += s.length();
}
return length1 - length2;
}
};
arrays.sort(sortByLength);
Hasil:
- ibu mencuci bingkai itu
- perdamaian Buruh mungkin
- Saya sangat menyukai bahasa Jawa
Di sini array diurutkan berdasarkan jumlah kata di setiap array. Array dengan kata yang lebih sedikit dianggap “lebih kecil”. Itu sebabnya hal itu terjadi di awal. Kata yang memiliki lebih banyak kata dianggap “lebih banyak” dan berakhir di akhir. Jika
sort()
kita meneruskan komparator lain ke metode tersebut
(sortByWordsLength)
, maka
hasilnya akan berbeda:
- perdamaian Buruh mungkin
- ibu mencuci bingkai itu
- Saya sangat menyukai bahasa Jawa
Sekarang array diurutkan berdasarkan jumlah total huruf dalam kata-kata dari array tersebut. Dalam kasus pertama ada 10 huruf, yang kedua 12, dan yang ketiga 15. Jika kita hanya menggunakan satu pembanding, maka kita tidak dapat membuat variabel terpisah untuknya, tetapi cukup membuat objek kelas anonim tepat di waktu memanggil metode tersebut
sort()
. Seperti itu:
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};
List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);
arrays.sort(new Comparator<String[]>() {
@Override
public int compare(String[] o1, String[] o2) {
return o1.length - o2.length;
}
});
Hasilnya akan sama seperti pada kasus pertama. Tugas 1 . Tulis ulang contoh ini sehingga mengurutkan array bukan dalam urutan jumlah kata dalam array, tetapi dalam urutan menurun. Kita sudah mengetahui semua ini. Kita tahu cara meneruskan objek ke metode, kita dapat meneruskan objek ini atau itu ke suatu metode tergantung pada apa yang kita perlukan saat ini, dan di dalam metode tempat kita meneruskan objek tersebut, metode yang kita tulis implementasinya akan dipanggil . Timbul pertanyaan: apa hubungannya ekspresi lambda dengan itu?
Mengingat lambda adalah objek yang berisi tepat satu metode. Ini seperti objek metode. Sebuah metode yang dibungkus dalam suatu objek. Mereka hanya memiliki sintaks yang sedikit tidak biasa (tetapi akan dibahas lebih lanjut nanti). Mari kita lihat lagi entri ini
arrays.sort(new Comparator<String[]>() {
@Override
public int compare(String[] o1, String[] o2) {
return o1.length - o2.length;
}
});
Di sini kita mengambil daftar kita
arrays
dan memanggil metodenya
sort()
, di mana kita meneruskan objek pembanding dengan satu metode
compare()
(tidak masalah bagi kita apa namanya, karena itu satu-satunya di objek ini, kita tidak akan melewatkannya). Metode ini mengambil dua parameter, yang selanjutnya kita kerjakan. Jika Anda bekerja di
IntelliJ IDEA , Anda mungkin pernah melihat bagaimana ia menawarkan kode ini untuk mempersingkat secara signifikan:
arrays.sort((o1, o2) -> o1.length - o2.length);
Begitulah enam baris berubah menjadi satu baris pendek. 6 baris ditulis ulang menjadi satu baris pendek. Sesuatu telah hilang, tetapi saya jamin tidak ada hal penting yang hilang, dan kode ini akan berfungsi persis sama dengan kelas anonim.
Tugas 2 . Cari tahu cara menulis ulang solusi untuk masalah 1 menggunakan lambda (sebagai upaya terakhir, minta
IntelliJ IDEA untuk mengubah kelas anonim Anda menjadi lambda).
Mari kita bicara tentang antarmuka
Pada dasarnya, antarmuka hanyalah daftar metode abstrak. Ketika kita membuat sebuah kelas dan mengatakan bahwa kelas tersebut akan mengimplementasikan semacam antarmuka, kita harus menulis di kelas kita sebuah implementasi dari metode yang tercantum dalam antarmuka (atau, sebagai upaya terakhir, tidak menulisnya, tetapi membuat kelas tersebut abstrak ). Ada antarmuka dengan banyak metode berbeda (misalnya
List
), ada antarmuka dengan hanya satu metode (misalnya, Comparator atau Runnable yang sama). Ada antarmuka tanpa metode tunggal sama sekali (disebut antarmuka penanda, misalnya Serializable). Antarmuka yang hanya memiliki satu metode disebut juga
antarmuka fungsional . Di Java 8 mereka bahkan ditandai dengan anotasi
@FunctionalInterface khusus . Ini adalah antarmuka dengan satu metode yang cocok untuk digunakan oleh ekspresi lambda. Seperti yang saya katakan di atas, ekspresi lambda adalah metode yang dibungkus dalam suatu objek. Dan ketika kita meneruskan objek seperti itu ke suatu tempat, sebenarnya kita meneruskan satu metode ini. Ternyata bagi kita tidak masalah apa sebutan metode ini. Yang penting bagi kami hanyalah parameter yang digunakan metode ini, dan, pada kenyataannya, kode metode itu sendiri. Ekspresi lambda pada dasarnya adalah. implementasi antarmuka fungsional. Jika kita melihat antarmuka dengan satu metode, itu berarti kita dapat menulis ulang kelas anonim tersebut menggunakan lambda. Jika antarmuka memiliki lebih/kurang dari satu metode, maka ekspresi lambda tidak cocok untuk kita, dan kita akan menggunakan kelas anonim, atau bahkan kelas biasa. Saatnya menggali lebih dalam lambda. :)
Sintaksis
Sintaks umumnya kira-kira seperti ini:
(параметры) -> {тело метода}
Artinya, tanda kurung, di dalamnya terdapat parameter metode, sebuah "panah" (ini adalah dua karakter berturut-turut: minus dan lebih besar), setelah itu isi metode berada dalam kurung kurawal, seperti biasa. Parameternya sesuai dengan yang ditentukan di antarmuka saat menjelaskan metode. Jika jenis variabel dapat didefinisikan dengan jelas oleh kompiler (dalam kasus kita, diketahui dengan pasti bahwa kita bekerja dengan array string, karena diketik
List
secara tepat oleh array string), maka jenis variabel
String[]
tidak perlu ditulis.
Jika Anda tidak yakin, tentukan jenisnya, dan IDEA akan menyorotnya dengan warna abu-abu jika tidak diperlukan. |
Anda dapat membaca lebih lanjut di
tutorial Oracle , misalnya. Ini disebut
"pengetikan target" . Anda dapat memberi nama apa pun pada variabel, tidak harus nama yang ditentukan di antarmuka. Jika tidak ada parameter, cukup tanda kurung saja. Jika hanya ada satu parameter, nama variabelnya saja tanpa tanda kurung. Kami telah memilah parameternya, sekarang tentang isi ekspresi lambda itu sendiri. Di dalam kurung kurawal, tulis kode seperti metode biasa. Jika seluruh kode Anda hanya terdiri dari satu baris, Anda tidak perlu menulis kurung kurawal sama sekali (seperti halnya if dan loop). Jika lambda Anda mengembalikan sesuatu, tetapi tubuhnya terdiri dari satu baris, maka
return
tidak perlu menulis sama sekali. Tetapi jika Anda memiliki kurung kurawal, seperti pada metode biasa, Anda perlu menulis
return
.
Contoh
Contoh 1.
() -> {}
Pilihan paling sederhana. Dan yang paling tidak berarti :) Karena tidak menghasilkan apa-apa.
Contoh 2.
() -> ""
Juga pilihan yang menarik. Ia tidak menerima apa pun dan mengembalikan string kosong (
return
dihilangkan karena tidak perlu). Sama, tetapi dengan
return
:
() -> {
return "";
}
Contoh 3. Halo dunia menggunakan lambda
() -> System.out.println("Hello world!")
Tidak menerima apa pun, tidak mengembalikan apa pun (kita tidak dapat meletakkan
return
sebelum panggilan
System.out.println()
, karena tipe pengembalian dalam metode ini
println() — void)
, cukup menampilkan tulisan di layar. Ideal untuk mengimplementasikan antarmuka
Runnable
. Contoh yang sama lebih lengkap:
public class Main {
public static void main(String[] args) {
new Thread(() -> System.out.println("Hello world!")).start();
}
}
Atau seperti ini:
public class Main {
public static void main(String[] args) {
Thread t = new Thread(() -> System.out.println("Hello world!"));
t.start();
}
}
Atau kita bahkan dapat menyimpan ekspresi lambda sebagai objek bertipe
Runnable
, dan kemudian meneruskannya ke konstruktor
thread’а
:
public class Main {
public static void main(String[] args) {
Runnable runnable = () -> System.out.println("Hello world!");
Thread t = new Thread(runnable);
t.start();
}
}
Mari kita lihat lebih dekat momen menyimpan ekspresi lambda ke dalam variabel. Antarmuka
Runnable
memberi tahu kita bahwa objeknya harus memiliki metode
public void run()
. Menurut antarmuka, metode run tidak menerima apa pun sebagai parameter. Dan itu tidak mengembalikan apa pun
(void)
. Oleh karena itu, ketika menulis dengan cara ini, sebuah objek akan dibuat dengan beberapa metode yang tidak menerima atau mengembalikan apapun. Yang cukup konsisten dengan metode
run()
di antarmuka
Runnable
. Itu sebabnya kami dapat memasukkan ekspresi lambda ini ke dalam variabel seperti
Runnable
.
Contoh 4
() -> 42
Sekali lagi, ia tidak menerima apa pun, tetapi mengembalikan angka 42. Ekspresi lambda ini dapat ditempatkan dalam variabel bertipe
Callable
, karena antarmuka ini hanya mendefinisikan satu metode, yang terlihat seperti ini:
V call(),
di mana
V
adalah jenis nilai yang dikembalikan (dalam kasus kami
int
). Oleh karena itu, kita dapat menyimpan ekspresi lambda sebagai berikut:
Callable<Integer> c = () -> 42;
Contoh 5. Lambda dalam beberapa baris
() -> {
String[] helloWorld = {"Hello", "world!"};
System.out.println(helloWorld[0]);
System.out.println(helloWorld[1]);
}
Sekali lagi, ini adalah ekspresi lambda tanpa parameter dan tipe pengembaliannya
void
(karena tidak ada
return
).
Contoh 6
x -> x
Di sini kita mengambil sesuatu ke dalam variabel
х
dan mengembalikannya. Perlu diketahui bahwa jika hanya satu parameter yang diterima, maka tanda kurung disekitarnya tidak perlu ditulis. Sama, tetapi dengan tanda kurung:
(x) -> x
Dan inilah opsi dengan yang eksplisit
return
:
x -> {
return x;
}
Atau seperti ini, dengan tanda kurung dan
return
:
(x) -> {
return x;
}
Atau dengan indikasi eksplisit jenisnya (dan, karenanya, dengan tanda kurung):
(int x) -> x
Contoh 7
x -> ++x
Kami menerimanya
х
dan mengembalikannya, tetapi untuk
1
lebih. Anda juga dapat menulis ulang seperti ini:
x -> x + 1
Dalam kedua kasus tersebut, kami tidak menunjukkan tanda kurung di sekitar parameter, isi metode, dan kata
return
, karena hal ini tidak diperlukan. Varian dengan tanda kurung dan return dijelaskan pada Contoh 6.
Contoh 8
(x, y) -> x % y
Kami menerima sebagian
х
dan
у
mengembalikan sisa pembagian
x
sebesar
y
. Tanda kurung di sekitar parameter sudah diperlukan di sini. Ini opsional hanya jika hanya ada satu parameter. Seperti ini dengan indikasi jenis yang jelas:
(double x, int y) -> x % y
Contoh 9
(Cat cat, String name, int age) -> {
cat.setName(name);
cat.setAge(age);
}
Kami menerima objek Cat, string dengan nama dan umur bilangan bulat. Dalam metodenya sendiri, kami menetapkan nama dan usia yang diteruskan ke Kucing. Karena variabel
cat
kita adalah tipe referensi, objek Cat di luar ekspresi lambda akan berubah (ia akan menerima nama dan umur yang dimasukkan di dalamnya). Versi yang sedikit lebih rumit yang menggunakan lambda serupa:
public class Main {
public static void main(String[] args) {
Cat myCat = new Cat();
System.out.println(myCat);
Settable<Cat> s = (obj, name, age) -> {
obj.setName(name);
obj.setAge(age);
};
changeEntity(myCat, s);
System.out.println(myCat);
}
private static <T extends WithNameAndAge> void changeEntity(T entity, Settable<T> s) {
s.set(entity, "Murzik", 3);
}
}
interface WithNameAndAge {
void setName(String name);
void setAge(int age);
}
interface Settable<C extends WithNameAndAge> {
void set(C entity, String name, int age);
}
class Cat implements WithNameAndAge {
private String name;
private int age;
@Override
public void setName(String name) {
this.name = name;
}
@Override
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
Hasil: Cat{name='null', age=0} Cat{name='Murzik', age=3} Seperti yang Anda lihat, pada awalnya objek Cat memiliki satu status, tetapi setelah menggunakan ekspresi lambda, status tersebut berubah . Ekspresi Lambda berfungsi baik dengan obat generik. Dan jika kita perlu membuat sebuah kelas
Dog
, misalnya, yang juga akan mengimplementasikan
WithNameAndAge
, maka dalam metode tersebut
main()
kita dapat melakukan operasi yang sama dengan Dog, tanpa mengubah ekspresi lambda itu sendiri sama sekali.
Tugas 3 . Tulis antarmuka fungsional dengan metode yang mengambil angka dan mengembalikan nilai Boolean. Tuliskan implementasi antarmuka tersebut dalam bentuk ekspresi lambda yang kembali
true
jika bilangan yang diteruskan habis dibagi 13 tanpa sisa.Tugas
4 . Tulis antarmuka fungsional dengan metode yang mengambil dua string dan mengembalikan string yang sama. Tulis implementasi antarmuka tersebut dalam bentuk lambda yang mengembalikan string terpanjang.
Tugas 5 . Tulis antarmuka fungsional dengan metode yang menerima tiga bilangan pecahan:
a
,
b
,
c
dan mengembalikan bilangan pecahan yang sama. Tulis implementasi antarmuka tersebut dalam bentuk ekspresi lambda yang mengembalikan diskriminan. Siapa yang lupa,
D = b^2 - 4ac .
Tugas 6 . Menggunakan antarmuka fungsional dari tugas 5, tulis ekspresi lambda yang mengembalikan hasil operasi
a * b^c
.
Populer tentang ekspresi lambda di Jawa. Dengan contoh dan tugas. Bagian 2.
GO TO FULL VERSION