JavaRush /Blog Java /Random-PL /Popularny na temat wyrażeń lambda w Javie. Z przykładami ...
Стас Пасинков
Poziom 26
Киев

Popularny na temat wyrażeń lambda w Javie. Z przykładami i zadaniami. Część 2

Opublikowano w grupie Random-PL
Dla kogo jest ten artykuł?
  • Dla tych, którzy przeczytali pierwszą część tego artykułu;

  • Dla tych, którzy myślą, że dobrze znają już Java Core, ale nie mają pojęcia o wyrażeniach lambda w Javie. A może słyszałeś już coś o lambdach, ale bez szczegółów.

  • dla tych, którzy mają pewną wiedzę na temat wyrażeń lambda, ale nadal boją się ich używać i nie są w stanie ich używać.

Dostęp do zmiennych zewnętrznych

Czy ten kod skompiluje się z anonimową klasą?
int counter = 0;
Runnable r = new Runnable() {
    @Override
    public void run() {
        counter++;
    }
};
NIE. Zmienna countermusi być final. Albo niekoniecznie final, ale w żadnym wypadku nie może zmienić swojej wartości. Tę samą zasadę stosuje się w wyrażeniach lambda. Mają dostęp do wszystkich zmiennych, które są dla nich „widoczne” z miejsca, w którym są zadeklarowane. Ale lambda nie powinna ich zmieniać (przypisywać nową wartość). To prawda, że ​​​​istnieje możliwość ominięcia tego ograniczenia w klasach anonimowych. Wystarczy utworzyć zmienną typu referencyjnego i zmienić stan wewnętrzny obiektu. W tym wypadku sama zmienna będzie wskazywała na ten sam obiekt i w tym wypadku śmiało można go oznaczyć jako final.
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() {
    @Override
    public void run() {
        counter.incrementAndGet();
    }
};
Tutaj nasza zmienna counterjest referencją do obiektu typu AtomicInteger. Aby zmienić stan tego obiektu, stosuje się metodę incrementAndGet(). Sama wartość zmiennej nie zmienia się w trakcie działania programu i zawsze wskazuje na ten sam obiekt, co pozwala nam na natychmiastowe zadeklarowanie zmiennej za pomocą słowa kluczowego final. Te same przykłady, ale z wyrażeniami lambda:
int counter = 0;
Runnable r = () -> counter++;
Nie kompiluje się z tego samego powodu, co opcja z klasą anonimową: counternie powinna się zmieniać podczas działania programu. Ale tak - wszystko jest w porządku:
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = () -> counter.incrementAndGet();
Dotyczy to również wywoływania metod. Wewnątrz wyrażenia lambda możesz nie tylko uzyskać dostęp do wszystkich „widocznych” zmiennych, ale także wywołać te metody, do których masz dostęp.
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> staticMethod();
        new Thread(runnable).start();
    }

    private static void staticMethod() {
        System.out.println("Я - метод staticMethod(), и меня только-что кто-то вызвал!");
    }
}
Chociaż metoda staticMethod()jest prywatna, można ją wywołać wewnątrz metody main(), zatem można ją także wywołać z wnętrza lambdy utworzonej w metodzie main.

Moment wykonania kodu wyrażenia lambda

To pytanie może wydawać Ci się zbyt proste, ale warto zadać sobie pytanie: kiedy zostanie wykonany kod zawarty w wyrażeniu lambda? W momencie stworzenia? Albo w momencie, kiedy (jeszcze nie wiadomo gdzie) zostanie wywołany? Całkiem łatwo to sprawdzić.
System.out.println("Запуск программы");

// много всякого разного kodа
// ...

System.out.println("Перед объявлением лямбды");

Runnable runnable = () -> System.out.println("Я - лямбда!");

System.out.println("После объявления лямбды");

// много всякого другого kodа
// ...

System.out.println("Перед передачей лямбды в тред");
new Thread(runnable).start();
Dane wyjściowe na wyświetlaczu:
Запуск программы
Перед объявлением лямбды
После объявления лямбды
Перед передачей лямбды в тред
Я - лямбда!
Widać, że kod wyrażenia lambda został wykonany na samym końcu, po utworzeniu wątku i dopiero wtedy, gdy proces wykonawczy programu doszedł do faktycznego wykonania metody run(). I wcale nie w momencie jego ogłoszenia. Deklarując wyrażenie lambda stworzyliśmy jedynie obiekt typu Runnablei opisaliśmy zachowanie jego metody run(). Sama metoda została wprowadzona znacznie później.

Odniesienia do metod?

Nie jest to bezpośrednio związane z samymi lambdami, ale myślę, że logiczne byłoby powiedzieć o tym kilka słów w tym artykule. Załóżmy, że mamy wyrażenie lambda, które nie robi nic specjalnego, a jedynie wywołuje jakąś metodę.
x -> System.out.println(x)
Podali mu coś х, a ono po prostu go przywołało System.out.println()i przekazało tam х. W takim przypadku możemy zastąpić go linkiem do potrzebnej nam metody. Lubię to:
System.out::println
Tak, bez nawiasów na końcu! Bardziej kompletny przykład:
List<String> strings = new LinkedList<>();
strings.add("Matka");
strings.add("mydło");
strings.add("rama");

strings.forEach(x -> System.out.println(x));
W ostatniej linii używamy metody forEach()akceptującej obiekt interfejsu Consumer. Jest to ponownie funkcjonalny interfejs z tylko jedną metodą void accept(T t). Odpowiednio piszemy wyrażenie lambda, które przyjmuje jeden parametr (ponieważ jest on wpisany w samym interfejsie, nie wskazujemy typu parametru, ale wskazujemy, że zostanie on wywołany х). W treści wyrażenia lambda wpisujemy kod która zostanie wykonana po wywołaniu metody.Tutaj accept()po prostu wyświetlamy na ekranie co znajduje się w zmiennej.Metoda хsama forEach()przechodzi przez wszystkie elementy kolekcji, wywołuje Consumermetodę przekazanego jej obiektu interfejsu (naszą lambdę) accept(), gdzie przekazuje każdy element z kolekcji.Jak już mówiłem, jest to wyrażenie lambda (po prostu wywołujące inną metodę), które możemy zastąpić referencją do potrzebnej nam metody.Wtedy nasz kod będzie wyglądał następująco:
List<String> strings = new LinkedList<>();
strings.add("Matka");
strings.add("mydło");
strings.add("rama");

strings.forEach(System.out::println);
Najważniejsze jest to, że przyjęte parametry metod (println()i accept()). Ponieważ metoda println()może przyjąć wszystko (jest przeciążona dla wszystkich prymitywów i dla dowolnych obiektów), zamiast wyrażenia lambda możemy przekazać forEach()tylko referencję do metody.Wtedy pobierze ona każdy element kolekcji i przekaże go bezpośrednio println()do forEach()metoda.Dla println()tych, którzy spotykają się z tym po raz pierwszy, uwaga. Należy pamiętać, że nie wywołujemy metody System.out.println()(z kropkami między wyrazami i nawiasami na końcu), ale przekazujemy referencję do samej metody.
strings.forEach(System.out.println());
będziemy mieli błąd kompilacji. Ponieważ przed wywołaniem forEach()Java zobaczy, że jest wywoływana System.out.println(), zrozumie, że jest zwracana voidi spróbuje voidprzekazać to do forEach()obiektu typu, który tam czeka Consumer.

Składnia korzystania z odwołań do metod

To całkiem proste:
  1. Przekazywanie referencji do metody statycznejNazwaКласса:: NazwaСтатическогоМетода?

    public class Main {
        public static void main(String[] args) {
            List<String> strings = new LinkedList<>();
            strings.add("Matka");
            strings.add("mydło");
            strings.add("rama");
    
            strings.forEach(Main::staticMethod);
        }
    
        private static void staticMethod(String s) {
            // do something
        }
    }
  2. Przekazywanie referencji do metody niestatycznej przy użyciu istniejącego obiektuNazwaПеременнойСОбъектом:: nazwa metody

    public class Main {
        public static void main(String[] args) {
            List<String> strings = new LinkedList<>();
            strings.add("Matka");
            strings.add("mydło");
            strings.add("rama");
    
            Main instance = new Main();
            strings.forEach(instance::nonStaticMethod);
        }
    
        private void nonStaticMethod(String s) {
            // do something
        }
    }
  3. Referencję do metody niestatycznej przekazujemy za pomocą klasy, w której taka metoda jest zaimplementowanaNazwaКласса:: nazwa metody

    public class Main {
        public static void main(String[] args) {
            List<User> users = new LinkedList<>();
            users.add(new User("Wasya"));
            users.add(new User("Коля"));
            users.add(new User("Петя"));
    
            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. Przekazanie linku do konstruktora NazwaКласса::new
    Korzystanie z metody links jest bardzo wygodne, gdy istnieje gotowa metoda, z której jesteś w pełni zadowolony i chciałbyś ją wykorzystać jako wywołanie zwrotne. W tym przypadku zamiast pisać wyrażenie lambda z kodem tej metody lub wyrażenie lambda, w którym po prostu wywołujemy tę metodę, po prostu przekazujemy do niej referencję. To wszystko.

Interesująca różnica między klasą anonimową a wyrażeniem lambda

W klasie anonimowej słowo kluczowe thiswskazuje obiekt tej klasy anonimowej. A jeśli użyjemy go thiswewnątrz lambdy, uzyskamy dostęp do obiektu klasy framingu. Gdzie faktycznie napisaliśmy to wyrażenie. Dzieje się tak, ponieważ wyrażenia lambda po skompilowaniu stają się prywatną metodą klasy, w której są zapisywane. Nie polecałbym korzystania z tej „funkcji”, ponieważ ma ona efekt uboczny, który jest sprzeczny z zasadami programowania funkcjonalnego. Ale to podejście jest całkiem spójne z OOP. ;)

Skąd zaczerpnąłem informacje i co jeszcze warto przeczytać

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION