Artykuł ten dotyczy bardziej zaawansowanego fragmentu składni języka Java. Z tego powodu aby móc w pełni skorzystać z artykułu warto zapoznać się z wcześniejszymi artykułami, które dotyczą:
Czym jest wyrażenie lambda
Dla uproszczenia można powiedzieć, że wyrażenie lambda jest metodą1. Metodą, którą możesz przypisać do zmiennej. Możesz ją także wywołać czy przekazać jako argument do innej metody.
Wyrażenia lambda możesz także porównać do klas anonimowych2. Mają one jednak dużo bardziej czytelną i zwięzłą składnię.
Na przykład wyrażenie lambda, które podnosi do kwadratu przekazaną liczbę wygląda następująco:
x -> x * x
Składnia wyrażeń lambda
Wyrażenie lambda ma następującą składnię
<lista parametrów> -> <ciało wyrażenia>
Lista parametrów
Lista parametrów zawiera wszystkie parametry przekazane do “ciała” wyrażenia lambda. W szczególności lista ta może być pusta. Wyrażenie lambda poniżej nie przyjmuje żadnych argumentów, zwraca natomiast instancję klasy String
:
() -> “some return value”
Podawanie typów parametrów jest opcjonalne. Kompilator jest w stanie poznać te parametry z kontekstu w którym znajduje się dane wyrażenie lambda. Jeśli chcesz możesz je także podać:
(Integer x, Long y) -> System.out.println(x * y)
Nawiasy otaczające listę parametrów są opcjonalne jeśli wyrażenie ma wyłącznie jeden parametr bez określonego typu3.
Ciało wyrażenia lambda
W ogromnej większości przypadków wyrażenia lambda zawierają jedną linijkę kodu:
x -> x * x
() -> “some return value”
(Integer x, Long y) -> System.out.println(x * y);
Może się jednak zdarzyć, że Twoje wyrażenie lambda będzie zawierało więcej linii. W takim przypadku musisz otoczyć je nawiasami {}
jak w przykładzie poniżej:
x -> {
if (x != null && x % 2 == 0) {
return (long) x * x;
}
else {
return 123L;
}
}
Można sobie wyobrazić wyrażenie lambda, które nie przyjmuje żadnych parametrów i nie zwraca żadnych wartości. Najprostsza wersja takiego wyrażenia wygląda następująco:
() -> {}
Od klasy anonimowej do wyrażenia lambda
Wiesz już czym jest klasa anonimowa. Dla przypomnienia powiem, że jest to stworzenie jedynej instancji klasy w miejscu jej użycia. Wiesz już też jak wyglądają wyrażenia lambda. Teraz nadszedł czas na zamianę klasy anonimowej na wyrażenie lambda. Proszę spójrz na przykład poniżej:
public interface Checker<T> {
boolean check(T object);
}
Checker<Integer> isOddAnonymous = new Checker<Integer>() {
@Override
public boolean check(Integer object) {
return object % 2 != 0;
}
};
System.out.println(isOddAnonymous.check(123));
System.out.println(isOddAnonymous.check(124));
W przykładzie tym zdefiniowałem interfejs Checker
, który posiada jedną metodę check
. Metoda ta zwraca wartość logiczną na podstawie przekazanego argumentu.
Fragment kodu robiący to samo jednak przy użyciu składni wyrażeń lambda wygląda następująco:
Checker<Integer> isOddLambda = object -> object % 2 != 0;
System.out.println(isOddLambda.check(123));
System.out.println(isOddLambda.check(124));
Prawda, że ładniej :)?
Dochodzimy teraz do momentu, w którym muszę Ci powiedzieć o typach w wyrażeniach lambda. Każde wyrażenie lambda jest instancją dowolnego interfejsu funkcyjnego. Jest to bardzo ważne, dlatego też musisz dokładnie wiedzieć czym jest interfejs funkcyjny.
Pobierz opracowania zadań z rozmów kwalifikacyjnych
Przygotowałem rozwiązania kilku zadań algorytmicznych z rozmów kwalifikacyjnych. Rozkładam je na czynniki pierwsze i pokazuję różne sposoby ich rozwiązania. Dołącz do grupy ponad 6147 Samouków, którzy jako pierwsi dowiadują się o nowych treściach na blogu, a prześlę je na Twój e-mail.
Interfejs funkcyjny
Interfejs funkcyjny to interfejs, który ma jedną abstrakcyjną metodę4. Wprowadzono adnotację @FunctionalInterface
, którą możesz dodać do interfejsów tego typu.
Adnotacja ta zapewnia, że kompilator upewni się, że dany interfejs jest interfejsem funkcyjnym. Jeśli nie, wówczas kompilacja się nie powiedzie.
Przykładem interfejsu funkcyjnego może być zdefiniowany wcześniej interfejs Checker
.
@FunctionalInterface
public interface Checker<T> {
boolean check(T object);
}
Zawiera on wyłącznie jedną metodę check
.
Przykładowe interfejsy funkcyjne
Twórcy języka Java przygotowali zestaw interfejsów funkcyjnych, które możesz implementować. W większości przypadków w zupełności wystarczy ich użycie. Część z nich znajduje się w pakiecie java.util.function
. Najważniejsze z nich zebrałem poniżej:
Function<T, R>
zawiera metodęapply
, która przyjmuje instancję klasyT
zwracając instancję klasyR
,Consumer<T>
zawiera metodęaccept
, która przyjmuje instancję klasyT
,Predicate<T>
zawiera metodętest
, która przyjmuje instancję klasy T i zwraca flagę. Interfejs ten może posłużyć do zastąpienia interfejsuChecker
,Supplier<T>
zawiera metodęget
, która nie przyjmuje żadnych parametrów i zwraca instancję klasyT
,UnaryOperator<T>
jest specyficznym przypadkiem interfejsuFunction
. W tym przypadku typ argumentu i typ zwracany są te same.
Wyrażenia lambda zdefiniowane na początku artykułu można przypisać do tych właśnie interfejsów:
UnaryOperator<Integer> square = x -> x * x;
Supplier<String> someString = () -> "some return value";
BiConsumer<Integer, Long> multiplier = (Integer x, Long y) -> System.out.println(x * y);
Function<Integer, Long> multiline = x -> {
if (x != null && x % 2 == 0) {
return (long) x * x;
}
else {
return 123L;
}
};
Zalety stosowania wyrażeń lambda
Wyrażenia lambda są bardzo pomocne przy operacji na kolekcjach. Są niezastąpione także przy pracy ze strumieniami. Pozwalają także na pisanie w Javie w sposób “funkcyjny”5.
Oczywistą zaletą wyrażeń lambda jest ich zwięzłość. Kod zajmuje o wiele mniej miejsca, staje się przez to bardziej czytelny.
Odwoływanie się do metod
Wraz z wyrażeniami lambda Java została rozbudowana o składnię pozwalającą na odwoływanie się do metod. Służy do tego ::
. Dzięki temu wyrażeniu możemy przypisać metodę do zmiennej bez jej wywołania. Takie podejście pozwala na przekazanie tak wyłuskanej metody i wywołanie jej w zupełnie innym miejscu. Proszę spójrz na przykład poniżej:
Object objectInstance = new Object();
IntSupplier equalsMethodOnObject = objectInstance::hashCode;
System.out.println(equalsMethodOnObject.getAsInt());
W przykładzie tym tworzę nową instancję klasy Object
. Następnie pobieram metodę hashCode
z tego obiektu i przypisuję ją do typu IntSupplier
. Jest to kolejny interfejs funkcyjny znajdujący się w standardowej bibliotece. Ostatnia linijka to wywołanie metody znajdującej się w tym interfejsie.
Kod powyżej można porównać do:
Object objectInstance = new Object();
System.out.println(objectInstance.hashCode());
W obu przypadkach tworzę nowy obiekt klasy Object
i wywołują na nim metodę hashCode
.
Odwoływanie się do metod bez podania instancji
Można także odwołać się do metody bez podania instancji, na której metoda powinna być wywołana. Wówczas ta instancja musi być przekazana jako pierwszy argument. Przykład poniżej powinien pomóc zrozumieć to zastosowanie:
ToIntFunction<Object> hashCodeMethodOnClass = Object::hashCode;
Object objectInstance = new Object();
System.out.println(hashCodeMethodOnClass.applyAsInt(objectInstance));
W odróżnieniu do poprzedniego przykładu tutaj na początku pobieram metodę. Tym razem metoda nie jest przypisana do instancji. W związku z tym wyrażenie lambda jest już innego typu. W takim przypadku zawsze pierwszym argumentem jest instancja na której metoda powinna być wywołana. W kolejnej linijce tworzę instancję klasy Object
. Ostatnia linijka to wywołanie metody na tej instancji.
Kod bez użycia odwołania do metody robiący dokładnie to samo wygląda trochę mniej skomplikowanie:
Object objectInstance = new Object();
System.out.println(objectInstance.hashCode());
Odwoływanie się do konstruktora
Notacja z ::
może być także użyta do odwołania się do konstruktora. W tym przypadku należy użyć ::
wraz ze słowem kluczowym new
. Proszę spójrz na przykład poniżej:
Supplier<Object> objectCreator = Object::new;
System.out.println(objectCreator.get());
W pierwszej linijce przykładu przypisuje konstruktor klasy Object
do zmiennej objectCreator
. Kolejna linijka to wywołanie konstruktora.
To samo bez użycia referencji metody możesz uzyskać w dobrze Ci znany sposób:
System.out.println(new Object());
Przykład zastosowania wyrażeń lambda i odwołania do metody
Załóżmy, że chcemy wypisać na konsoli liczby znajdujące się w kolekcji. Możemy to zrobić przy pomocy standardowej pętli, którą już znasz:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
for (Integer number : numbers) {
System.out.println(number);
}
To samo zadanie można także zrobić przy pomocy wyrażeń lambda:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
Consumer<Integer> integerConsumer = n -> System.out.println(n);
numbers.forEach(integerConsumer);
Pierwsza linijka to utworzenie listy z liczbami. Kolejna jest bardziej ciekawa, zawiera wyrażenie lambda, które konsumuje liczbę wypisując ją na konsoli. Ostatnia to wywołanie metody forEach
wraz z wyrażeniem lambda. Wyrażenie to zostanie wywołane dla każdego elementu.
Kod ten można jeszcze bardziej skrócić używając mechanizmu odwoływania się do metod:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
numbers.forEach(System.out::println);
Efekt działania wszystkich trzech fragmentów jest dokładnie taki sam. Różnią się między sobą sposobem rozwiązania danego problemu.
Zadania
Na koniec mam dla Ciebie kilka zadań, które pomogą przećwiczyć Ci wiedzę z tego artykułu.
- Napisz program, który pobierze o użytkownika cztery łańcuchy znaków, które umieścisz w liście. Następnie posortuj tę listę używając metody
List.sort
. Użyj wyrażenia lambda, które posortuje łańcuchy znaków malejąco po długości. - Napisz program, który wywoła funkcję
equals
na instancji klasyObject
używając mechanizmu odwoływania się do metody (przy pomocy::
). - Utwórz instancję klasy
Human
przy pomocy mechanizmu odwoływania się do konstruktora (przy pomocy::
).
public class Human {
private int age;
private String name;
public Human(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
}
Jeśli będziesz miał problem z rozwiązaniem zadań możesz rzucić okiem na przykładowe rozwiązania, które umieściłem na samouczkowym githubie.
Dodatkowe materiały do nauki
Przygotowałem dla Ciebie zestaw kilku linków z materiałami dodatkowymi:
- Wprowadzenie do wyrażeń lambda na stronie Oracle,
- Tutorial dotyczący wyrażeń lambda na stronie Oracle,
- Opis interfejsów funkcyjnych w JLS,
- Referencja do metody w JLS,
- Wyrażenia lambda w JLS.
Podsumowanie
Wyrażenia lambda nie są proste. Mogą powodować sporo zakłopotania, szczególnie na początku. Jeśli jednak się do nich przyzwyczaisz pisanie kodu z ich udziałem będzie sprawiało Ci sporo frajdy :). Po pewnym czasie docenisz też zwięzłość wyrażeń lambda.
Po przeczytaniu artykułu wiesz czym są wyrażenia lambda i jak je stosować. Znasz też mechanizm odwoływania się do metod. Przećwiczyłeś te mechanizmy rozwiązując przykładowe zadania. Nie zapomnij pochwalić się w komentarzu gdzie ostatnio użyłeś wyrażeń lambda :).
Na koniec mam do Ciebie prośbę. Jeśli uważasz, że artykuł ten był dla Ciebie pomocny proszę podziel się nim ze swoimi znajomymi. Zależy mi na dotarciu do jak największej grupy czytelników a Ty możesz mi w tym pomóc. Jeśli nie chcesz pominąć żadnego nowego artykułu dopisz się do samouczkowego newslettera i polub samouczka na Facebooku. Do następnego razu!
-
Nie jest to do końca prawda, na przykład wyrażenie lambda nie wprowadza nowego zakresu zmiennych, ale takie uproszczenie pomoże zrozumieć działanie wyrażeń lambda. ↩
-
Podobnie jak przy poprzednim porównaniu, są różnice pomiędzy wyrażeniami lambda i klasami anonimowymi. Jednak na potrzeby tego wprowadzenia możemy je pominąć. ↩
-
Oczywiście w trakcie kompilacji typ jest znany, ale nie jest jawnie podany w kodzie źródłowym. ↩
-
Efektywnie abstrakcyjną, czyli dodanie do interfejsu np. metody equals, która jest w klasie Object nadal spełnia to wymaganie. ↩
-
Oczywiście Java nie jest językiem w pełni funkcyjnym, jednak taka namiastka jest przydatna. ↩
Pobierz opracowania zadań z rozmów kwalifikacyjnych
Przygotowałem rozwiązania kilku zadań algorytmicznych z rozmów kwalifikacyjnych. Rozkładam je na czynniki pierwsze i pokazuję różne sposoby ich rozwiązania. Dołącz do grupy ponad 6147 Samouków, którzy jako pierwsi dowiadują się o nowych treściach na blogu, a prześlę je na Twój e-mail.
Zostaw komentarz