W tym artykule postaram się przybliżyć zagadnienie wyrażeń Lambda dostępnych w języku Java. Wyrażenia Lambda skracają zapis kodu, tym samym czyniąc go bardziej czytelnym. Może się to wydawać niewielką zaletą i cały szum w około, tego elementu języka Java jest na wyrost. Na szczęście poprawa czytelności kodu jest bardzo duża. W telegraficznym skrócie, zamiast tworzyć nową klasę Java (nowy plik), która składa się z ośmiu linijek kodu, możemy utworzyć jedno linijkowe wyrażenie Lambda, które zrobi, to samo, co oddzielna klasa, dodatkowo całość kodu mamy w jednym miejscu – brak potrzeby nawigowania po klasach. Wyrażenia Lambda swój pełen potencjał pokazują w połączeniu z Java Streams.

Artykuł podzielę na dwie sekcje, każda z sekcji będzie składać się z trzech kroków pokazujących przejście, przeobrażenie się kodu do wyrażeń Lambda. Na sam koniec prezentuję praktyczne zastosowanie wyrażeń Lambda z Java Streams – Processing Data with Java SE 8 Streams. W momencie kiedy znamy już wyrażenia Lambda nie musimy przechodzić przez cały proces, krok po kroku, wystarczy ostatnia forma z punktu Proste wyrażenie Lambda – anonimowa implementacja metody interfejsu. Tworzenie wyrażeń Lambda w krokach opisane w tym artykule są w celach edukacyjnych.

Zamiast tworzyć nową klasę Java (nowy plik), która składa się z ośmiu linijek kodu (plus klasa z metodą main(), do uruchomienia przykładu) …

public class RetroJukebox implements Jukebox {
    @Override
    public void makeNoise() {
        System.out.println("Retro Make Some Noise!");
    }
}

public class LambdaStepByStepMain {
    public static void main(String[] args) {
        Jukebox retroJukebox = new RetroJukebox();
        retroJukebox.makeNoise();
    }
}

… możemy utworzyć jedno linijkowe wyrażenie Lambda (plus klasa z metodą main(), do uruchomienia przykładu).

public class LambdaStepByStepMain {
    public static void main(String[] args) {
        Jukebox lambdaJukebox = 
            () -> System.out.println("Lambda Make Some Noise!");
        lambdaJukebox.makeNoise();
    }
}

Tworzenie wyrażeń Lambda odbywa się przy użyciu interfejsu funkcyjnego. Jest, to interfejs dobrze znany z języka Java. Interfejs funkcyjny, musi posiadać jedną i tylko jedną metodę, która musi być publiczna i abstrakcyjna. Domyślnie, metody w interfejsach są publiczne i abstrakcyjne, stąd brak słów kluczowych przy metodzie – public abstract void makeNoise().

Więcej informacji o interfejsach można znaleźć tutaj Interfaces (The Java™ Tutorials > Learning the Java Language > Interfaces and Inheritance), informacje o interfejsie funkcyjnym tutaj FunctionalInterface (Java Platform SE 8). Interfejs funkcyjny jest „normalnym” interfejsem Java, który ma pewne ograniczenia.

Oficjalna dokumentacja Oracle Java opisuje wyrażania Lambda w następujący sposób „Lambda expressions let you express instances of single-method classes more compactly.”. Moja własna interpretacja jest następująca, „lambda, to anonimowa implementacja metody”.

Dla uproszczenia, działanie kodu zaprezentuję przy użyciu metody main(). Właściwym podejściem było by utworzenie testu jednostkowego, który weryfikuje poprawność działania napisanego kodu. Poniżej kod z metodą main().

public class LambdaStepByStepMain {
    public static void main(String[] args) {
        // TODO: Implementacja wyrażenia Lambda, krok po kroku ...
        // Sekcja #1 ...
        // Sekcja #2 ...
    }
}

Sekcja #1 – prosta metoda dla wyrażeń Lambda

Dla tego przykładu przyjmuję, że prosta metoda, to taka która nie przyjmuje parametrów oraz nic nie zwraca.

Poniżej kod interfejsu funkcyjnego Jukebox z metodą void makeNoise(), który będzie używany do zaprezentowania wyrażeń Lambda. Jak widać metoda makeNoise() nie przyjmuje parametrów – puste nawiasy () – oraz nie zwraca wartości – słowo kluczowe void.

@FunctionalInterface
public interface Jukebox {
    void makeNoise();
}

Korzystanie z interfejsu, również funkcyjnego, wymaga jego implementacji. Poniżej zaprezentuję kilka sposobów implementacji interfejsu. Przejście do utworzenia wyrażenia Lambda, czyli anonimowej implementacji metody, rozpocznę od kilku „kroków wstecz”, tak, aby dokładnie wyjaśnić, dlaczego nazywam, to anonimową implementacją metody.

Kroki, które pozwolą zrozumieć proces przejścia do wyrażenia Lambda:

  1. Klasyczna implementacja interfejsu z wykorzystaniem oddzielnej klasy.
  2. Anonimowa implementacja interfejsu z wykorzystaniem anonimowej klasy.
  3. Proste wyrażenie Lambda – anonimowa implementacja metody interfejsu.

Warto zapoznać się z oficjalną dokumentacją Oracle Java – When to Use Nested Classes, Local Classes, Anonymous Classes, and Lambda Expressions, aby w pełni zrozumieć kroki, które opisuję poniżej.

Klasyczna implementacja interfejsu z wykorzystaniem oddzielnej klasy

Poniżej opisany jest pierwszy krok do zrozumienia wyrażeń Lambda, czyli klasyczna implementacja interfejsu z wykorzystaniem oddzielnej klasy.

public class RetroJukebox implements Jukebox {
    @Override
    public void makeNoise() {
        System.out.println("Retro Make Some Noise!");
    }
}

Taki sposób implementacji interfejsu oraz jego metody wymaga stworzenia dodatkowej, oddzielnej klasy, pliku z kodem Java. Następnie dla nowej klasy trzeba będzie stworzyć obiekt, wywołać odpowiednią metodę, co prezentuje poniższy kod.

public class LambdaStepByStepMain {
    public static void main(String[] args) {
        Jukebox retroJukebox = new RetroJukebox();
        retroJukebox.makeNoise();
    }
}
Retro Make Some Noise!

Process finished with exit code 0

Wynik działania kodu z metody main() z klasy LambdaStepByStepMain.

Jak widać powyższe rozwiązanie wymaga sporo kodu, który jest „rozrzucony” w dwóch różnych klasach, RetroJukeBox oraz LambdaStepByStepMain. Tak wygląda klasyczne podejście do pisania kodu w języku Java. Wyrażenia Lambda zmieniają, to podejście.


Anonimowa implementacja interfejsu z wykorzystaniem anonimowej klasy

Poniżej opisany jest drugi krok do zrozumienia wyrażeń Lambda, czyli anonimowa implementacja interfejsu z wykorzystaniem anonimowej klasy.

public class LambdaStepByStepMain {
    public static void main(String[] args) {
        Jukebox anonymousJukebox = new Jukebox() {
            @Override
            public void makeNoise() {
                System.out.println("Anonymous Make Some Noise!");
            }
        };
        anonymousJukebox.makeNoise();
    }
}

Powyższy sposób implementacji nie wymaga oddzielnej klasy, zastępuje ją anonimowa implementacja interfejsu, która wykorzystuje klasę anonimową z implementacją metody makeNoise(), a następnie tworzy dla niej obiekt – Jukebox anonymousJukebox = new Jukebox() {}. Ponownie wywołana zostaje odpowiednia metoda.

Ten fragment kodu można rozbić na kilka elementów, aby lepiej go zrozumieć:

  1. Deklaracja zmiennej anonymousJukebox, typu Jukebx (interfejs funkcyjny), do której przypisujemy wartość – fragment Jukebox anonymousJukebox =.
  2. Anonimowa klasa, która implementuje metodę makeNoise() interfejsu Jukebox – poniższy fragment kodu.
new Jukebox() {
    @Override
    public void makeNoise() {
        System.out.println("Anonymous Make Some Noise!");
    }
};

Więcej informacji o anonimowych klasach można znaleźć tutaj Anonymous Classes (The Java™ Tutorials > Learning the Java Language > Classes and Objects).

Anonymous Make Some Noise!

Process finished with exit code 0

Wynik działania kodu z metody main() z klasy LambdaStepByStepMain.


Proste wyrażenie Lambda – anonimowa implementacja metody interfejsu

Poniżej opisany jest trzeci krok do zrozumienia wyrażeń Lambda, który prezentuje wyrażenia Lambda we własnej osobie.

public class LambdaStepByStepMain {
    public static void main(String[] args) {
        Jukebox lambdaJukebox = 
            () -> System.out.println("Lambda Make Some Noise!");
        lambdaJukebox.makeNoise();
    }
}

Anonimowa implementacja metody z wykorzystaniem wyrażenia Lambda – zamiast tworzyć dodatkową, oddzielną klasę (plik z kodem Java) jak w Ad. 1. Klasyczna implementacja interfejsu z wykorzystaniem oddzielnej klasy lub anonimową klasę implementującą interfejs jak w Ad. 2. Anonimowa implementacja interfejsu z wykorzystaniem anonimowej klasy.

Ten fragment kodu trzeba rozbić na kilka elementów, aby lepiej zrozumieć wyrażenia Lambda: Jukebox lambdaJukebox = () -> System.out.println(„Lambda Make Some Noise!”);

  1. Deklaracja zmiennej lambdaJukebox, typu Jukebx (interfejs funkcyjny), do której przypisujemy wartość – fragment Jukebox lambdaJukebox =.
  2. Anonimowa implementacja metody z wykorzystaniem wyrażenia Lambda – fragment () -> System.out.println(„Lambda Make Some Noise!”);
@FunctionalInterface
public interface Jukebox {
    void makeNoise();
}

Jukebox lambdaJukebox = 
    () -> System.out.println("Lambda Make Some Noise!");
Lambda Make Some Noise!

Process finished with exit code 0

Wynik działania kodu z metody main() z klasy LambdaStepByStepMain.


public class LambdaStepByStepMain {
    public static void main(String[] args) {
        // Ad. 1. Klasyczna implementacja interfejsu z wykorzystaniem oddzielnej klasy
        Jukebox retroJukebox = new RetroJukebox();
        retroJukebox.makeNoise();

        // Ad. 2. Anonimowa implementacja interfejsu z wykorzystaniem anonimowej klasy
        Jukebox anonymousJukebox = new Jukebox() {
            @Override
            public void makeNoise() {
                System.out.println("Anonymous Make Some Noise!");
            }
        };
        anonymousJukebox.makeNoise();

        // Ad. 3. Proste wyrażenie Lambda - anonimowa implementacja metody interfejsu
        Jukebox lambdaJukebox = () -> System.out.println("Lambda Make Some Noise!");
        lambdaJukebox.makeNoise();
    }
}

Powyższy kod prezentuje wszystkie kroki w jednym miejscu, to cały proces przejścia do wyrażeń Lambda w jednej metodzie main().

Retro Make Some Noise!
Anonymous Make Some Noise!
Lambda Make Some Noise!

Process finished with exit code 0

Wynik działania kodu z metody main() z klasy LambdaStepByStepMain dla wszystkich kroków prowadzących do wyjaśnienia wyrażenia Lambda – dla Sekcja #1 – prosta metoda dla wyrażeń Lambda.


Poniżej prezentuję w jednym miejscu wszystkie kroki. Jest, to cały proces przejścia do wyrażeń Lambda. Oczywiście pisząc kod wyrażeń Lambda nie musimy przechodzić przez cały proces, krok po kroku, wystarczy ostatnia forma Ad. 3. Proste wyrażenie Lambda – anonimowa implementacja metody interfejsu Tworzenie wyrażeń Lambda w krokach opisane w tym artykule są w celach edukacyjnych.

public class RetroJukebox implements Jukebox {
    @Override
    public void makeNoise() {
        System.out.println("Retro Make Some Noise!");
    }
}
Jukebox anonymousJukebox = new Jukebox() {
    @Override
    public void makeNoise() {
        System.out.println("Anonymous Make Some Noise!");
    }
};
Jukebox lambdaJukebox = 
    () -> System.out.println("Lambda Make Some Noise!");

W powyższym kodzie widać, jak dla metody public void makeNoise() „znikają zbędne elementy”, które kompilator Java dobrze zna, bo opisaliśmy je w interfejsie Jukebox. Co rozumiem przez znikające zbędne elementy?

  • Zbędne elementy dla wyrażenia Lambda, to: sama deklaracja metody public void makeNoise oraz ciało metody zawarte między nawiasami klamrowymi {System.out.println("Anonymous Make Some Noise!");}.
  • Niezbędne elementy dla wyrażania Lambda, to: parametry metody zawarte między nawiasami () oraz ciało metody umieszczone po „strzałce” -> System.out.println("Anonymous Make Some Noise!");.
 public void makeNoise() {
    System.out.println("Anonymous Make Some Noise!");
 }
() -> System.out.println("Lambda Make Some Noise!"); 

Więcej informacji o metodach można znaleźć tutaj Defining Methods (The Java™ Tutorials > Learning the Java Language > Classes and Objects).


Sekcja #2 – metoda złożona dla wyrażeń Lambda

Dla tego przykładu przyjmuję, że metoda złożona, to taka, która przyjmuje parametr oraz zwraca wartość.

Poniżej kod interfejsu funkcyjnego Printer z metodą String printText(String text), który będzie używany do zaprezentowania wyrażeń Lambda. Jak widać metoda przyjmuje parametr 'text’ oraz zwraca wartość typu 'String’.

@FunctionalInterface
public interface Printer {
    String printText(String text);
}

Posiadając wiedzę z sekcji #1 pozwolę sobie zaprezentować tylko cały proces przejścia do wyrażeń Lambda, zawierający wszystkie kroki w jednym miejscu.

public class LocalPrinter implements Printer{
    @Override
    public String printText(String text) {
        return "Local printer is printing: " + text;
    }
}
Printer anonymousPrinter = new Printer() {
    @Override
    public String printText(String text) {
        return "Anonymous printer is printing: " + text;
    }
};
// Lambda - pełna wersja zapisu.
Printer fullLambdaPrinter = (text) -> {
    return "Printing some text: " + text;
};
// Lambda - skrócona wersja zapisu.
Printer shortLambdaPrinter = 
    text -> "Printing some text: " + text;

Wynik działania kodu z metody main() z klasy LambdaStepByStepMain dla wszystkich kroków prowadzących do wyjaśnienia wyrażenia Lambda – dla Sekcja #2 – metoda złożona dla wyrażeń Lambda.

public class LambdaStepByStepMain {
    public static void main(String[] args) {
        // Sekcja #2

        // #1 Separate Class for Interface method 'makeNoise()' implementation.
        Printer homePrinter = new LocalPrinter();
        String homePrinterText = homePrinter.printText("Local Home Printer example");
        System.out.println(homePrinterText);


        // #2 Anonymous Interface implementation with method 'printText()'.
        Printer anonymousPrinter = new Printer() {
            @Override
            public String printText(String text) {
                return "Anonymous printer is printing: " + text;
            }
        };
        String anonymousPrinterText = 
            anonymousPrinter.printText("Anonymous Printer example");
        System.out.println(anonymousPrinterText);

        // #3 Lambda Anonymous method 'printText()' Implementation.
        // Lambda expression with input parameter and return value.
        // Lambda full version.
        Printer fullLambdaPrinter = (text) -> {
            return "Printing some text: " + text;
        };
        String fullLambdaText = fullLambdaPrinter.printText("Full Lambda example");
        System.out.println(fullLambdaText);

        // Lambda expression with input parameter and return value.
        // Lambda short version.
        Printer shortLambdaPrinter = text -> "Printing some text: " + text;
        String shortLambdaText = shortLambdaPrinter.printText("Short Lambda example");
        System.out.println(shortLambdaText);
}
Local printer is printing: Local Home Printer example
Anonymous printer is printing: Anonymous Printer example
Printing some text: Full Lambda example
Printing some text: Short Lambda example

Process finished with exit code 0

Wynik działania kodu z metody main() z klasy LambdaStepByStepMain dla wszystkich kroków prowadzących do wyjaśnienia wyrażenia Lambda – dla sekcji #2.


Praktyczne zastosowanie wyrażeń Lambda z Java Streams

Poniżej zaprezentuję niewielki przykład praktycznego wykorzystania wyrażeń Lambda w Java Streams, pomimo, że w tym artykule nie opiszę szczegółów związanych z Java Streams. Poniższy przykład pokaże moc wyrażeń Lambda, która prowadzi do przejrzystego i eleganckiego kodu, którego logika działania jest dostępna od razu w jednym miejscu, co prowadzi do poprawy czytelności kodu.

public class LambdaStreamsMain {
    public static void main(String[] args) {
        List<Person> people = Arrays.asList(
                new Person("John", 29, Person.Gender.MALE),
                new Person("Christina", 19, Person.Gender.FEMALE),
                new Person("Max", 33, Person.Gender.MALE)
        );

        // Using Lambda in short form ...
        List<String> collectedPeopleNamesShortLambda = people.stream()
                .filter(person -> person.getGender().equals(Person.Gender.MALE))
                .map(Person::getName)
                .collect(Collectors.toList());

        System.out.println("All male names (short Lambda): " +
            collectedPeopleNamesShortLambda);

        // Using Lambda in full form ...
        List<String> collectedPeopleNamesFullLambda = people.stream()
                .filter((person) -> person.getGender().equals(Person.Gender.MALE))
                .map((person) -> person.getName())
                .collect(Collectors.toList());

        System.out.println("All male names (full Lambda): " +
            collectedPeopleNamesFullLambda);
}
All male names: [John, Max]

Process finished with exit code 0

Powyżej wynik działania kodu z metody main() z klasy LambdaStreamsMain pokazującej użycie wyrażeń Lambda dla Java Streams.

Poniżej kod z czasów Java 1.5 (od grudnia 2006 r.), który nie posiadał wyrażeń Lambda oraz Java Streams …

List<String> peopleMaleNames = new ArrayList<>();
for (Person person : people) {
    if (person.getGender().equals(Person.Gender.MALE)) {
        peopleMaleNames.add(person.getName());
    }
}

… po ośmiu latach w Java 1.8 (od marca 2014 r.) wprowadzono wyrażenia Lambda – Wikipedia: Java version history.

List<String> collectedPeopleNamesShortLambda = people.stream()
    .filter(person ->
        person.getGender().equals(Person.Gender.MALE))
    .map(Person::getName)
    .collect(Collectors.toList());

Podsumowując powyższe dwie sekcje oraz kroki w nich zawarte, widać wyraźnie, że wyrażenie Lambda jest bardzo krótkie i zwięzłe, a dodatkowo kod implementacji samej metody makeNoise() jest od razu widoczny. To, że wyrażenia Lambda są krótkie i zwięzłe sprawia, że są bardzo użyteczne w Java Streams, które operują na Java Collections.

Zdjęcie autorstwa Mitchell Luo z Pexels.