JavaRush /Java Blogu /Random-AZ /Java-da lambda ifadələri haqqında məşhurdur. Nümunələr və...
Стас Пасинков
Səviyyə
Киев

Java-da lambda ifadələri haqqında məşhurdur. Nümunələr və tapşırıqlarla. 1-ci hissə

Qrupda dərc edilmişdir
Bu məqalə kimin üçündür?
  • Java Core-u artıq yaxşı bildiyini düşünən, lakin Java-da lambda ifadələri haqqında heç bir fikri olmayanlar üçün. Və ya, bəlkə də, siz artıq lambdalar haqqında bir şey eşitmisiniz, lakin təfərrüatlar olmadan.
  • lambda ifadələri haqqında bir qədər anlayışı olan, lakin hələ də onlardan istifadə etməkdən qorxan və qeyri-adi olanlar üçün.
Bu kateqoriyalardan birinə aid deyilsinizsə, bu məqaləni cansıxıcı, səhv və ümumiyyətlə “sərin deyil” tapa bilərsiniz. Bu halda, ya keçməkdən çəkinməyin, ya da mövzunu yaxşı bilirsinizsə, şərhlərdə məqaləni necə təkmilləşdirə və ya əlavə edə biləcəyimi təklif edin. Material heç bir akademik dəyər iddia etmir, daha az yenilikdir. Əksinə, əksinə: mən burada mürəkkəb (bəziləri üçün) şeyi mümkün qədər sadə təsvir etməyə çalışacağam. Mən axın api izah etmək xahişi ilə yazmağa ilham verdim. Mən bu barədə düşündüm və lambda ifadələrini başa düşmədən "axınlar" haqqında bəzi nümunələrimin anlaşılmaz olacağına qərar verdim. Beləliklə, lambdalardan başlayaq. Java-da lambda ifadələri haqqında məşhurdur.  Nümunələr və tapşırıqlarla.  1-1 hissəBu məqaləni başa düşmək üçün hansı bilik tələb olunur:
  1. Obyekt yönümlü proqramlaşdırma (bundan sonra OOP adlandırılacaq), yəni:
    • siniflərin və obyektlərin nə olduğunu, onlar arasındakı fərqin nə olduğunu bilmək;
    • interfeyslərin nə olduğunu, onların siniflərdən nə ilə fərqləndiyini, onlar arasında hansı əlaqənin olduğunu (interfeyslər və siniflər) bilmək;
    • metodun nə olduğunu, onun necə adlandırılacağını, mücərrəd metodun (yaxud icrası olmayan metod) nə olduğunu, metodun parametrlərinin/arqumentlərinin nə olduğunu, onları ora necə ötürməli olduğunu bilmək;
    • giriş modifikatorları, statik metodlar/dəyişənlər, yekun metodlar/dəyişənlər;
    • irsilik (siniflər, interfeyslər, interfeyslərin çoxlu irsiyyəti).
  2. Java Core bilikləri: generiklər, kolleksiyalar (siyahılar), mövzular.
Yaxşı, başlayaq.

Bir az tarix

Lambda ifadələri Java-ya funksional proqramlaşdırmadan, orada isə riyaziyyatdan gəldi. 20-ci əsrin ortalarında Amerikada Princeton Universitetində riyaziyyatı və hər cür abstraksiyanı çox sevən müəyyən bir Alonzo Kilsəsi işləyirdi. Əvvəlcə bəzi mücərrəd ideyalar toplusu olan və proqramlaşdırma ilə heç bir əlaqəsi olmayan lambda hesabı ilə çıxış edən Alonzo Kilsəsi idi. Eyni zamanda, Alan Turing və John von Neumann kimi riyaziyyatçılar eyni Princeton Universitetində çalışırdılar. Hər şey bir araya gəldi: Church lambda hesablama sistemi ilə gəldi, Türinq indi "Türinq maşını" kimi tanınan mücərrəd hesablama maşınını inkişaf etdirdi. Yaxşı, fon Neumann müasir kompüterlərin əsasını təşkil edən kompüterlərin arxitekturasının diaqramını təklif etdi (və indi "von Neumann memarlığı" adlanır). O dövrdə Alonzo Kilsənin ideyaları həmkarlarının işi ("saf" riyaziyyat sahəsi istisna olmaqla) qədər şöhrət qazanmamışdı. Ancaq bir az sonra müəyyən bir Con Makkarti (həmçinin, hekayə zamanı Prinston Universitetinin məzunu - Massaçusets Texnologiya İnstitutunun əməkdaşı) Churchun fikirləri ilə maraqlandı. Onların əsasında 1958-ci ildə ilk funksional proqramlaşdırma dili olan Lisp-i yaratdı. 58 il sonra isə funksional proqramlaşdırma ideyaları Java-ya 8 rəqəmi kimi sızdı. Aradan 70 il belə keçmədi... Əslində bu, riyazi ideyanın praktikada tətbiqi üçün ən uzun müddət deyil.

mahiyyəti

Lambda ifadəsi belə bir funksiyadır. Bunu Java-da adi metod kimi düşünə bilərsiniz, yeganə fərq odur ki, arqument kimi digər metodlara ötürülə bilər. Bəli, üsullara təkcə rəqəmləri, simləri və pişikləri deyil, digər üsulları da ötürmək mümkün oldu! Buna nə vaxt ehtiyacımız ola bilər? Məsələn, bəzi geri çağırışlardan keçmək istəsək. Bizə zəng etdiyimiz metod lazımdır ki, ona keçdiyimiz başqa bir metodu çağıra bilək. Yəni, bəzi hallarda bir geri çağırışı, digərlərində isə digərini ötürmək imkanımız olsun. Geri çağırışlarımızı qəbul edən metodumuz onları çağıracaq. Sadə bir nümunə çeşidləmədir. Tutaq ki, biz bu kimi görünən bir növ çətin çeşidləmə yazırıq:
public void mySuperSort() {
    // ... do something here
    if(compare(obj1, obj2) > 0)
    // ... and here we do something
}
Harada, ifmetodu adlandırırıq compare(), ora müqayisə etdiyimiz iki obyekti veririk və bu obyektlərdən hansının “böyük” olduğunu öyrənmək istəyirik. "Daha çox" olanı "kiçik" olandan əvvəl qoyacağıq. Mən dırnaq içərisində “daha ​​çox” yazdım, çünki biz təkcə artan deyil, həm də azalan qaydada çeşidləyə biləcək universal bir üsul yazırıq (bu halda “daha ​​çox” mahiyyətcə daha kiçik olan obyekt olacaq və əksinə) . Düzgün çeşidləmək istədiyimiz qaydanı təyin etmək üçün onu birtəhər özümüzə ötürməliyik mySuperSort(). Bu halda, metodumuzu çağırarkən bir şəkildə "nəzarət" edə biləcəyik. Əlbəttə ki, artan və azalan qaydada çeşidləmə mySuperSortAsc()üçün iki ayrı üsul yaza bilərsiniz . mySuperSortDesc()Və ya metodun daxilində bəzi parametrləri keçirin (məsələn, booleanəgər true, artan qaydada və əgər falseazalan qaydada olarsa). Bəs bəzi sadə strukturları deyil, məsələn, sətir massivlərinin siyahısını çeşidləmək istəsək nə olar? Metodumuz mySuperSort()bu sətir massivlərinin necə çeşidlənməsini necə biləcək? Ölçü üçün? Sözlərin ümumi uzunluğuna görə? Bəlkə də serialın birinci cərgəsindən asılı olaraq əlifba sırası ilə? Bəs bəzi hallarda massivlərin siyahısını massivin ölçüsünə, digər halda isə massivdəki sözlərin ümumi uzunluğuna görə çeşidləmək lazım gələrsə, necə? Düşünürəm ki, siz artıq müqayisələr haqqında eşitmisiniz və belə hallarda biz sadəcə olaraq müqayisə obyektini çeşidləmək istədiyimiz qaydaları təsvir etdiyimiz çeşidləmə metodumuza keçirik. Standart metod sort()eyni prinsiplə həyata keçirildiyi üçün mySuperSort()nümunələrdə standart metoddan istifadə edəcəyəm 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);
Nəticə:
  1. Ana çərçivəni yudu
  2. sülh Əmək bilər
  3. Mən java-nı həqiqətən sevirəm
Burada massivlər hər massivdəki sözlərin sayına görə sıralanır. Daha az sözdən ibarət massiv “kiçik” hesab olunur. Buna görə də başlanğıcda gəlir. Daha çox söz olan söz "daha çox" sayılır və sonunda bitir. Əgər metoda sort()başqa bir müqayisə aparsaq (sortByWordsLength), nəticə fərqli olacaq:
  1. sülh Əmək bilər
  2. Ana çərçivəni yudu
  3. Mən java-nı həqiqətən sevirəm
İndi massivlər belə massivin sözlərindəki hərflərin ümumi sayına görə sıralanır. Birinci halda 10 hərf, ikincidə 12, üçüncüdə isə 15 hərf var. Əgər biz yalnız bir müqayisəçidən istifadə etsək, onda biz onun üçün ayrıca dəyişən yarada bilmərik, sadəcə olaraq anonim sinif obyekti yarada bilərik. metodu çağırma vaxtı sort(). Bunun kimi:
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;
    }
});
Nəticə birinci halda olduğu kimi olacaq. Tapşırıq 1 . Bu nümunəni yenidən yazın ki, o, massivləri massivdəki sözlərin sayına görə artan qaydada deyil, azalan ardıcıllıqla çeşidləsin. Biz bütün bunları artıq bilirik. Biz obyektləri metodlara necə ötürəcəyimizi bilirik, bu və ya digər obyekti hal-hazırda ehtiyacımızdan asılı olaraq metoda ötürə bilərik və belə bir obyekti keçdiyimiz metodun içərisində həyata keçirməyi yazdığımız üsul adlanacaq. . Sual yaranır: lambda ifadələrinin bununla nə əlaqəsi var? Nəzərə alsaq ki, lambda məhz bir metodu ehtiva edən obyektdir. Metod obyekti kimidir. Bir obyektə bükülmüş üsul. Onların bir az qeyri-adi sintaksisi var (lakin bu barədə daha sonra). Bu girişə bir daha nəzər salaq
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Burada siyahımızı götürürük arraysvə onun metodunu çağırırıq sort(), burada müqayisə obyektini bir metodla keçirik compare()(nə adlanması bizim üçün fərqi yoxdur, çünki bu obyektdə yeganədir, onu qaçırmayacağıq). Bu üsul iki parametr götürür, biz bundan sonra işləyəcəyik. Əgər siz IntelliJ IDEA- da işləyirsinizsə , yəqin ki, onun sizə bu kodu əhəmiyyətli dərəcədə qısaltmaq üçün necə təklif etdiyini görmüsünüz:
arrays.sort((o1, o2) -> o1.length - o2.length);
Beləliklə, altı sətir bir qısa sətirə çevrildi. 6 sətir bir qısa sətirə yenidən yazılmışdır. Nəsə itdi, amma zəmanət verirəm ki, heç bir vacib heç nə yoxa çıxmayıb və bu kod anonim siniflə eyni şəkildə işləyəcək. Tapşırıq 2 . Lambdalardan istifadə edərək 1-ci problemin həllini necə yenidən yazacağınızı anlayın (son çarə olaraq IntelliJ IDEA-dan anonim sinifinizi lambdaya çevirməsini xahiş edin).

İnterfeyslər haqqında danışaq

Əsasən, interfeys yalnız mücərrəd metodların siyahısıdır. Biz bir sinif yaratdıqda və onun bir növ interfeys həyata keçirəcəyini söylədikdə, biz sinifimizdə interfeysdə sadalanan metodların tətbiqini yazmalıyıq (yaxud son çarə olaraq onu yazmamalı, sinfi mücərrəd etməliyik. ). Çox müxtəlif üsullarla interfeyslər var (məsələn List), yalnız bir metodu olan interfeyslər var (məsələn, eyni Müqayisə edən və ya İşlənə bilən). Heç bir metodu olmayan interfeyslər var (marker interfeysləri adlanır, məsələn Serializable). Yalnız bir metodu olan interfeyslərə funksional interfeyslər də deyilir . Java 8-də onlar hətta xüsusi @FunctionalInterface annotasiyası ilə qeyd olunurlar . Bu, lambda ifadələri ilə istifadə üçün uyğun olan bir metodla interfeyslərdir. Yuxarıda dediyim kimi, lambda ifadəsi obyektə bükülmüş bir üsuldur. Və belə bir obyektin yanından keçəndə, əslində, bu tək metoddan keçirik. Məlum oldu ki, bu metodun necə adlandırılması bizim üçün heç bir əhəmiyyət kəsb etmir. Bizim üçün vacib olan bu metodun qəbul etdiyi parametrlər və əslində metod kodunun özüdür. Bir lambda ifadəsi, əslində. funksional interfeysin həyata keçirilməsi. Bir metodla interfeys gördüyümüz yerdə bu, lambdadan istifadə edərək belə anonim sinfi yenidən yaza biləcəyimiz deməkdir. Əgər interfeysdə birdən çox/az metod varsa, o zaman lambda ifadəsi bizə uyğun gəlməyəcək və biz anonim sinifdən, hətta adi sinifdən istifadə edəcəyik. Lambdaları qazmağın vaxtı gəldi. :)

Sintaksis

Ümumi sintaksis belədir:
(параметры) -> {тело метода}
Yəni, mötərizə, onların içərisində metod parametrləri, bir "ox" (bunlar ard-arda iki simvoldur: mənfi və daha böyük), bundan sonra metodun gövdəsi həmişə olduğu kimi qıvrımlı mötərizədədir. Parametrlər metodu təsvir edərkən interfeysdə göstərilənlərə uyğun gəlir. Dəyişənlərin tipi kompilyator tərəfindən aydın şəkildə müəyyən edilə bilərsə (bizim vəziyyətimizdə sətir massivləri ilə işlədiyimiz dəqiq məlumdur, çünki o, Listsətir massivləri ilə dəqiq şəkildə yazılmışdır), onda dəyişənlərin tipi String[]lazım deyil. yazılsın.
Əmin deyilsinizsə, növü göstərin və ehtiyac olmadıqda IDEA onu boz rəngdə vurğulayacaq.
Məsələn, Oracle təlimatında daha çox oxuya bilərsiniz . Buna "hədəf yazmaq" deyilir . Siz dəyişənlərə hər hansı ad verə bilərsiniz, mütləq interfeysdə göstərilənləri deyil. Parametrlər yoxdursa, sadəcə mötərizələr. Yalnız bir parametr varsa, sadəcə mötərizəsiz dəyişənin adı. Parametrləri sıraladıq, indi lambda ifadəsinin özü haqqında. Buruq mötərizələrin içərisinə kodu adi üsul kimi yazın. Bütün kodunuz yalnız bir sətirdən ibarətdirsə, siz ümumiyyətlə əyri mötərizələr yazmağa ehtiyac yoxdur (ifs və döngələrdə olduğu kimi). Əgər lambdanız bir şey qaytarırsa, lakin onun gövdəsi bir sətirdən ibarətdirsə, returnyazmağa heç ehtiyac yoxdur. Ancaq buruq mötərizələriniz varsa, adi üsulda olduğu kimi, açıq şəkildə yazmalısınız return.

Nümunələr

Misal 1.
() -> {}
Ən sadə variant. Və ən mənasızı :).Çünki heç nə etmir. Misal 2.
() -> ""
Həm də maraqlı bir seçim. O, heç nə qəbul etmir və boş sətir qaytarır ( returnlazımsız kimi buraxılıb). Eyni, lakin ilə return:
() -> {
    return "";
}
Nümunə 3. Lambdalardan istifadə edən salam dünya
() -> System.out.println("Hello world!")
returnHeç nə qəbul etmir, heç nə qaytarmır ( zəngdən əvvəl qoya bilmərik System.out.println(), çünki metodda qaytarma növü println() — void)sadəcə olaraq ekranda yazı göstərir. İnterfeys həyata keçirmək üçün idealdır Runnable. Eyni nümunə daha dolğundur:
public class Main {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello world!")).start();
    }
}
Və ya bu kimi:
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("Hello world!"));
        t.start();
    }
}
RunnableVə ya hətta lambda ifadəsini tipli obyekt kimi saxlaya və sonra onu konstruktora ötürə bilərik thread’а:
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("Hello world!");
        Thread t = new Thread(runnable);
        t.start();
    }
}
Lambda ifadəsinin dəyişənə saxlanma anına daha yaxından nəzər salaq. İnterfeys Runnablebizə deyir ki, onun obyektlərinin metodu olmalıdır public void run(). İnterfeys görə, run metodu heç nəyi parametr kimi qəbul etmir. Və heç nə qaytarmır (void). Buna görə də, bu şəkildə yazarkən, heç nə qəbul etməyən və ya qaytarmayan hansısa üsulla obyekt yaradılacaq. run()Hansı ki, interfeysdəki üsulla olduqca uyğundur Runnable. Buna görə də biz bu lambda ifadəsini kimi dəyişənə qoya bildik Runnable. Misal 4
() -> 42
Yenə də heç nə qəbul etmir, lakin 42 rəqəmini qaytarır. Bu lambda ifadəsi tipli dəyişəndə ​​yerləşdirilə bilər Callable, çünki bu interfeys yalnız bir metodu müəyyən edir, bu da belə görünür:
V call(),
Vqayıdış dəyərinin növü haradadır (bizim vəziyyətimizdə ) int. Müvafiq olaraq, belə bir lambda ifadəsini aşağıdakı kimi saxlaya bilərik:
Callable<Integer> c = () -> 42;
Misal 5. Bir neçə sətirdə lambda
() -> {
    String[] helloWorld = {"Hello", "world!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);
}
Yenə də bu, parametrləri və qaytarma növü olmayan lambda ifadəsidir void(çünki yoxdur return). Misal 6
x -> x
Burada nəyisə dəyişənə daxil edirik хvə onu qaytarırıq. Nəzərə alın ki, yalnız bir parametr qəbul edilərsə, onun ətrafındakı mötərizələrin yazılmasına ehtiyac yoxdur. Eyni, lakin mötərizələrlə:
(x) -> x
Və burada açıq bir seçim var return:
x -> {
    return x;
}
Və ya bu kimi, mötərizədə və return:
(x) -> {
    return x;
}
Və ya növün açıq bir göstəricisi ilə (və müvafiq olaraq mötərizə ilə):
(int x) -> x
Misal 7
x -> ++x
Biz onu qəbul edirik хvə geri qaytarırıq, amma 1daha çoxu üçün. Siz həmçinin bu şəkildə yenidən yaza bilərsiniz:
x -> x + 1
Hər iki halda biz parametr, metod gövdəsi və söz ətrafında mötərizə göstərmirik return, çünki bu lazım deyil. Mötərizədə və qaytarma ilə seçimlər nümunə 6-da təsvir edilmişdir. Misal 8
(x, y) -> x % y
Bəzilərini qəbul edirik хуbölmənin qalan hissəsini xilə qaytarırıq y. Parametrlər ətrafında mötərizələr artıq burada tələb olunur. Yalnız bir parametr olduqda onlar isteğe bağlıdır. Tiplərin açıq şəkildə göstərildiyi kimi:
(double x, int y) -> x % y
Misal 9
(Cat cat, String name, int age) -> {
    cat.setName(name);
    cat.setAge(age);
}
Biz Cat obyektini, adı və tam yaşı olan sətri qəbul edirik. Metodun özündə biz keçmiş adı və yaşı Pişikə təyin etdik. Dəyişənimiz istinad növü olduğundan, lambda ifadəsinin xaricindəki Cat obyekti dəyişəcək (o, daxilə ötürülən adı catvə yaşı alacaq). Bənzər bir lambda istifadə edən bir az daha mürəkkəb versiya:
public class Main {
    public static void main(String[] args) {
        // create a cat and print to the screen to make sure it's "blank"
        Cat myCat = new Cat();
        System.out.println(myCat);

        // create lambda
        Settable<Cat> s = (obj, name, age) -> {
            obj.setName(name);
            obj.setAge(age);
        };

        // call the method, to which we pass the cat and the lambda
        changeEntity(myCat, s);
        // display on the screen and see that the state of the cat has changed (has a name and age)
        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 +
                '}';
    }
}
Nəticə: Cat{name='null', age=0} Cat{name='Murzik', age=3} Gördüyünüz kimi əvvəlcə Cat obyektinin bir vəziyyəti var idi, lakin lambda ifadəsindən istifadə etdikdən sonra vəziyyət dəyişdi . Lambda ifadələri generiklərlə yaxşı işləyir. DogVə əgər məsələn, onu da həyata keçirəcək bir sinif yaratmalıyıqsa WithNameAndAge, o zaman metodda main()lambda ifadəsinin özünü dəyişdirmədən eyni əməliyyatları Dog ilə edə bilərik. Tapşırıq 3 . Rəqəm götürən və Boolean dəyərini qaytaran metodla funksional interfeys yazın. Bu cür interfeysin icrasını lambda ifadəsi şəklində yazın, trueəgər ötürülən ədəd 13-ə qalıqsız bölünürsə , onu qaytarın.Tapşırıq 4 . İki sətir götürən və eyni sətri qaytaran metodla funksional interfeys yazın. Ən uzun sətri qaytaran lambda şəklində belə bir interfeysin həyata keçirilməsini yazın. Tapşırıq 5 . Üç fraksiyalı ədədi qəbul edən metodla funksional interfeys yazın: a, b, cvə eyni kəsr ədədini qaytarır. Diskriminant qaytaran lambda ifadəsi şəklində belə bir interfeysin həyata keçirilməsini yazın. Kim unutdu, D = b^2 - 4ac . Tapşırıq 6 . 5-ci tapşırığın funksional interfeysindən istifadə edərək, əməliyyatın nəticəsini qaytaran lambda ifadəsini yazın a * b^c. Java-da lambda ifadələri haqqında məşhurdur. Nümunələr və tapşırıqlarla. 2-ci hissə.
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION