CodeGym /Java блог /Случаен /Обяснение на ламбда изрази в Java. С примери и задачи. Ча...
John Squirrels
Ниво
San Francisco

Обяснение на ламбда изрази в Java. С примери и задачи. Част 2

Публикувано в групата
За кого е тази статия?
  • Това е за хора, които четат първата част на тази статия;
  • Това е за хора, които смятат, че вече познават добре Java Core, но нямат представа за ламбда изразите в Java. Или може би са чували нещо за ламбда изрази, но подробностите липсват.
  • Това е за хора, които имат известно разбиране за ламбда изразите, но все още са уплашени от тях и не са свикнали да ги използват.
Ако не отговаряте на една от тези категории, може да намерите тази статия за скучна, недостатъчна or като цяло не е вашата чаша чай. В този случай не се колеbyteе да преминете към други неща or, ако сте добре запознати с темата, моля, направете предложения в коментарите How бих могъл да подобря or допълня статията. Обяснение на ламбда изрази в Java.  С примери и задачи.  Част 2 - 1Материалът не претендира за академична стойност, камо ли за новост. Точно обратното: ще се опитам да опиша нещата, които са сложни (за някои хора) възможно най-просто. Молба за обяснение на Stream API ме вдъхнови да напиша това. Помислих за това и реших, че някои от моите примери за потоци биха бor неразбираеми без разбиране на ламбда изрази. Така че ще започнем с ламбда изрази.

Достъп до външни променливи

Този code компorра ли се с анонимен клас?

int counter = 0;
Runnable r = new Runnable() { 

    @Override 
    public void run() { 
        counter++;
    }
};
Не. counter Променливата трябва да бъде final. Или ако не final, тогава поне не може да промени стойността си. Същият принцип се прилага в ламбда изразите. Те имат достъп до всички променливи, които могат да "видят" от мястото, където са декларирани. Но ламбда не трябва да ги променя (да им присвоява нова стойност). Има обаче начин да заобиколите това ограничение в анонимните класове. Просто създайте референтна променлива и променете вътрешното състояние на обекта. При това самата променлива не се променя (сочи към същия обект) и може безопасно да бъде маркирана като final.

final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() { 

    @Override
    public void run() {
        counter.incrementAndGet();
    }
};
Тук нашата counterпроменлива е препратка към AtomicIntegerобект. И incrementAndGet()методът се използва за промяна на състоянието на този обект. Стойността на самата променлива не се променя, докато програмата работи. Той винаги сочи към един и същ обект, което ни позволява да декларираме променливата с ключовата дума final. Ето същите примери, но с ламбда изрази:

int counter = 0;
Runnable r = () -> counter++;
Това няма да се компorра по същата причина като versionта с анонимен клас:  counterне трябва да се променя, докато програмата работи. Но всичко е наред, ако го направим така:

final AtomicInteger counter = new AtomicInteger(0); 
Runnable r = () -> counter.incrementAndGet();
Това важи и за методите за извикване. В рамките на ламбда изразите можете не само да получите достъп до всички „видими“ променливи, но и да извикате всички достъпни методи.

public class Main { 

    public static void main(String[] args) {
        Runnable runnable = () -> staticMethod();
        new Thread(runnable).start();
    } 

    private static void staticMethod() { 

        System.out.println("I'm staticMethod(), and someone just called me!");
    }
}
Въпреки че staticMethod()е частен, той е достъпен вътре в main()метода, така че може да бъде извикан и от вътрешността на ламбда, създадена в mainметода.

Кога се изпълнява ламбда израз?

Може да намерите следния въпрос за твърде прост, но трябва да го зададете по същия начин: кога ще бъде изпълнен codeът в ламбда израза? Кога се създава? Или кога се нарича (което все още не е известно)? Това се проверява сравнително лесно.

System.out.println("Program start"); 

// All sorts of code here
// ...

System.out.println("Before lambda declaration");

Runnable runnable = () -> System.out.println("I'm a lambda!");

System.out.println("After lambda declaration"); 

// All sorts of other code here
// ...

System.out.println("Before passing the lambda to the thread");
new Thread(runnable).start(); 
Изход на екрана:

Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
Можете да видите, че ламбда изразът е изпълнен в самия край, след като нишката е създадена и само когато изпълнението на програмата достигне до метода run(). Със сигурност не, когато се декларира. С декларирането на ламбда израз ние само създадохме Runnableобект и описахме How run()се държи неговият метод. Самият метод се изпълнява много по-късно.

Препратки към методите?

Препратките към методите не са пряко свързани с ламбда, но мисля, че има смисъл да кажа няколко думи за тях в тази статия. Да предположим, че имаме ламбда израз, който не прави нищо специално, а просто извиква метод.

x -> System.out.println(x)
Получава някои xи просто обаждания System.out.println(), преминавайки x. В този случай можем да го заменим с препратка към желания метод. Като този:

System.out::println
Точно така — без скоби в края! Ето по-пълен пример:

List<String> strings = new LinkedList<>(); 

strings.add("Dota"); 
strings.add("GTA5"); 
strings.add("Halo"); 

strings.forEach(x -> System.out.println(x));
В последния ред използваме forEach()метода, който взема обект, който имплементира интерфейса Consumer. Отново, това е функционален интерфейс, който има само един void accept(T t)метод. Съответно, ние пишем ламбда израз, който има един параметър (тъй като той е въведен в самия интерфейс, ние не указваме типа на параметъра; ние само посочваме, че ще го извикаме x). В тялото на ламбда израза ние записваме codeа, който ще бъде изпълнен, когато методът accept()бъде извикан. Тук просто показваме Howво се е оказало в xпроменливата. Същият този forEach()метод преминава през всички елементи в колекцията и извиква accept()метода при изпълнението наConsumerинтерфейс (нашата ламбда), предавайки всеки елемент в колекцията. Както казах, можем да заменим такъв ламбда израз (който просто класифицира различен метод) с препратка към желания метод. Тогава нашият code ще изглежда така:

List<String> strings = new LinkedList<>(); 

strings.add("Dota"); 
strings.add("GTA5"); 
strings.add("Halo");

strings.forEach(System.out::println);
Основното е, че параметрите на методите println()и accept()съвпадат. Тъй като println()методът може да приеме всичко (той е претоварен за всички примитивни типове и всички обекти), instead of ламбда изрази, можем просто да предадем препратка към метода println()към forEach(). След това forEach()ще вземе всеки елемент от колекцията и ще го предаде директно на println()метода. За всеки, който се сблъсква с това за първи път, имайте предвид, че не се обаждаме System.out.println()(с точки между думите и със скоби в края). Вместо това предаваме препратка към този метод. Ако напишем това

strings.forEach(System.out.println());
ще имаме грешка при компилация. Преди извикването на forEach()Java вижда, че System.out.println()се извиква, така че разбира, че върнатата стойност е voidи ще се опита да прехвърли voidкъм forEach(), което instead of това очаква Consumerобект.

Синтаксис за препратки към методи

Съвсем просто е:
  1. Предаваме препратка към статичен метод като този:ClassName::staticMethodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<String> strings = new LinkedList<>(); 
            strings.add("Dota"); 
            strings.add("GTA5"); 
            strings.add("Halo"); 
    
            strings.forEach(Main::staticMethod); 
        } 
    
        private static void staticMethod(String s) { 
    
            // Do something 
        } 
    }
    
  2. Предаваме препратка към нестатичен метод, използвайки съществуващ обект, като този:objectName::instanceMethodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<String> strings = new LinkedList<>();
            strings.add("Dota"); 
            strings.add("GTA5"); 
            strings.add("Halo"); 
    
            Main instance = new Main(); 
            strings.forEach(instance::nonStaticMethod); 
        } 
    
        private void nonStaticMethod(String s) { 
    
            // Do something 
        } 
    }
    
  3. Предаваме препратка към нестатичен метод, използвайки класа, който го имплементира, Howто следва:ClassName::methodName

    
    public class Main { 
    
        public static void main(String[] args) { 
    
            List<User> users = new LinkedList<>(); 
            users.add (new User("John")); 
            users.add(new User("Paul")); 
            users.add(new User("George")); 
    
            users.forEach(User::print); 
        } 
    
        private static class User { 
            private String name; 
    
            private User(String name) { 
                this.name = name; 
            } 
    
            private void print() { 
                System.out.println(name); 
            } 
        } 
    }
    
  4. Предаваме препратка към конструктор като този:ClassName::new

    Препратките към методите са много удобни, когато вече имате метод, който би работил перфектно като обратно извикване. В този случай, instead of да пишем ламбда израз, съдържащ codeа на метода, or да пишем ламбда израз, който просто извиква метода, ние просто предаваме препратка към него. И това е.

Интересна разлика между анонимни класове и ламбда изрази

В анонимен клас thisключовата дума сочи към обект от анонимния клас. Но ако използваме това вътре в ламбда, получаваме достъп до обекта на съдържащия клас. Тази, в която всъщност написахме ламбда израза. Това се случва, защото ламбда изразите се компorрат в частен метод на класа, в който са написани. Не бих препоръчал използването на тази „функция“, тъй като има страничен ефект и това противоречи на принципите на функционалното програмиране. Въпреки това, този подход е напълно съвместим с OOP. ;)

Откъде получих информацията си и Howво още трябва да прочетете?

И, разбира се, намерих много неща в Google :)
Коментари
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION