CodeGym /Blog Java /Random-PL /Wyjaśnienie wyrażeń lambda w Javie. Z przykładami i zadan...
John Squirrels
Poziom 41
San Francisco

Wyjaśnienie wyrażeń lambda w Javie. Z przykładami i zadaniami. Część 2

Opublikowano w grupie Random-PL
Dla kogo jest ten artykuł?
  • To jest dla osób, które przeczytały pierwszą część tego artykułu;
  • To jest dla ludzi, którzy myślą, że znają już dobrze Java Core, ale nie mają pojęcia o wyrażeniach lambda w Javie. A może słyszeli coś o wyrażeniach lambda, ale brakuje im szczegółów.
  • Jest przeznaczony dla osób, które mają pewną wiedzę na temat wyrażeń lambda, ale nadal są nimi zniechęcone i nie są przyzwyczajone do ich używania.
Jeśli nie pasujesz do żadnej z tych kategorii, ten artykuł może okazać się nudny, wadliwy lub ogólnie nie dla ciebie. W takim przypadku nie krępuj się przejść do innych rzeczy lub, jeśli jesteś dobrze zorientowany w temacie, proszę o sugestie w komentarzach, jak mógłbym poprawić lub uzupełnić artykuł. Wyjaśnienie wyrażeń lambda w Javie.  Z przykładami i zadaniami.  Część 2 - 1Materiał nie twierdzi, że ma jakąkolwiek wartość akademicką, nie mówiąc już o nowości. Wręcz przeciwnie: postaram się opisać rzeczy skomplikowane (dla niektórych osób) możliwie najprościej. Prośba o wyjaśnienie Stream API zainspirowała mnie do napisania tego. Pomyślałem o tym i zdecydowałem, że niektóre z moich przykładów strumieniowych byłyby niezrozumiałe bez zrozumienia wyrażeń lambda. Zaczniemy więc od wyrażeń lambda.

Dostęp do zmiennych zewnętrznych

Czy ten kod kompiluje się z anonimową klasą?

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

    @Override 
    public void run() { 
        counter++;
    }
};
Nie. counter Zmienna musi być final. A jeśli nie final, to przynajmniej nie może zmienić swojej wartości. Ta sama zasada obowiązuje w wyrażeniach lambda. Mogą uzyskać dostęp do wszystkich zmiennych, które mogą „zobaczyć” z miejsca, w którym zostały zadeklarowane. Ale lambda nie może ich zmieniać (przypisać im nową wartość). Istnieje jednak sposób na ominięcie tego ograniczenia w klasach anonimowych. Po prostu utwórz zmienną referencyjną i zmień stan wewnętrzny obiektu. W ten sposób sama zmienna nie zmienia się (wskazuje na ten sam obiekt) i może być bezpiecznie oznaczona jako final.

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

    @Override
    public void run() {
        counter.incrementAndGet();
    }
};
Tutaj nasza counterzmienna jest referencją do AtomicIntegerobiektu. A incrementAndGet()metoda służy do zmiany stanu tego obiektu. Wartość samej zmiennej nie zmienia się podczas działania programu. Zawsze wskazuje na ten sam obiekt, co pozwala nam zadeklarować zmienną za pomocą słowa kluczowego final. Oto te same przykłady, ale z wyrażeniami lambda:

int counter = 0;
Runnable r = () -> counter++;
To się nie skompiluje z tego samego powodu, co wersja z klasą anonimową:  counternie może się zmieniać podczas działania programu. Ale wszystko jest w porządku, jeśli zrobimy to w ten sposób:

final AtomicInteger counter = new AtomicInteger(0); 
Runnable r = () -> counter.incrementAndGet();
Dotyczy to również metod wywoływania. W wyrażeniach lambda można nie tylko uzyskać dostęp do wszystkich „widocznych” zmiennych, ale także wywoływać dostępne metody.

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!");
    }
}
Chociaż staticMethod()jest prywatna, jest dostępna wewnątrz main()metody, więc można ją również wywołać z wnętrza lambdy utworzonej w mainmetodzie.

Kiedy wykonywane jest wyrażenie lambda?

Poniższe pytanie może ci się wydawać zbyt proste, ale i tak powinieneś je zadać: kiedy zostanie wykonany kod wewnątrz wyrażenia lambda? Kiedy powstaje? Lub kiedy zostanie wywołany (co jeszcze nie jest znane)? Jest to dość łatwe do sprawdzenia.

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(); 
Wyjście ekranu:

Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
Widać, że wyrażenie lambda zostało wykonane na samym końcu, po utworzeniu wątku i dopiero wtedy, gdy wykonanie programu dotrze do metody run(). Na pewno nie wtedy, gdy zostanie to ogłoszone. Deklarując wyrażenie lambda, stworzyliśmy jedynie Runnableobiekt i opisaliśmy, jak run()zachowuje się jego metoda. Sama metoda jest wykonywana znacznie później.

odniesienia do metod?

Odwołania do metod nie są bezpośrednio związane z lambdami, ale myślę, że warto powiedzieć o nich kilka słów w tym artykule. Załóżmy, że mamy wyrażenie lambda, które nie robi nic specjalnego, ale po prostu wywołuje metodę.

x -> System.out.println(x)
Odbiera niektóre xi tylko połączenia System.out.println(), przechodząc x. W takim przypadku możemy go zastąpić odniesieniem do pożądanej metody. Lubię to:

System.out::println
Zgadza się — bez nawiasów na końcu! Oto pełniejszy przykład:

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

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

strings.forEach(x -> System.out.println(x));
W ostatniej linii używamy forEach()metody, która pobiera obiekt implementujący Consumerinterfejs. Ponownie, jest to funkcjonalny interfejs, posiadający tylko jedną void accept(T t)metodę. W związku z tym piszemy wyrażenie lambda, które ma jeden parametr (ponieważ jest wpisany w sam interfejs, nie określamy typu parametru, tylko wskazujemy, że będziemy go nazywać x). W treści wyrażenia lambda zapisujemy kod, który zostanie wykonany w momencie accept()wywołania metody. Tutaj po prostu wyświetlamy, co znalazło się w xzmiennej. Ta sama forEach()metoda wykonuje iterację przez wszystkie elementy w kolekcji i wywołuje accept()metodę w implementacjiConsumerinterface (nasza lambda), przekazując każdy element w kolekcji. Jak powiedziałem, możemy zastąpić takie wyrażenie lambda (takie, które po prostu klasyfikuje inną metodę) odwołaniem do pożądanej metody. Wtedy nasz kod będzie wyglądał następująco:

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

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

strings.forEach(System.out::println);
Najważniejsze jest to, aby parametry metod println()i accept()były zgodne. Ponieważ println()metoda może przyjąć wszystko (jest przeciążona dla wszystkich typów prymitywów i wszystkich obiektów), zamiast wyrażeń lambda możemy po prostu przekazać referencję do metody println()do forEach(). Następnie forEach()weźmie każdy element w kolekcji i przekaże go bezpośrednio do println()metody. Dla każdego, kto spotyka się z tym po raz pierwszy, pamiętaj, że nie dzwonimy System.out.println()(z kropkami między słowami i nawiasami na końcu). Zamiast tego przekazujemy odwołanie do tej metody. Jeśli to napiszemy

strings.forEach(System.out.println());
będziemy mieli błąd kompilacji. Przed wywołaniem do forEach(), Java widzi, że System.out.println()jest wywoływana, więc rozumie, że zwracana wartość to voidi spróbuje przekazać voiddo forEach(), która zamiast tego oczekuje Consumerobiektu.

Składnia odwołań do metod

to całkiem proste:
  1. Przekazujemy odniesienie do metody statycznej w następujący sposób: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. Przekazujemy odwołanie do metody niestatycznej przy użyciu istniejącego obiektu, jak poniżej: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. Odwołanie do metody niestatycznej przekazujemy za pomocą klasy, która ją implementuje, w następujący sposób: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. Przekazujemy referencję do konstruktora w następujący sposób:ClassName::new

    Odwołania do metod są bardzo wygodne, gdy masz już metodę, która doskonale sprawdziłaby się jako wywołanie zwrotne. W tym przypadku, zamiast pisać wyrażenie lambda zawierające kod metody lub pisać wyrażenie lambda, które po prostu wywołuje metodę, po prostu przekazujemy do niej odwołanie. I to wszystko.

Interesujące rozróżnienie między klasami anonimowymi a wyrażeniami lambda

W klasie anonimowej thissłowo kluczowe wskazuje na obiekt klasy anonimowej. Ale jeśli użyjemy tego wewnątrz lambda, uzyskamy dostęp do obiektu klasy zawierającej. Ten, w którym faktycznie napisaliśmy wyrażenie lambda. Dzieje się tak, ponieważ wyrażenia lambda są kompilowane do prywatnej metody klasy, w której zostały zapisane. Nie polecam używania tej „funkcji”, ponieważ ma ona efekt uboczny i jest sprzeczna z zasadami programowania funkcyjnego. To powiedziawszy, to podejście jest całkowicie zgodne z OOP. ;)

Skąd czerpię informacje i co jeszcze warto przeczytać?

I oczywiście znalazłem mnóstwo rzeczy w Google :)
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION