CodeGym /Java Blogu /Rastgele /Java'daki lambda ifadelerinin açıklaması. Örnekler ve gör...
John Squirrels
Seviye
San Francisco

Java'daki lambda ifadelerinin açıklaması. Örnekler ve görevler ile. Bölüm 1

grupta yayınlandı
Bu makale kimin için?
  • Java Core'u zaten iyi bildiğini düşünen, ancak Java'daki lambda ifadeleri hakkında hiçbir fikri olmayan kişiler içindir. Ya da belki lambda ifadeleri hakkında bir şeyler duymuşlardır, ancak ayrıntılar eksiktir.
  • Lambda ifadeleri konusunda belirli bir anlayışa sahip olan, ancak yine de bunlardan yılan ve bunları kullanmaya alışkın olmayan insanlar içindir.
Java'daki lambda ifadelerinin açıklaması.  Örnekler ve görevler ile.  Bölüm 1 - 1Bu kategorilerden birine uymuyorsanız, bu makaleyi sıkıcı, kusurlu veya genel olarak size göre olmayabilir. Bu durumda, başka şeylere geçmekten çekinmeyin veya konuyla ilgili bilginiz varsa, lütfen yorumlarda makaleyi nasıl geliştirebileceğim veya tamamlayabileceğim konusunda önerilerde bulunun. Materyal, yenilik bir yana, herhangi bir akademik değere sahip olduğunu iddia etmemektedir. Tam tersine: Karmaşık olan şeyleri (bazı insanlar için) olabildiğince basit bir şekilde açıklamaya çalışacağım. Stream API'yi açıklamaya yönelik bir istek, bunu yazmam için bana ilham verdi. Bunun üzerine düşündüm ve lambda ifadelerini anlamadan bazı akış örneklerimin anlaşılmaz olacağına karar verdim. Lambda ifadeleriyle başlayacağız. Bu makaleyi anlamak için bilmeniz gerekenler nelerdir?
  1. Nesne yönelimli programlamayı (OOP) anlamalısınız, yani:

    • sınıflar, nesneler ve aralarındaki fark;
    • arayüzler, sınıflardan farklılıkları ve arayüzler ile sınıflar arasındaki ilişki;
    • metotlar, bunların nasıl çağrılacağı, soyut metotlar (yani uygulaması olmayan metotlar), metot parametreleri, metot argümanları ve bunların nasıl iletileceği;
    • erişim değiştiricileri, statik yöntemler/değişkenler, nihai yöntemler/değişkenler;
    • sınıfların ve arayüzlerin kalıtımı, arayüzlerin çoklu kalıtımı.
  2. Java Çekirdeği Bilgisi: genel türler (jenerikler), koleksiyonlar (listeler), iş parçacıkları.
Peki, hadi başlayalım.

Biraz tarih

Lambda ifadeleri, Java'ya işlevsel programlamadan ve oraya matematikten geldi. 20. yüzyılın ortalarında Amerika Birleşik Devletleri'nde matematiğe ve her türlü soyutlamaya çok düşkün olan Alonzo Church, Princeton Üniversitesi'nde çalıştı. Başlangıçta programlamayla tamamen ilgisiz bir dizi soyut fikir olan lambda hesabını icat eden Alonzo Church'dü. Alan Turing ve John von Neumann gibi matematikçiler aynı zamanda Princeton Üniversitesi'nde çalıştılar. Her şey bir araya geldi: Church, lambda hesabını buldu. Turing, artık "Turing makinesi" olarak bilinen soyut bilgi işlem makinesini geliştirdi. Ve von Neumann, modern bilgisayarların temelini oluşturan bir bilgisayar mimarisi önerdi (şimdi "von Neumann mimarisi" olarak adlandırılıyor). O zamanlar, Alonzo Kilisesi' Fikirleri, meslektaşlarının çalışmaları kadar tanınmadı (saf matematik alanı hariç). Ancak, kısa bir süre sonra John McCarthy (aynı zamanda bir Princeton Üniversitesi mezunu ve hikayemiz sırasında Massachusetts Teknoloji Enstitüsü'nün bir çalışanıydı) Church'ün fikirleriyle ilgilenmeye başladı. 1958'de bu fikirlere dayanarak ilk işlevsel programlama dili olan LISP'yi yarattı. Ve 58 yıl sonra, işlevsel programlama fikirleri Java 8'e sızdı. Aradan 70 yıl bile geçmedi... Dürüst olmak gerekirse bu, matematiksel bir fikrin pratikte uygulanması için geçen en uzun süre değil. Massachusetts Institute of Technology'nin bir çalışanı) Church'ün fikirleriyle ilgilenmeye başladı. 1958'de bu fikirlere dayanarak ilk işlevsel programlama dili olan LISP'yi yarattı. Ve 58 yıl sonra, işlevsel programlama fikirleri Java 8'e sızdı. Aradan 70 yıl bile geçmedi... Dürüst olmak gerekirse bu, matematiksel bir fikrin pratikte uygulanması için geçen en uzun süre değil. Massachusetts Institute of Technology'nin bir çalışanı) Church'ün fikirleriyle ilgilenmeye başladı. 1958'de bu fikirlere dayanarak ilk işlevsel programlama dili olan LISP'yi yarattı. Ve 58 yıl sonra, işlevsel programlama fikirleri Java 8'e sızdı. Aradan 70 yıl bile geçmedi... Dürüst olmak gerekirse bu, matematiksel bir fikrin pratikte uygulanması için geçen en uzun süre değil.

konunun kalbi

Bir lambda ifadesi bir tür işlevdir. Sıradan bir Java yöntemi olarak düşünebilirsiniz, ancak ayırt edici yeteneği diğer yöntemlere argüman olarak iletilebilir. Bu doğru. Yöntemlere sadece sayıları, dizileri ve kedileri değil, diğer yöntemleri de geçirmek mümkün hale geldi! Buna ne zaman ihtiyacımız olabilir? Örneğin, bir geri arama yöntemini geçmek istiyorsak bu yardımcı olacaktır. Yani, çağırdığımız yönteme, ona ilettiğimiz başka bir yöntemi çağırabilme yeteneğine ihtiyacımız varsa. Başka bir deyişle, belirli koşullar altında bir geri aramayı ve diğerlerinde farklı bir geri aramayı geçme yeteneğine sahibiz. Ve böylece geri aramalarımızı alan yöntemimiz onları çağırır. Sıralama basit bir örnektir. Diyelim ki şuna benzeyen zekice bir sıralama algoritması yazıyoruz:

public void mySuperSort() { 
    // We do something here 
    if(compare(obj1, obj2) > 0) 
    // And then we do something here 
}
İfadede , karşılaştırılacak iki nesneyi geçerek yöntemi ifçağırıyoruz ve bu nesnelerden hangisinin "daha büyük" olduğunu bilmek istiyoruz. compare()"Daha büyük" olanın "daha küçük" olandan önce geldiğini varsayıyoruz. "Daha büyük" kelimesini tırnak içine aldım, çünkü yalnızca artan düzende değil, aynı zamanda azalan düzende de sıralama yapmayı bilecek evrensel bir yöntem yazıyoruz (bu durumda, "büyük" nesne aslında "daha küçük" nesne olacaktır. ve tersi). Sıralamamız için belirli bir algoritma ayarlamak için, onu yöntemimize geçirecek bir mekanizmaya ihtiyacımız var mySuperSort(). Bu şekilde, çağrıldığında yöntemimizi "kontrol edebileceğiz". Elbette iki ayrı yöntem yazabiliriz — mySuperSortAscend()vemySuperSortDescend()— artan ve azalan düzende sıralamak için. Veya yönteme bazı argümanlar iletebiliriz (örneğin, bir boole değişkeni; doğruysa, artan düzende ve yanlışsa azalan düzende sıralayın). Ancak, dizi dizileri listesi gibi karmaşık bir şeyi sıralamak istersek ne olur? mySuperSort()Yöntemimiz bu dize dizilerini nasıl sıralayacağını nasıl bilecek? Boyuta göre mi? Tüm kelimelerin kümülatif uzunluğuna göre mi? Belki de dizideki ilk dizeye göre alfabetik olarak? Peki ya dizi listesini bazı durumlarda dizi boyutuna göre, diğer durumlarda ise her dizideki tüm sözcüklerin kümülatif uzunluğuna göre sıralamamız gerekirse? Karşılaştırıcılar hakkında zaten bir şeyler duymuşsunuzdur ve bu durumda, sıralama yöntemimize, istenen sıralama algoritmasını açıklayan bir karşılaştırma nesnesini basitçe ileteceğimizi umuyorum. Çünkü standartsort()yöntemi, örneklerimde mySuperSort()kullanacağım ile aynı prensibe dayalı olarak uygulanmaktadır .sort()

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);
Sonuç:

  1. Dota GTA5 Halo
  2. if then else
  3. I really love Java
Burada diziler, her dizideki kelime sayısına göre sıralanır. Daha az kelime içeren bir dizi "daha az" olarak kabul edilir. Bu yüzden önce gelir. Daha fazla kelime içeren bir dizi "daha büyük" olarak kabul edilir ve sona yerleştirilir. Yönteme farklı bir karşılaştırıcı iletirsek sort(), örneğin sortByCumulativeWordLength, o zaman farklı bir sonuç alırız:

  1. if then else
  2. Dota GTA5 Halo
  3. I really love Java
Artık are dizileri, dizinin sözcüklerindeki toplam harf sayısına göre sıralanır. İlk dizide 10, ikincide 12 ve üçüncüde 15 harf vardır. Yalnızca tek bir karşılaştırıcımız varsa, bunun için ayrı bir değişken bildirmemiz gerekmez. Bunun yerine, yöntemin çağrıldığı anda anonim bir sınıf oluşturabiliriz sort(). Bunun gibi bir şey:

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; 
    } 
}); 
İlk durumdakiyle aynı sonucu alacağız. Görev 1. Bu örneği, dizileri her dizideki sözcük sayısına göre artan düzende değil, azalan düzende sıralayacak şekilde yeniden yazın. Bunların hepsini zaten biliyoruz. Nesneleri yöntemlere nasıl aktaracağımızı biliyoruz. Şu anda neye ihtiyacımız olduğuna bağlı olarak, uyguladığımız yöntemi çağıracak olan bir yönteme farklı nesneler iletebiliriz. Bu şu soruyu akla getiriyor: neden burada bir lambda ifadesine ihtiyacımız var?  Çünkü bir lambda ifadesi, tam olarak bir yöntemi olan bir nesnedir. Bir "yöntem nesnesi" gibi. Bir nesnede paketlenmiş bir yöntem. Sadece biraz alışılmadık bir sözdizimine sahip (ancak daha sonra buna daha fazla değineceğiz). Bu koda bir kez daha göz atalım:

arrays.sort(new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
});
Burada diziler listemizi alıyoruz ve yöntemini çağırıyoruz sort(), buna tek bir yöntemle bir karşılaştırma nesnesi iletiyoruz compare()(adı bizim için önemli değil - sonuçta, bu nesnenin tek yöntemi bu, bu yüzden yanlış gidemeyiz). Bu metot üzerinde çalışacağımız iki parametreye sahiptir. IntelliJ IDEA'da çalışıyorsanız, muhtemelen kodu aşağıdaki gibi önemli ölçüde yoğunlaştırmayı teklif ettiğini görmüşsünüzdür:

arrays.sort((o1, o2) -> o1.length - o2.length);
Bu, altı satırı tek bir kısa satıra indirger. 6 satır bir kısa olarak yeniden yazılır. Bir şey kayboldu, ama önemli bir şey olmadığını garanti ederim. Bu kod, anonim bir sınıfta olduğu gibi tam olarak aynı şekilde çalışacaktır. Görev 2. Bir lambda ifadesi kullanarak Görev 1'in çözümünü yeniden yazmaya ilişkin bir tahminde bulunun (en azından IntelliJ IDEA'dan anonim sınıfınızı bir lambda ifadesine dönüştürmesini isteyin).

Arayüzler hakkında konuşalım

Prensip olarak, bir arayüz basitçe soyut yöntemlerin bir listesidir. Bazı arabirimleri uygulayan bir sınıf oluşturduğumuzda, sınıfımız arabirimde bulunan yöntemleri uygulamalıdır (veya sınıfı soyut yapmalıyız). Pek çok farklı yöntemi olan arayüzler vardır (örneğin,  List) ve yalnızca bir yöntemi olan arayüzler vardır (örneğin, Comparatorveya Runnable). Tek bir yöntemi olmayan arabirimler vardır (örneğin işaretleyici arabirimler Serializable). Yalnızca bir yöntemi olan arabirimlere işlevsel arabirimler de denir . Java 8'de, özel bir ek açıklama ile bile işaretlenirler:@FunctionalInterface. Lambda ifadeleri için hedef türleri olarak uygun olan bu tek yöntemli arabirimlerdir. Yukarıda söylediğim gibi, lambda ifadesi bir nesneye sarılmış bir yöntemdir. Ve böyle bir nesneyi geçtiğimizde, aslında bu tek yöntemi geçiyoruz. Metodun ne dendiği umurumuzda değilmiş meğer. Bizim için önemli olan tek şey yöntem parametreleri ve tabii ki yöntemin gövdesidir. Özünde, bir lambda ifadesi, işlevsel bir arayüzün uygulanmasıdır. Tek bir metoda sahip bir arayüz gördüğümüz her yerde, isimsiz bir sınıf lambda olarak yeniden yazılabilir. Arayüzün birden fazla veya daha az yöntemi varsa, o zaman bir lambda ifadesi çalışmaz ve bunun yerine anonim bir sınıf veya hatta sıradan bir sınıfın bir örneğini kullanırız. Şimdi lambdaları biraz inceleme zamanı. :)

Sözdizimi

Genel sözdizimi şöyle bir şeydir:

(parameters) -> {method body}
Yani, yöntem parametrelerini çevreleyen parantezler, bir "ok" (bir tire ve büyüktür işaretinden oluşur) ve ardından her zaman olduğu gibi parantez içinde yöntem gövdesi. Parametreler, arayüz yönteminde belirtilenlere karşılık gelir. Değişken türleri derleyici tarafından açık bir şekilde belirlenebiliyorsa (bizim durumumuzda, nesnemiz ] Listkullanılarak yazıldığı için dize dizileriyle çalıştığımızı bilir String[), o zaman türlerini belirtmeniz gerekmez.
Belirsizlerse, türü belirtin. IDEA, gerekmediği takdirde gri renge boyayacaktır.
Bu Oracle eğitiminde ve başka yerlerde daha fazlasını okuyabilirsiniz . Buna " hedef yazma " denir . Değişkenleri istediğiniz gibi adlandırabilirsiniz — arabirimde belirtilen adların aynısını kullanmak zorunda değilsiniz. Parametre yoksa boş parantezleri belirtmeniz yeterlidir. Yalnızca bir parametre varsa, değişken adını herhangi bir parantez kullanmadan belirtmeniz yeterlidir. Artık parametreleri anladığımıza göre, lambda ifadesinin gövdesini tartışmanın zamanı geldi. Kıvrımlı parantezlerin içine, sıradan bir yöntemde olduğu gibi kod yazarsınız. Kodunuz tek bir satırdan oluşuyorsa, süslü parantezleri tamamen atlayabilirsiniz (if-deyimleri ve for-döngülerine benzer şekilde). Tek satırlık lambdanız bir şey döndürürse, bir tane eklemeniz gerekmez.returnifade. Ancak kaşlı ayraçlar kullanıyorsanız, returntıpkı sıradan bir yöntemde yaptığınız gibi, açıkça bir ifade eklemeniz gerekir.

örnekler

Örnek 1.

() -> {}
En basit örnek. Ve en anlamsız :) çünkü hiçbir şey yapmıyor. Örnek 2.

() -> ""
Başka bir ilginç örnek. Hiçbir şey almaz ve boş bir dize döndürür ( returngereksiz olduğu için atlanır). İşte aynı şey, ancak return:

() -> { 
    return ""; 
}
Örnek 3. "Merhaba Dünya!" lambda kullanmak

() -> System.out.println("Hello, World!")
Hiçbir şey almaz ve hiçbir şey döndürmez ( yöntemin dönüş türü olduğu için returnto çağrısından önce koyamayız ). Sadece selamlamayı görüntüler. Bu, arayüzün uygulanması için idealdir . Aşağıdaki örnek daha eksiksizdir: System.out.println()println()voidRunnable

public class Main { 
    public static void main(String[] args) { 
        new Thread(() -> System.out.println("Hello, World!")).start(); 
    } 
}
Veya bunun gibi:

public class Main { 
    public static void main(String[] args) { 
        Thread t = new Thread(() -> System.out.println("Hello, World!")); 
        t.start();
    } 
}
Ya da lambda ifadesini bir nesne olarak kaydedebilir Runnableve ardından yapıcıya iletebiliriz Thread:

public class Main { 
    public static void main(String[] args) { 
        Runnable runnable = () -> System.out.println("Hello, World!"); 
        Thread t = new Thread(runnable); 
        t.start(); 
    } 
}
Bir lambda ifadesinin bir değişkene kaydedildiği ana daha yakından bakalım. Arayüz Runnablebize nesnelerinin bir public void run()yöntemi olması gerektiğini söyler. Arayüze göre, runyöntem parametre almaz. Ve hiçbir şey döndürmez, yani dönüş tipi void. Buna göre, bu kod hiçbir şey almayan veya döndürmeyen bir yöntemle bir nesne oluşturacaktır. RunnableBu, arayüzün yöntemiyle mükemmel bir şekilde eşleşir run(). Bu yüzden bu lambda ifadesini bir değişkene koyabildik Runnable.  Örnek 4.

() -> 42
Yine hiçbir şey almaz, ancak 42 sayısını döndürür. Böyle bir lambda ifadesi bir değişkene yerleştirilebilir Callable, çünkü bu arayüzün şuna benzeyen tek bir yöntemi vardır:

V call(),
dönüş türü nerede  V (bizim durumumuzda,  int). Buna göre bir lambda ifadesini aşağıdaki gibi kaydedebiliriz:

Callable<Integer> c = () -> 42;
Örnek 5. Birkaç satır içeren bir lambda ifadesi

() -> { 
    String[] helloWorld = {"Hello", "World!"}; 
    System.out.println(helloWorld[0]); 
    System.out.println(helloWorld[1]); 
}
Yine, bu, parametresiz ve voiddönüş tipi olmayan bir lambda ifadesidir (çünkü ifade yoktur return).  Örnek 6

x -> x
Burada bir xdeğişken alıp geri döndürüyoruz. Yalnızca bir parametre varsa, etrafındaki parantezleri atlayabileceğinizi lütfen unutmayın. İşte aynı şey, ancak parantez içinde:

(x) -> x
Ve işte açık bir dönüş ifadesine sahip bir örnek:

x -> { 
    return x;
}
Veya bunun gibi parantezler ve bir dönüş ifadesi ile:

(x) -> { 
    return x;
}
Veya türün açık bir göstergesiyle (ve dolayısıyla parantezlerle):

(int x) -> x
Örnek 7

x -> ++x
Alıp geri veriyoruz xama ancak 1 ekledikten sonra. O lambdayı şu şekilde yeniden yazabilirsiniz:

x -> x + 1
returnHer iki durumda da, isteğe bağlı olduklarından, ifadeyle birlikte parametre ve yöntem gövdesi etrafındaki parantezleri atlıyoruz . Parantezli ve dönüş ifadeli sürümler Örnek 6'da verilmiştir. Örnek 8

(x, y) -> x % y
ile bölümünden kalanını alır xve döndürürüz . Parametrelerin etrafındaki parantezler burada gereklidir. Yalnızca bir parametre olduğunda isteğe bağlıdırlar. İşte türlerin açık bir göstergesi ile: yxy

(double x, int y) -> x % y
Örnek 9

(Cat cat, String name, int age) -> {
    cat.setName(name); 
    cat.setAge(age); 
}
CatBir nesne, bir Stringisim ve bir int yaşı alıyoruz . Metodun kendisinde, kedi üzerindeki değişkenleri ayarlamak için geçen adı ve yaşı kullanırız. Nesnemiz bir referans tipi olduğu için catlambda ifadesinin dışında değiştirilecektir (geçen adı ve yaşı alacaktır). İşte benzer bir lambda kullanan biraz daha karmaşık bir sürüm:

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 + 
                '}';
    }
}
Sonuç:

Cat{name='null', age=0}
Cat{name='Smokey', age=3}
Gördüğünüz gibi, Catnesnenin bir durumu vardı ve lambda ifadesini kullandıktan sonra durum değişti. Lambda ifadeleri, jeneriklerle mükemmel bir şekilde birleşir. DogVe ayrıca implement eden bir sınıf yaratmamız gerekirse , o zaman aynı işlemleri  lambda ifadesini değiştirmeden metot içinde HasNameAndAgegerçekleştirebiliriz . Görev 3. Bir sayı alan ve bir mantıksal değer döndüren bir yöntemle işlevsel bir arayüz yazın. Geçirilen sayı 13'e bölünebilirse doğru döndüren bir lambda ifadesi gibi bir arayüzün uygulamasını yazın . Görev 4.Dogmain()İki dizi alan ve aynı zamanda bir dizi döndüren bir yöntemle işlevsel bir arabirim yazın. Daha uzun dizeyi döndüren bir lambda ifadesi gibi bir arayüzün uygulamasını yazın. Görev 5. Üç kayan noktalı sayı alan (a, b ve c) ve ayrıca bir kayan noktalı sayı döndüren bir yöntemle işlevsel bir arayüz yazın. Ayırt ediciyi döndüren bir lambda ifadesi gibi bir arabirimin uygulamasını yazın. Unuttuysan, bu D = b^2 — 4ac. Görev 6. Görev 5'teki işlevsel arabirimi kullanarak, sonucunu döndüren bir lambda ifadesi yazın a * b^c. Java'daki lambda ifadelerinin açıklaması. Örnekler ve görevler ile. Bölüm 2
Yorumlar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION