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.
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 counter
zmienna jest referencją do AtomicInteger
obiektu. 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ą: counter
nie 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 main
metodzie.
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 Runnable
obiekt 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 x
i 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 Consumer
interfejs. 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 x
zmiennej. Ta sama forEach()
metoda wykonuje iterację przez wszystkie elementy w kolekcji i wywołuje accept()
metodę w implementacjiConsumer
interface (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 void
i spróbuje przekazać void
do forEach()
, która zamiast tego oczekuje Consumer
obiektu.
Składnia odwołań do metod
to całkiem proste:-
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 } }
-
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 } }
-
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); } } }
-
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 anonimowejthis
sł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ć?
- Samouczek na oficjalnej stronie Oracle. Dużo szczegółowych informacji, w tym przykłady.
- Rozdział dotyczący odwołań do metod w tym samym samouczku Oracle.
- Daj się wciągnąć w Wikipedię , jeśli jesteś naprawdę ciekawy.
GO TO FULL VERSION