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 counter
musi 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 counter
jest 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ą: counter
nie 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 Runnable
i 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 Consumer
metodę 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 void
i spróbuje void
przekazać to do forEach()
obiektu typu, który tam czeka Consumer
.
Składnia korzystania z odwołań do metod
To całkiem proste:-
Przekazywanie referencji do metody statycznej
NazwaКласса:: 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 } }
-
Przekazywanie referencji do metody niestatycznej przy użyciu istniejącego obiektu
NazwaПеременнойСОбъектом:: 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 } }
-
Referencję do metody niestatycznej przekazujemy za pomocą klasy, w której taka metoda jest zaimplementowana
NazwaКласса:: 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); } } }
-
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 kluczowethis
wskazuje obiekt tej klasy anonimowej. A jeśli użyjemy go this
wewną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ć
- Tutorial na oficjalnej stronie Oracle . Dużo, szczegółowo, z przykładami, ale po angielsku.
- Ten sam samouczek „Oracle” , ale rozdział dotyczy odwołań do metod.
- Artykuł na Habré o programowaniu funkcjonalnym (tłumaczenie innego artykułu). Istnieje bardzo niewiele multiksiążek na temat lambd, ponieważ dotyczą one ogólnie programowania funkcjonalnego.
- Dla tych, którzy lubią utknąć w Wikipedii .
- I oczywiście znalazłem mnóstwo rzeczy w Google :)
GO TO FULL VERSION