CodeGym/Blog Java/rawak/Penjelasan tentang ungkapan lambda di Jawa. Dengan contoh...
John Squirrels
Tahap
San Francisco

Penjelasan tentang ungkapan lambda di Jawa. Dengan contoh dan tugasan. Bahagian 1

Diterbitkan dalam kumpulan
Untuk siapa artikel ini?
  • Ia adalah untuk orang yang berpendapat mereka sudah mengetahui Java Core dengan baik, tetapi tidak tahu tentang ungkapan lambda di Jawa. Atau mungkin mereka pernah mendengar sesuatu tentang ungkapan lambda, tetapi butirannya kurang
  • Ia adalah untuk orang yang mempunyai pemahaman tertentu tentang ungkapan lambda, tetapi masih takut dengannya dan tidak biasa menggunakannya.
Penjelasan tentang ungkapan lambda di Jawa.  Dengan contoh dan tugasan.  Bahagian 1 - 1Jika anda tidak sesuai dengan salah satu daripada kategori ini, anda mungkin mendapati artikel ini membosankan, cacat, atau biasanya bukan secawan teh anda. Dalam kes ini, jangan ragu untuk beralih kepada perkara lain atau, jika anda mahir dalam subjek, sila berikan cadangan dalam ulasan tentang cara saya boleh menambah baik atau menambah artikel. Bahan tersebut tidak mendakwa mempunyai sebarang nilai akademik, apatah lagi kebaharuan. Sebaliknya: Saya akan cuba menerangkan perkara yang rumit (bagi sesetengah orang) semudah mungkin. Permintaan untuk menerangkan API Strim telah memberi inspirasi kepada saya untuk menulis ini. Saya memikirkannya dan memutuskan bahawa beberapa contoh strim saya tidak dapat difahami tanpa pemahaman tentang ungkapan lambda. Jadi kita akan mulakan dengan ungkapan lambda. Apa yang anda perlu tahu untuk memahami artikel ini?
  1. Anda harus memahami pengaturcaraan berorientasikan objek (OOP), iaitu:

    • kelas, objek, dan perbezaan di antara mereka;
    • antara muka, bagaimana ia berbeza daripada kelas, dan hubungan antara antara muka dan kelas;
    • kaedah, cara memanggilnya, kaedah abstrak (iaitu kaedah tanpa pelaksanaan), parameter kaedah, hujah kaedah dan cara menghantarnya;
    • pengubah suai capaian, kaedah/pembolehubah statik, kaedah/pembolehubah akhir;
    • pewarisan kelas dan antara muka, pewarisan berbilang antara muka.
  2. Pengetahuan Teras Java: jenis generik (generik), koleksi (senarai), benang.
Baiklah, mari kita lakukannya.

Sedikit sejarah

Ekspresi Lambda datang ke Java dari pengaturcaraan berfungsi, dan ke sana dari matematik. Di Amerika Syarikat pada pertengahan abad ke-20, Gereja Alonzo, yang sangat menyukai matematik dan semua jenis abstraksi, bekerja di Universiti Princeton. Gereja Alonzolah yang mencipta kalkulus lambda, yang pada mulanya merupakan satu set idea abstrak yang sama sekali tidak berkaitan dengan pengaturcaraan. Ahli matematik seperti Alan Turing dan John von Neumann bekerja di Universiti Princeton pada masa yang sama. Semuanya bersatu: Gereja menghasilkan kalkulus lambda. Turing membangunkan mesin pengkomputeran abstraknya, yang kini dikenali sebagai "Mesin Turing". Dan von Neumann mencadangkan seni bina komputer yang telah membentuk asas komputer moden (kini dipanggil "seni bina von Neumann"). Pada masa itu, Gereja Alonzo' Idea-ideanya tidak begitu terkenal sebagai karya rakan-rakannya (kecuali bidang matematik tulen). Walau bagaimanapun, tidak lama kemudian John McCarthy (juga lulusan Universiti Princeton dan, pada masa cerita kami, seorang pekerja Institut Teknologi Massachusetts) mula berminat dengan idea-idea Gereja. Pada tahun 1958, beliau mencipta bahasa pengaturcaraan berfungsi pertama, LISP, berdasarkan idea tersebut. Dan 58 tahun kemudian, idea-idea pengaturcaraan berfungsi bocor ke Java 8. Belum pun 70 tahun berlalu... Sejujurnya, ini bukanlah yang paling lama untuk idea matematik digunakan dalam amalan. seorang pekerja Institut Teknologi Massachusetts) mula berminat dengan idea-idea Gereja. Pada tahun 1958, beliau mencipta bahasa pengaturcaraan berfungsi pertama, LISP, berdasarkan idea tersebut. Dan 58 tahun kemudian, idea-idea pengaturcaraan berfungsi bocor ke Java 8. Belum pun 70 tahun berlalu... Sejujurnya, ini bukanlah yang paling lama untuk idea matematik digunakan dalam amalan. seorang pekerja Institut Teknologi Massachusetts) mula berminat dengan idea-idea Gereja. Pada tahun 1958, beliau mencipta bahasa pengaturcaraan berfungsi pertama, LISP, berdasarkan idea tersebut. Dan 58 tahun kemudian, idea-idea pengaturcaraan berfungsi bocor ke Java 8. Belum pun 70 tahun berlalu... Sejujurnya, ini bukanlah yang paling lama untuk idea matematik digunakan dalam amalan.

Hati perkara itu

Ungkapan lambda ialah sejenis fungsi. Anda boleh menganggapnya sebagai kaedah Java biasa tetapi dengan keupayaan tersendiri untuk dihantar kepada kaedah lain sebagai hujah. betul tu. Ia telah menjadi mungkin untuk menghantar bukan sahaja nombor, rentetan, dan kucing kepada kaedah, tetapi juga kaedah lain! Bilakah kita mungkin memerlukan ini? Ia akan membantu, sebagai contoh, jika kita ingin menghantar beberapa kaedah panggil balik. Iaitu, jika kita memerlukan kaedah yang kita panggil untuk mempunyai keupayaan untuk memanggil beberapa kaedah lain yang kita berikan kepadanya. Dalam erti kata lain, jadi kami mempunyai keupayaan untuk menghantar satu panggilan balik dalam keadaan tertentu dan panggilan balik yang berbeza pada orang lain. Dan supaya kaedah kami yang menerima panggilan balik kami memanggil mereka. Menyusun adalah contoh mudah. Katakan kita sedang menulis beberapa algoritma pengisihan pintar yang kelihatan seperti ini:
public void mySuperSort() {
    // We do something here
    if(compare(obj1, obj2) > 0)
    // And then we do something here
}
Dalam ifpenyataan itu, kami memanggil compare()kaedah, menghantar dua objek untuk dibandingkan, dan kami ingin tahu yang mana antara objek ini "lebih hebat". Kami menganggap yang "lebih besar" datang sebelum yang "lebih rendah". Saya meletakkan "lebih besar" dalam petikan, kerana kami menulis kaedah sejagat yang akan mengetahui cara mengisih bukan sahaja dalam susunan menaik, tetapi juga dalam susunan menurun (dalam kes ini, objek "lebih besar" sebenarnya akan menjadi objek "kurang" , dan begitu juga sebaliknya). Untuk menetapkan algoritma khusus untuk jenis kami, kami memerlukan beberapa mekanisme untuk menghantarnya kepada mySuperSort()kaedah kami. Dengan cara itu kami akan dapat "mengawal" kaedah kami apabila ia dipanggil. Sudah tentu, kita boleh menulis dua kaedah berasingan — mySuperSortAscend()danmySuperSortDescend()— untuk menyusun mengikut tertib menaik dan menurun. Atau kita boleh menyampaikan beberapa hujah kepada kaedah (contohnya, pembolehubah boolean; jika benar, kemudian susun dalam tertib menaik, dan jika palsu, maka dalam tertib menurun). Tetapi bagaimana jika kita ingin menyusun sesuatu yang rumit seperti senarai tatasusunan rentetan? Bagaimanakah mySuperSort()kaedah kami mengetahui cara mengisih tatasusunan rentetan ini? Mengikut saiz? Dengan panjang kumulatif semua perkataan? Mungkin mengikut abjad berdasarkan rentetan pertama dalam tatasusunan? Dan bagaimana jika kita perlu mengisih senarai tatasusunan mengikut saiz tatasusunan dalam beberapa kes, dan mengikut panjang terkumpul semua perkataan dalam setiap tatasusunan dalam kes lain? Saya menjangkakan anda telah pun mendengar tentang pembanding dan dalam kes ini kami hanya akan menyerahkan kepada kaedah pengisihan kami objek pembanding yang menerangkan algoritma pengisihan yang diingini. Kerana standardsort()kaedah dilaksanakan 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);
Keputusan:
Dota GTA5 Halo
if then else
I really love Java
Di sini tatasusunan diisih mengikut bilangan perkataan dalam setiap tatasusunan. Tatasusunan dengan perkataan yang lebih sedikit dianggap "kurang". Itulah sebabnya ia didahulukan. Tatasusunan dengan lebih banyak perkataan dianggap "lebih hebat" dan diletakkan pada penghujungnya. Jika kita lulus pembanding yang berbeza kepada sort()kaedah, seperti sortByCumulativeWordLength, maka kita akan mendapat hasil yang berbeza:
if then else
Dota GTA5 Halo
I really love Java
Kini tatasusunan adalah diisih mengikut jumlah bilangan huruf dalam perkataan tatasusunan. Dalam tatasusunan pertama, terdapat 10 huruf, dalam kedua — 12, dan dalam ketiga — 15. Jika kita hanya mempunyai satu pembanding, maka kita tidak perlu mengisytiharkan pembolehubah berasingan untuknya. Sebaliknya, kita hanya boleh membuat kelas tanpa nama tepat pada masa panggilan ke sort()kaedah tersebut. 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 mendapat hasil yang sama seperti dalam kes pertama. Tugasan 1. Tulis semula contoh ini supaya ia mengisih tatasusunan bukan dalam susunan menaik bilangan perkataan dalam setiap tatasusunan, tetapi dalam susunan menurun. Kita sudah tahu semua ini. Kami tahu cara menghantar objek ke kaedah. Bergantung pada perkara yang kita perlukan pada masa ini, kita boleh menghantar objek berbeza kepada kaedah, yang kemudiannya akan menggunakan kaedah yang kita laksanakan. Ini menimbulkan persoalan: mengapa di dunia ini kita memerlukan ungkapan lambda di sini?  Kerana ungkapan lambda ialah objek yang mempunyai satu kaedah. Seperti "objek kaedah". Kaedah yang dibungkus dalam objek. Ia hanya mempunyai sintaks yang agak asing (tetapi lebih lanjut mengenainya kemudian). Mari kita lihat lagi kod ini:
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Di sini kami mengambil senarai tatasusunan kami dan memanggil kaedahnya sort(), yang mana kami lulus objek pembanding dengan satu compare()kaedah (namanya tidak penting kepada kami - lagipun, ia adalah satu-satunya kaedah objek ini, jadi kami tidak boleh tersilap). Kaedah ini mempunyai dua parameter yang akan kami gunakan. Jika anda bekerja dalam IntelliJ IDEA, anda mungkin melihat ia menawarkan untuk memekatkan kod dengan ketara seperti berikut:
arrays.sort((o1, o2) -> o1.length - o2.length);
Ini mengurangkan enam baris kepada satu baris pendek. 6 baris ditulis semula sebagai satu yang pendek. Sesuatu telah hilang, tetapi saya jamin ia bukan sesuatu yang penting. Kod ini akan berfungsi dengan cara yang sama seperti dengan kelas tanpa nama. Tugasan 2. Buat tekaan untuk menulis semula penyelesaian kepada Tugasan 1 menggunakan ungkapan lambda (sekurang-kurangnya, minta IntelliJ IDEA untuk menukar kelas tanpa nama anda kepada ungkapan lambda).

Mari kita bercakap tentang antara muka

Pada dasarnya, antara muka hanyalah senarai kaedah abstrak. Apabila kita mencipta kelas yang melaksanakan beberapa antara muka, kelas kita mesti melaksanakan kaedah yang disertakan dalam antara muka (atau kita perlu menjadikan kelas itu abstrak). Terdapat antara muka dengan banyak kaedah yang berbeza (contohnya,  List), dan terdapat antara muka dengan hanya satu kaedah (contohnya, Comparatoratau Runnable). Terdapat antara muka yang tidak mempunyai satu kaedah (yang dipanggil antara muka penanda seperti Serializable). Antara muka yang mempunyai hanya satu kaedah juga dipanggil antara muka berfungsi . Dalam Java 8, mereka juga ditandakan dengan anotasi khas:@FunctionalInterface. Antara muka kaedah tunggal inilah yang sesuai sebagai jenis sasaran untuk ungkapan lambda. Seperti yang saya katakan di atas, ungkapan lambda ialah kaedah yang dibungkus dalam objek. Dan apabila kita melepasi objek sedemikian, kita pada dasarnya melepasi kaedah tunggal ini. Ternyata kita tidak kisah kaedah itu dipanggil apa. Satu-satunya perkara yang penting kepada kami ialah parameter kaedah dan, sudah tentu, badan kaedah. Pada dasarnya, ungkapan lambda ialah pelaksanaan antara muka berfungsi. Di mana sahaja kita melihat antara muka dengan kaedah tunggal, kelas tanpa nama boleh ditulis semula sebagai lambda. Jika antara muka mempunyai lebih atau kurang daripada satu kaedah, maka ungkapan lambda tidak akan berfungsi dan sebaliknya kami akan menggunakan kelas tanpa nama atau malah contoh kelas biasa. Kini tiba masanya untuk menggali sedikit lambda. :)

Sintaks

Sintaks umum adalah seperti ini:
(parameters) -> {method body}
Iaitu, kurungan mengelilingi parameter kaedah, "anak panah" (dibentuk oleh tanda sempang dan lebih besar daripada tanda), dan kemudian badan kaedah dalam pendakap, seperti biasa. Parameter sepadan dengan yang dinyatakan dalam kaedah antara muka. Jika jenis pembolehubah boleh ditentukan dengan jelas oleh pengkompil (dalam kes kami, ia tahu bahawa kami sedang bekerja dengan tatasusunan rentetan, kerana Listobjek kami ditaip menggunakan String[]), maka anda tidak perlu menunjukkan jenisnya.
Jika mereka samar-samar, maka nyatakan jenisnya. IDEA akan mewarnakannya dengan kelabu jika tidak diperlukan.
Anda boleh membaca lebih lanjut dalam tutorial Oracle ini dan di tempat lain. Ini dipanggil " menaip sasaran ". Anda boleh menamakan pembolehubah apa sahaja yang anda mahu — anda tidak perlu menggunakan nama yang sama yang dinyatakan dalam antara muka. Jika tiada parameter, maka nyatakan kurungan kosong. Jika terdapat hanya satu parameter, nyatakan nama pembolehubah tanpa sebarang kurungan. Sekarang setelah kita memahami parameter, tiba masanya untuk membincangkan badan ungkapan lambda. Di dalam pendakap kerinting, anda menulis kod seperti yang anda lakukan untuk kaedah biasa. Jika kod anda terdiri daripada satu baris, maka anda boleh meninggalkan pendakap kerinting sepenuhnya (serupa dengan pernyataan-jika dan gelung untuk). Jika lambda satu baris anda mengembalikan sesuatu, anda tidak perlu memasukkan areturnkenyataan. Tetapi jika anda menggunakan pendakap kerinting, maka anda mesti menyertakan returnpernyataan secara eksplisit, seperti yang anda lakukan dalam kaedah biasa.

Contoh

Contoh 1.
() -> {}
Contoh paling mudah. Dan yang paling sia-sia :), kerana ia tidak melakukan apa-apa. Contoh 2.
() -> ""
Satu lagi contoh yang menarik. Ia tidak memerlukan apa-apa dan mengembalikan rentetan kosong ( returndiabaikan, kerana ia tidak perlu). Inilah perkara yang sama, tetapi dengan return:
() -> {
    return "";
}
Contoh 3. "Hello, Dunia!" menggunakan lambdas
() -> System.out.println("Hello, World!")
Ia tidak memerlukan apa-apa dan tidak mengembalikan apa-apa (kami tidak boleh meletakkan returnsebelum panggilan ke System.out.println(), kerana println()jenis pulangan kaedah ialah void). Ia hanya memaparkan ucapan. Ini sesuai untuk pelaksanaan antara Runnablemuka. Contoh berikut adalah 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 juga boleh menyimpan ungkapan lambda sebagai Runnableobjek dan kemudian menyerahkannya kepada Threadpembina:
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 dengan lebih dekat pada saat ungkapan lambda disimpan pada pembolehubah. Antara Runnablemuka memberitahu kita bahawa objeknya mesti mempunyai public void run()kaedah. Mengikut antara muka, runkaedah tidak mengambil parameter. Dan ia tidak mengembalikan apa-apa, iaitu jenis pulangannya ialah void. Sehubungan itu, kod ini akan mencipta objek dengan kaedah yang tidak mengambil atau mengembalikan apa-apa. Ini sangat sepadan dengan kaedah Runnableantara muka run(). Itulah sebabnya kami dapat meletakkan ungkapan lambda ini dalam pembolehubah Runnable.  Contoh 4.
() -> 42
Sekali lagi, ia tidak memerlukan apa-apa, tetapi ia mengembalikan nombor 42. Ungkapan lambda sedemikian boleh dimasukkan ke dalam pembolehubah Callable, kerana antara muka ini hanya mempunyai satu kaedah yang kelihatan seperti ini:
V call(),
di manakah  V jenis pulangan (dalam kes kami,  int). Oleh itu, kita boleh menyimpan ungkapan lambda seperti berikut:
Callable<Integer> c = () -> 42;
Contoh 5. Ungkapan lambda yang melibatkan beberapa baris
() -> {
    String[] helloWorld = {"Hello", "World!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);
}
Sekali lagi, ini adalah ungkapan lambda tanpa parameter dan voidjenis pulangan (kerana tiada returnpernyataan).  Contoh 6
x -> x
Di sini kita mengambil xpembolehubah dan mengembalikannya. Sila ambil perhatian bahawa jika terdapat hanya satu parameter, maka anda boleh meninggalkan tanda kurung di sekelilingnya. Inilah perkara yang sama, tetapi dengan kurungan:
(x) -> x
Dan berikut ialah contoh dengan pernyataan pulangan yang jelas:
x -> {
    return x;
}
Atau seperti ini dengan kurungan dan pernyataan pulangan:
(x) -> {
    return x;
}
Atau dengan petunjuk jenis yang jelas (dan dengan itu dengan kurungan):
(int x) -> x
Contoh 7
x -> ++x
Kami mengambil xdan mengembalikannya, tetapi hanya selepas menambah 1. Anda boleh menulis semula lambda itu seperti ini:
x -> x + 1
Dalam kedua-dua kes, kami meninggalkan kurungan di sekeliling parameter dan badan kaedah, bersama-sama dengan pernyataan return, kerana ia adalah pilihan. Versi dengan kurungan dan pernyataan pemulangan diberikan dalam Contoh 6. Contoh 8
(x, y) -> x % y
Kami mengambil xdan ydan mengembalikan baki pembahagian xoleh y. Tanda kurung di sekeliling parameter diperlukan di sini. Ia adalah pilihan hanya apabila terdapat hanya satu parameter. Ini adalah dengan petunjuk jelas jenis:
(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 umur int. Dalam kaedah itu sendiri, kami menggunakan nama dan umur yang diluluskan untuk menetapkan pembolehubah pada kucing. Oleh kerana objek kami catialah jenis rujukan, ia akan ditukar di luar ungkapan lambda (ia akan mendapat nama dan umur yang diluluskan). Berikut ialah versi yang lebih rumit yang menggunakan lambda yang 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 +
                '}';
    }
}
Keputusan:
Cat{name='null', age=0}
Cat{name='Smokey', age=3}
Seperti yang anda lihat, Catobjek mempunyai satu keadaan, dan kemudian keadaan berubah selepas kami menggunakan ungkapan lambda. Ungkapan Lambda digabungkan dengan sempurna dengan generik. Dan jika kita perlu mencipta Dogkelas yang turut melaksanakan HasNameAndAge, maka kita boleh melakukan operasi yang sama dalam Dogkaedah main() tanpa mengubah ungkapan lambda. Tugasan 3. Tulis antara muka berfungsi dengan kaedah yang mengambil nombor dan mengembalikan nilai boolean. Tulis pelaksanaan antara muka sedemikian sebagai ungkapan lambda yang mengembalikan benar jika nombor yang diluluskan boleh dibahagikan dengan 13. Tugasan 4.Tulis antara muka berfungsi dengan kaedah yang mengambil dua rentetan dan juga mengembalikan rentetan. Tulis pelaksanaan antara muka sedemikian sebagai ungkapan lambda yang mengembalikan rentetan yang lebih panjang. Tugasan 5. Tulis antara muka berfungsi dengan kaedah yang mengambil tiga nombor titik terapung: a, b, dan c dan juga mengembalikan nombor titik terapung. Tulis pelaksanaan antara muka sedemikian sebagai ungkapan lambda yang mengembalikan diskriminasi. Sekiranya anda terlupa, itu D = b^2 — 4ac. Tugasan 6. Menggunakan antara muka berfungsi daripada Tugasan 5, tulis ungkapan lambda yang mengembalikan hasil a * b^c. Penjelasan tentang ungkapan lambda di Jawa. Dengan contoh dan tugasan. Bahagian 2
Komen
  • Popular
  • Baru
  • Tua
Anda mesti log masuk untuk meninggalkan ulasan
Halaman ini tidak mempunyai sebarang ulasan lagi