In this article I will try to explain the issue of Lambda expressions available in Java. Lambda expressions shorten the code, making it more readable. This may seem like a small advantage and all the hype surrounding this element of the Java language is exaggerated. Fortunately, the improvement in code readability is very large. In short, instead of creating a new Java class (new file) that consists of eight lines of code, we can create a one-line Lambda expression that will do the same as a separate class, plus we have all the code in one place - no need to navigate after classes. Lambda expressions show their full potential when combined with Java Streams.
I will divide the article into two sections, each section will consist of three steps showing the transition and transformation of the code into Lambda expressions. Finally, I present a practical application of Lambda expressions with Java Streams - Processing Data with Java SE 8 Streams. When we already know the Lambda expressions, we do not have to go through the entire process, step by step, the last form from point Simple Lambda expression - anonymous implementation of an interface method. Creating Lambda expressions in the steps described in this article are for educational purposes.
Instead of creating a new Java class (new file) that consists of eight lines of code (plus a class with a main() method to run the example) ...
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();
}
}
Creating Lambda expressions is done using a functional interface. Yes, it is an interface well-known from Java. A functional interface must have one and only one method, which must be public and abstract. By default, methods in interfaces are public and abstract, hence the lack of keywords next to the method – public abstract void makeNoise().
More information about interfaces can be found here Interfaces (The Java™ Tutorials > Learning the Java Language > Interfaces and Inheritance) , information about the functional interface here FunctionalInterface (Java Platform SE 8). The function interface is a "normal" Java interface that has some limitations.
Official Documentation Oracle Java describes Lambda expressions as follows: "Lambda expressions let you express instances of single-method classes more compactly." My own interpretation is, "lambda, is an anonymous method implementation".
For simplicity, I will present the code using the main() method. The right approach would be to create a unit test that verifies the correct operation of the written code. Below is the code with the main() method.
public class LambdaStepByStepMain {
public static void main(String[] args) {
// TODO: Implementacja wyrażenia Lambda, krok po kroku ...
// Sekcja #1 ...
// Sekcja #2 ...
}
}
Section #1 – a simple method for Lambda expressions
For this example, I assume that a simple method is one that takes no parameters and returns nothing.
Below is the code of the Jukebox functional interface with the void makeNoise() method, which will be used to present Lambda expressions. As you can see, the makeNoise() method takes no parameters - empty brackets () - and does not return a value - the void keyword.
@FunctionalInterface
public interface Jukebox {
void makeNoise();
}
Using an interface, including a functional one, requires its implementation. Below I will present several ways to implement the interface. To create a Lambda expression, i.e. an anonymous method implementation, I will start with a few "steps back" to explain exactly why I call it an anonymous method implementation.
Steps to understand the process of moving to a Lambda expression:
- Classic implementation of the interface using a separate class.
- Anonymous implementation of an interface using an anonymous class.
- Simple Lambda expression – anonymous implementation of an interface method
It's worth reading the official Oracle Java documentation - When to Use Nested Classes, Local Classes, Anonymous Classes, and Lambda Expressions to fully understand the steps I describe below.
Classic implementation of the interface using a separate class.
The first step to understanding Lambda expressions is described below, i.e. the classic implementation of the interface using a separate class.
public class RetroJukebox implements Jukebox {
@Override
public void makeNoise() {
System.out.println("Retro Make Some Noise!");
}
}
This way of implementing the interface and its methods requires creating an additional, separate class and a file with Java code. Then, you will need to create an object for the new class and call the appropriate method, as shown in the code below.
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
The result of running the code from the main() method of the LambdaStepByStepMain class.
As you can see, the above solution requires a lot of code, which is "scattered" in two different classes, RetroJukeBox and LambdaStepByStepMain. This is the classic approach to writing Java code. Lambda expressions change this approach.
Anonymous implementation of an interface using an anonymous class
The second step to understanding Lambda expressions is described below, i.e. anonymous implementation of the interface using an anonymous class.
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();
}
}
The above implementation method does not require a separate class, it is replaced by an anonymous interface implementation that uses the anonymous class with the implementation of the makeNoise() method and then creates an object for it - Jukebox anonymousJukebox = new Jukebox() {}. The appropriate method is called again.
This code snippet can be broken down into several elements to better understand it:
- Declaration of the anonymous Jukebox variable, of type Jukebox (functional interface), to which we assign a value - fragment Jukebox anonymousJukebox =.
- An anonymous class that implements the makeNoise() method of the Jukebox interface - the following code fragment.
new Jukebox() {
@Override
public void makeNoise() {
System.out.println("Anonymous Make Some Noise!");
}
};
More information about anonymous classes can be found here Anonymous Classes (The Java™ Tutorials > Learning the Java Language > Classes and Objects).
Anonymous Make Some Noise! Process finished with exit code 0
The result of running the code from the main() method of the LambdaStepByStepMain class.
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!”);
- Deklaracja zmiennej lambdaJukebox, typu Jukebx (interfejs funkcyjny), do której przypisujemy wartość – fragment Jukebox lambdaJukebox =.
- 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
The result of running the code from the main() method of the LambdaStepByStepMain class.
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 Section #1 – a simple method for Lambda expressions.
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 makeNoiseoraz 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.