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ę klasy T zwracając instancję klasy R,
  • Consumer<T> zawiera metodę accept, która przyjmuje instancję klasy T,
  • Predicate<T> zawiera metodę test, która przyjmuje instancję klasy T i zwraca flagę. Interfejs ten może posłużyć do zastąpienia interfejsu Checker,
  • Supplier<T> zawiera metodę get, która nie przyjmuje żadnych parametrów i zwraca instancję klasy T,
  • UnaryOperator<T> jest specyficznym przypadkiem interfejsu Function. 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.

  1. 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.
  2. Napisz program, który wywoła funkcję equals na instancji klasy Object używając mechanizmu odwoływania się do metody (przy pomocy ::).
  3. 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:

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!

  1. 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. 

  2. 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ąć. 

  3. Oczywiście w trakcie kompilacji typ jest znany, ale nie jest jawnie podany w kodzie źródłowym. 

  4. Efektywnie abstrakcyjną, czyli dodanie do interfejsu np. metody equals, która jest w klasie Object nadal spełnia to wymaganie. 

  5. 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.

Kategorie:

Ostatnia aktualizacja:

Autor: Marcin Pietraszek


Nie popełnia błędów tylko ten, kto nic nie robi ;). Bardzo możliwe, że znajdziesz błąd, literówkę, coś co wymaga poprawy. Jeśli chcesz możesz samodzielnie poprawić tę stronę. Jeśli nie chcesz poprawiać błędu, który udało Ci się znaleźć będę wdzięczny jeśli go zgłosisz. Z góry dziękuję!

Zostaw komentarz