CodeGym /Java Blog /Acak /Penjelasan ekspresi lambda di Jawa. Dengan contoh dan tug...
John Squirrels
Level 41
San Francisco

Penjelasan ekspresi lambda di Jawa. Dengan contoh dan tugas. Bagian 1

Dipublikasikan di grup Acak
Untuk siapa 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.
Penjelasan ekspresi lambda di Jawa.  Dengan contoh dan tugas.  Bagian 1 - 1Jika Anda tidak cocok dengan salah satu kategori ini, Anda mungkin menganggap artikel ini membosankan, cacat, atau secara umum tidak cocok untuk Anda. Dalam hal ini, jangan ragu untuk beralih ke hal lain atau, jika Anda berpengalaman dalam subjek ini, silakan berikan saran di komentar tentang bagaimana saya dapat meningkatkan atau menambah artikel. Materi tersebut tidak mengklaim memiliki nilai akademis, apalagi kebaruan. Justru sebaliknya: Saya akan mencoba mendeskripsikan hal-hal yang kompleks (bagi sebagian orang) sesederhana mungkin. Permintaan untuk menjelaskan Stream API mengilhami saya untuk menulis ini. Saya memikirkannya dan memutuskan bahwa beberapa contoh aliran saya tidak akan dapat dipahami tanpa pemahaman tentang ekspresi lambda. Jadi kita akan mulai dengan ekspresi lambda. Apa yang perlu Anda ketahui untuk memahami artikel ini?
  1. Anda harus memahami pemrograman berorientasi objek (OOP), yaitu:

    • kelas, objek, dan perbedaan di antara mereka;
    • antarmuka, perbedaannya dari kelas, dan hubungan antara antarmuka dan kelas;
    • metode, cara memanggilnya, metode abstrak (yaitu metode tanpa implementasi), parameter metode, argumen metode, dan cara meneruskannya;
    • pengubah akses, metode/variabel statis, metode/variabel final;
    • pewarisan kelas dan antarmuka, pewarisan berganda dari antarmuka.
  2. Pengetahuan tentang Inti Java: tipe generik (generik), koleksi (daftar), utas.
Baiklah, mari kita mulai.

Sedikit sejarah

Ekspresi Lambda datang ke Jawa dari pemrograman fungsional, dan ke sana dari matematika. Di Amerika Serikat pada pertengahan abad ke-20, Gereja Alonzo, yang sangat menyukai matematika dan segala macam abstraksi, bekerja di Universitas Princeton. Itu adalah Gereja Alonzo yang menemukan kalkulus lambda, yang awalnya merupakan sekumpulan ide abstrak yang sama sekali tidak terkait dengan pemrograman. Matematikawan seperti Alan Turing dan John von Neumann bekerja di Universitas Princeton pada waktu yang sama. Semuanya menyatu: Gereja muncul dengan kalkulus lambda. Turing mengembangkan mesin komputasi abstraknya, yang sekarang dikenal sebagai "mesin Turing". Dan von Neumann mengusulkan arsitektur komputer yang telah menjadi dasar komputer modern (sekarang disebut "arsitektur von Neumann"). Saat itu, Gereja Alonzo Ide-idenya tidak begitu terkenal seperti karya rekan-rekannya (kecuali bidang matematika murni). Namun, beberapa saat kemudian John McCarthy (juga lulusan Universitas Princeton dan, pada saat cerita kami, seorang karyawan Institut Teknologi Massachusetts) menjadi tertarik dengan ide-ide Church. Pada tahun 1958, ia menciptakan bahasa pemrograman fungsional pertama, LISP, berdasarkan ide tersebut. Dan 58 tahun kemudian, ide pemrograman fungsional bocor ke Java 8. Bahkan belum 70 tahun berlalu... Sejujurnya, ini bukanlah waktu terlama yang dibutuhkan untuk menerapkan ide matematika dalam praktik. seorang karyawan Institut Teknologi Massachusetts) menjadi tertarik dengan ide-ide Church. Pada tahun 1958, ia menciptakan bahasa pemrograman fungsional pertama, LISP, berdasarkan ide tersebut. Dan 58 tahun kemudian, ide pemrograman fungsional bocor ke Java 8. Bahkan belum 70 tahun berlalu... Sejujurnya, ini bukanlah waktu terlama yang dibutuhkan untuk menerapkan ide matematika dalam praktik. seorang karyawan Institut Teknologi Massachusetts) menjadi tertarik dengan ide-ide Church. Pada tahun 1958, ia menciptakan bahasa pemrograman fungsional pertama, LISP, berdasarkan ide tersebut. Dan 58 tahun kemudian, ide pemrograman fungsional bocor ke Java 8. Bahkan belum 70 tahun berlalu... Sejujurnya, ini bukanlah waktu terlama yang dibutuhkan untuk menerapkan ide matematika dalam praktik.

Inti masalahnya

Ekspresi lambda adalah sejenis fungsi. Anda dapat menganggapnya sebagai metode Java biasa tetapi dengan kemampuan khusus untuk diteruskan ke metode lain sebagai argumen. Itu benar. Menjadi mungkin untuk meneruskan tidak hanya angka, string, dan kucing ke metode, tetapi juga metode lain! Kapan kita membutuhkan ini? Akan sangat membantu, misalnya, jika kita ingin meneruskan beberapa metode callback. Yaitu, jika kita membutuhkan metode yang kita panggil untuk memiliki kemampuan memanggil beberapa metode lain yang kita berikan padanya. Dengan kata lain, jadi kami memiliki kemampuan untuk meneruskan satu panggilan balik dalam keadaan tertentu dan panggilan balik yang berbeda dalam keadaan lain. Dan agar metode kami yang menerima callback kami memanggil mereka. Pengurutan adalah contoh sederhana. Misalkan kita sedang menulis beberapa algoritma penyortiran pintar yang terlihat seperti ini:

public void mySuperSort() { 
    // We do something here 
    if(compare(obj1, obj2) > 0) 
    // And then we do something here 
}
Dalam ifpernyataan, kami memanggil compare()metode, meneruskan dua objek untuk dibandingkan, dan kami ingin mengetahui objek mana yang "lebih besar". Kami menganggap yang "lebih besar" datang sebelum yang "lebih kecil". Saya memberi tanda kutip "lebih besar", karena kami sedang menulis metode universal yang akan mengetahui cara mengurutkan tidak hanya dalam urutan menaik, tetapi juga dalam urutan menurun (dalam hal ini, objek "lebih besar" sebenarnya akan menjadi objek "lebih kecil" , dan sebaliknya). Untuk menyetel algoritme khusus untuk pengurutan kami, kami memerlukan beberapa mekanisme untuk meneruskannya ke mySuperSort()metode kami. Dengan begitu kita akan dapat "mengontrol" metode kita saat dipanggil. Tentu saja, kita dapat menulis dua metode terpisah — mySuperSortAscend()danmySuperSortDescend()— untuk menyortir dalam urutan menaik dan menurun. Atau kita dapat meneruskan beberapa argumen ke metode (misalnya, variabel boolean; jika benar, maka urutkan dalam urutan naik, dan jika salah, maka dalam urutan menurun). Tapi bagaimana jika kita ingin mengurutkan sesuatu yang rumit seperti daftar array string? Bagaimana mySuperSort()metode kami mengetahui cara mengurutkan array string ini? Berdasarkan ukuran? Dengan panjang kumulatif semua kata? Mungkin menurut abjad berdasarkan string pertama dalam array? Dan bagaimana jika kita perlu mengurutkan daftar array berdasarkan ukuran array dalam beberapa kasus, dan dengan panjang kumulatif semua kata di setiap array dalam kasus lain? Saya berharap Anda telah mendengar tentang pembanding dan bahwa dalam hal ini kami hanya akan meneruskan ke metode penyortiran kami objek pembanding yang menjelaskan algoritme penyortiran yang diinginkan. Karena standarsort()metode diimplementasikan berdasarkan prinsip yang sama seperti mySuperSort(), saya akan gunakan sort()dalam contoh saya.

String[] array1 = {"Dota", "GTA5", "Halo"}; 
String[] array2 = {"I", "really", "love", "Java"}; 
String[] array3 = {"if", "then", "else"}; 

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[]> sortByCumulativeWordLength = 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:

  1. Dota GTA5 Halo
  2. if then else
  3. I really love Java
Di sini array diurutkan berdasarkan jumlah kata di setiap array. Array dengan lebih sedikit kata dianggap "lebih rendah". Itu sebabnya itu datang lebih dulu. Array dengan lebih banyak kata dianggap "lebih besar" dan ditempatkan di bagian akhir. Jika kita meneruskan pembanding yang berbeda ke sort()metode, seperti sortByCumulativeWordLength, maka kita akan mendapatkan hasil yang berbeda:

  1. if then else
  2. Dota GTA5 Halo
  3. I really love Java
Sekarang are array diurutkan berdasarkan jumlah huruf dalam kata-kata dari array. Di larik pertama, ada 10 huruf, di larik kedua — 12, dan di larik ketiga — 15. Jika kita hanya memiliki satu pembanding, maka kita tidak perlu mendeklarasikan variabel terpisah untuknya. Sebagai gantinya, kita cukup membuat kelas anonim tepat pada saat pemanggilan metode sort(). Sesuatu seperti ini:

String[] array1 = {"Dota", "GTA5", "Halo"}; 
String[] array2 = {"I", "really", "love", "Java"}; 
String[] array3 = {"if", "then", "else"}; 

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; 
    } 
}); 
Kami akan mendapatkan hasil yang sama seperti pada kasus pertama. Tugas 1. Tulis ulang contoh ini sehingga mengurutkan array bukan dalam urutan naik dari jumlah kata di setiap larik, tetapi dalam urutan menurun. Kami sudah tahu semua ini. Kami tahu cara meneruskan objek ke metode. Bergantung pada apa yang kita butuhkan saat ini, kita dapat meneruskan objek yang berbeda ke suatu metode, yang kemudian akan memanggil metode yang telah kita implementasikan. Ini menimbulkan pertanyaan: mengapa kita membutuhkan ekspresi lambda di sini?  Karena ekspresi lambda adalah objek yang memiliki tepat satu metode. Seperti "objek metode". Metode yang dikemas dalam sebuah objek. Itu hanya memiliki sintaks yang sedikit asing (tetapi lebih lanjut tentang itu nanti). Mari kita lihat lagi kode ini:

arrays.sort(new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
});
Di sini kita mengambil daftar array kita dan memanggil metodenya sort(), di mana kita meneruskan objek pembanding dengan satu compare()metode (namanya tidak masalah bagi kita — lagipula, ini adalah satu-satunya metode objek ini, jadi kita tidak bisa salah). Metode ini memiliki dua parameter yang akan kita kerjakan. Jika Anda bekerja di IntelliJ IDEA, Anda mungkin melihatnya menawarkan untuk memadatkan kode secara signifikan sebagai berikut:

arrays.sort((o1, o2) -> o1.length - o2.length);
Ini mengurangi enam baris menjadi satu baris pendek. 6 baris ditulis ulang sebagai satu baris pendek. Sesuatu menghilang, tapi saya jamin itu bukan sesuatu yang penting. Kode ini akan bekerja dengan cara yang persis sama dengan kelas anonim. Tugas 2. Coba tebak dengan menulis ulang solusi untuk Tugas 1 menggunakan ekspresi lambda (setidaknya, mintalah IntelliJ IDEA untuk mengonversi kelas anonim Anda menjadi ekspresi lambda).

Mari kita bicara tentang antarmuka

Pada prinsipnya, antarmuka hanyalah daftar metode abstrak. Saat kita membuat kelas yang mengimplementasikan beberapa antarmuka, kelas kita harus mengimplementasikan metode yang termasuk dalam antarmuka (atau kita harus membuat abstrak kelas). Ada antarmuka dengan banyak metode berbeda (misalnya,  List), dan ada antarmuka dengan hanya satu metode (misalnya, Comparatoratau Runnable). Ada antarmuka yang tidak memiliki metode tunggal (disebut antarmuka penanda seperti Serializable). Antarmuka yang hanya memiliki satu metode disebut juga antarmuka fungsional . Di Java 8, mereka bahkan ditandai dengan anotasi khusus:@FunctionalInterface. Antarmuka metode tunggal inilah yang cocok sebagai tipe target untuk ekspresi lambda. Seperti yang saya katakan di atas, ekspresi lambda adalah metode yang dibungkus dengan objek. Dan saat kita melewatkan objek seperti itu, pada dasarnya kita melewatkan metode tunggal ini. Ternyata kita tidak peduli apa namanya metode itu. Satu-satunya hal yang penting bagi kami adalah parameter metode dan, tentu saja, isi metode. Intinya, ekspresi lambda adalah implementasi dari antarmuka fungsional. Di mana pun kita melihat antarmuka dengan satu metode, kelas anonim dapat ditulis ulang sebagai lambda. Jika antarmuka memiliki lebih atau kurang dari satu metode, maka ekspresi lambda tidak akan berfungsi dan sebagai gantinya kita akan menggunakan kelas anonim atau bahkan turunan dari kelas biasa. Sekarang saatnya untuk sedikit menggali lambda. :)

Sintaksis

Sintaks umumnya adalah seperti ini:

(parameters) -> {method body}
Yaitu, tanda kurung yang mengelilingi parameter metode, "panah" (dibentuk oleh tanda hubung dan tanda lebih besar dari), dan kemudian badan metode dalam kurung, seperti biasa. Parameter sesuai dengan yang ditentukan dalam metode antarmuka. Jika tipe variabel dapat ditentukan secara jelas oleh kompiler (dalam kasus kita, ia mengetahui bahwa kita bekerja dengan array string, karena Listobjek kita diketik menggunakan String[]), maka Anda tidak perlu menunjukkan tipenya.
Jika ambigu, tunjukkan jenisnya. IDEA akan mewarnainya abu-abu jika tidak diperlukan.
Anda dapat membaca lebih lanjut di tutorial Oracle ini dan di tempat lain. Ini disebut " pengetikan target ". Anda dapat memberi nama variabel apa pun yang Anda inginkan — Anda tidak harus menggunakan nama yang sama yang ditentukan di antarmuka. Jika tidak ada parameter, cukup tunjukkan tanda kurung kosong. Jika hanya ada satu parameter, cukup tunjukkan nama variabel tanpa tanda kurung. Sekarang setelah kita memahami parameternya, saatnya untuk membahas badan ekspresi lambda. Di dalam kurung kurawal, Anda menulis kode seperti yang Anda lakukan untuk metode biasa. Jika kode Anda terdiri dari satu baris, maka Anda dapat menghilangkan kurung kurawal seluruhnya (mirip dengan pernyataan if dan for-loop). Jika lambda satu baris Anda menampilkan sesuatu, Anda tidak perlu menyertakan areturnpenyataan. Tetapi jika Anda menggunakan kurung kurawal, maka Anda harus menyertakan returnpernyataan secara eksplisit, seperti yang Anda lakukan dalam metode biasa.

Contoh

Contoh 1.

() -> {}
Contoh paling sederhana. Dan yang paling tidak berguna :), karena tidak melakukan apa-apa. Contoh 2.

() -> ""
Contoh lain yang menarik. Tidak diperlukan apa pun dan mengembalikan string kosong ( returndihilangkan, karena tidak perlu). Ini hal yang sama, tetapi dengan return:

() -> { 
    return ""; 
}
Contoh 3. "Halo, Dunia!" menggunakan lambda

() -> System.out.println("Hello, World!")
Tidak memerlukan apa pun dan tidak mengembalikan apa pun (kita tidak dapat meletakkan returnsebelum panggilan ke System.out.println(), karena println()jenis pengembalian metode adalah void). Ini hanya menampilkan salam. Ini sangat ideal untuk implementasi antarmuka Runnable. Contoh berikut ini 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 Runnableobjek dan meneruskannya ke Threadkonstruktor:

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 saat ekspresi lambda disimpan ke variabel. Antarmuka Runnablememberi tahu kita bahwa objeknya harus memiliki public void run()metode. Menurut antarmuka, runmetode ini tidak memerlukan parameter. Dan tidak mengembalikan apa-apa, yaitu tipe pengembaliannya adalah void. Karenanya, kode ini akan membuat objek dengan metode yang tidak mengambil atau mengembalikan apa pun. Ini sangat cocok dengan metode Runnableantarmuka run(). Itu sebabnya kami dapat menempatkan ekspresi lambda ini dalam sebuah Runnablevariabel.  Contoh 4.

() -> 42
Sekali lagi, tidak diperlukan apa-apa, tetapi mengembalikan angka 42. Ekspresi lambda seperti itu dapat dimasukkan ke dalam variabel Callable, karena antarmuka ini hanya memiliki satu metode yang terlihat seperti ini:

V call(),
di mana  V tipe pengembalian (dalam kasus kami,  int). Oleh karena itu, kita dapat menyimpan ekspresi lambda sebagai berikut:

Callable<Integer> c = () -> 42;
Contoh 5. Ekspresi lambda yang melibatkan 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 voidtipe pengembalian (karena tidak ada returnpernyataan).  Contoh 6

x -> x
Di sini kita mengambil xvariabel dan mengembalikannya. Perlu diketahui bahwa jika hanya ada satu parameter, maka Anda dapat menghilangkan tanda kurung di sekitarnya. Ini hal yang sama, tetapi dengan tanda kurung:

(x) -> x
Dan inilah contoh dengan pernyataan pengembalian eksplisit:

x -> { 
    return x;
}
Atau seperti ini dengan tanda kurung dan pernyataan pengembalian:

(x) -> { 
    return x;
}
Atau dengan indikasi tipe yang eksplisit (dan dengan demikian dengan tanda kurung):

(int x) -> x
Contoh 7

x -> ++x
Kami mengambil xdan mengembalikannya, tetapi hanya setelah menambahkan 1. Anda dapat menulis ulang lambda itu seperti ini:

x -> x + 1
Dalam kedua kasus, kami menghilangkan tanda kurung di sekitar parameter dan badan metode, bersama dengan pernyataan return, karena bersifat opsional. Versi dengan tanda kurung dan pernyataan kembali diberikan dalam Contoh 6. Contoh 8

(x, y) -> x % y
Kami mengambil xdan ydan mengembalikan sisa pembagian xoleh y. Tanda kurung di sekitar parameter diperlukan di sini. Mereka bersifat opsional hanya jika hanya ada satu parameter. Ini dia dengan indikasi eksplisit dari jenisnya:

(double x, int y) -> x % y
Contoh 9

(Cat cat, String name, int age) -> {
    cat.setName(name); 
    cat.setAge(age); 
}
Kami mengambil Catobjek, Stringnama, dan usia int. Dalam metode itu sendiri, kami menggunakan nama dan usia yang diteruskan untuk menetapkan variabel pada kucing. Karena catobjek kita adalah tipe referensi, objek tersebut akan diubah di luar ekspresi lambda (akan mendapatkan nama dan usia yang diteruskan). Ini adalah versi yang sedikit lebih rumit yang menggunakan lambda serupa:

public class Main { 

    public static void main(String[] args) { 
        // Create a cat and display it to confirm that it is "empty" 
        Cat myCat = new Cat(); 
        System.out.println(myCat);
 
        // Create a lambda 
        Settable<Cat> s = (obj, name, age) -> { 
            obj.setName(name); 
            obj.setAge(age); 

        }; 

        // Call a method to which we pass the cat and lambda 
        changeEntity(myCat, s); 

        // Display the cat on the screen and see that its state has changed (it has a name and age) 
        System.out.println(myCat); 

    } 

    private static <T extends HasNameAndAge>  void changeEntity(T entity, Settable<T> s) { 
        s.set(entity, "Smokey", 3); 
    }
}

interface HasNameAndAge { 
    void setName(String name); 
    void setAge(int age); 
}

interface Settable<C extends HasNameAndAge> { 
    void set(C entity, String name, int age); 
}

class Cat implements HasNameAndAge { 
    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='Smokey', age=3}
Seperti yang Anda lihat, Catobjek memiliki satu keadaan, dan kemudian keadaan berubah setelah kita menggunakan ekspresi lambda. Ekspresi Lambda berpadu sempurna dengan obat generik. Dan jika kita perlu membuat Dogkelas yang juga mengimplementasikan HasNameAndAge, maka kita dapat melakukan operasi yang sama di dalam Dogmetode main() tanpa mengubah ekspresi lambda. Tugas 3. Tulis antarmuka fungsional dengan metode yang mengambil angka dan mengembalikan nilai boolean. Tulis implementasi antarmuka seperti ekspresi lambda yang mengembalikan true jika angka yang diteruskan habis dibagi 13. Tugas 4.Tulis antarmuka fungsional dengan metode yang mengambil dua string dan juga mengembalikan string. Tulis implementasi antarmuka seperti ekspresi lambda yang mengembalikan string yang lebih panjang. Tugas 5. Tulis antarmuka fungsional dengan metode yang menggunakan tiga angka titik-mengambang: a, b, dan c dan juga mengembalikan angka titik-mengambang. Tulis implementasi antarmuka seperti ekspresi lambda yang mengembalikan diskriminan. Jika Anda lupa, itu D = b^2 — 4ac. Tugas 6. Menggunakan antarmuka fungsional dari Tugas 5, tulis ekspresi lambda yang mengembalikan hasil a * b^c. Penjelasan ekspresi lambda di Jawa. Dengan contoh dan tugas. Bagian 2
Komentar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION