Java Lambda Expressions
Introduction
Lambda expressions were introduced in Java 8 as one of the major features that brought functional programming capabilities to Java. They provide a clear and concise way to implement single-method interfaces (known as functional interfaces) without the verbosity of anonymous inner classes.
At their core, lambda expressions are anonymous functions that can be treated as values – passed around, returned from methods, or stored in variables. They've become essential in modern Java development, especially when working with the Spring Framework, which has embraced the functional programming style in many of its newer APIs.
What are Lambda Expressions?
A lambda expression represents an anonymous function with the following characteristics:
- No name (anonymous)
- Parameter list
- Body
- Return type (inferred by the compiler)
- Potentially throws exceptions
The basic syntax of a lambda expression is:
(parameters) -> expression
or
(parameters) -> { statements; }
Functional Interfaces
Before we dive deeper into lambda expressions, it's important to understand functional interfaces. A functional interface is an interface that contains exactly one abstract method. Lambda expressions can be used to provide the implementation of that abstract method.
Java provides several built-in functional interfaces in the java.util.function
package, such as Predicate<T>
, Function<T,R>
, Consumer<T>
, and Supplier<T>
.
Here's a simple example of a functional interface:
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
The @FunctionalInterface
annotation is optional but recommended as it helps the compiler to verify that the interface has exactly one abstract method.
Basic Lambda Examples
Let's explore some basic examples of lambda expressions:
Example 1: Simple Lambda Expression
public class LambdaExample1 {
public static void main(String[] args) {
// Traditional anonymous class
Runnable traditionalRunnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello from traditional runnable!");
}
};
// Lambda expression
Runnable lambdaRunnable = () -> System.out.println("Hello from lambda runnable!");
// Execute both
traditionalRunnable.run();
lambdaRunnable.run();
}
}
Output:
Hello from traditional runnable!
Hello from lambda runnable!
In this example, we're implementing the Runnable
interface, which has a single abstract method run()
. The lambda expression provides a much more concise way to implement this interface.
Example 2: Lambda with Parameters
public class LambdaExample2 {
interface StringOperation {
String process(String str);
}
public static void main(String[] args) {
// Lambda to convert to uppercase
StringOperation toUpperCase = (s) -> s.toUpperCase();
// Lambda to remove spaces
StringOperation removeSpaces = (s) -> s.replace(" ", "");
String input = "Hello Lambda World";
System.out.println("Original: " + input);
System.out.println("Uppercase: " + toUpperCase.process(input));
System.out.println("No spaces: " + removeSpaces.process(input));
}
}
Output:
Original: Hello Lambda World
Uppercase: HELLO LAMBDA WORLD
No spaces: HelloLambdaWorld
In this example, we've defined our own functional interface StringOperation
and created two different implementations using lambda expressions.
Example 3: Lambda with Multiple Parameters
public class LambdaExample3 {
public static void main(String[] args) {
// Using our Calculator functional interface
Calculator add = (a, b) -> a + b;
Calculator subtract = (a, b) -> a - b;
Calculator multiply = (a, b) -> a * b;
Calculator divide = (a, b) -> a / b;
System.out.println("10 + 5 = " + add.calculate(10, 5));
System.out.println("10 - 5 = " + subtract.calculate(10, 5));
System.out.println("10 * 5 = " + multiply.calculate(10, 5));
System.out.println("10 / 5 = " + divide.calculate(10, 5));
}
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
}
Output:
10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
10 / 5 = 2
Here we've used lambda expressions to implement different calculations with our Calculator
interface.
Example 4: Block of Code in Lambda Expression
When your lambda expression contains multiple statements, you need to use curly braces:
public class LambdaExample4 {
public static void main(String[] args) {
// Lambda with multiple statements
Calculator complexCalc = (a, b) -> {
int result = a * b;
result = result + a;
System.out.println("Calculating: " + a + " * " + b + " + " + a);
return result;
};
System.out.println("Result: " + complexCalc.calculate(5, 3));
}
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
}
Output:
Calculating: 5 * 3 + 5
Result: 20
This example demonstrates that lambda expressions can contain multiple statements within a block of code.
Built-in Functional Interfaces
Java provides several standard functional interfaces in the java.util.function
package. Let's look at some examples:
Example 5: Using Predicate
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class LambdaExample5 {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Dave", "Eva");
// Predicate for names starting with 'C'
Predicate<String> startsWithC = name -> name.startsWith("C");
// Predicate for names with length > 3
Predicate<String> lengthGreaterThanThree = name -> name.length() > 3;
System.out.println("Names starting with 'C':");
printFilteredNames(names, startsWithC);
System.out.println("\nNames with length > 3:");
printFilteredNames(names, lengthGreaterThanThree);
System.out.println("\nNames starting with 'C' AND length > 3:");
printFilteredNames(names, startsWithC.and(lengthGreaterThanThree));
}
public static void printFilteredNames(List<String> names, Predicate<String> condition) {
for (String name : names) {
if (condition.test(name)) {
System.out.println(name);
}
}
}
}
Output:
Names starting with 'C':
Charlie
Names with length > 3:
Alice
Charlie
Dave
Names starting with 'C' AND length > 3:
Charlie
The Predicate<T>
interface has a single method test(T t)
that returns a boolean. In this example, we're using predicates to filter a list of names based on different criteria.
Example 6: Using Consumer
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
public class LambdaExample6 {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// Consumer to print in uppercase
Consumer<String> printUpperCase = name -> System.out.println(name.toUpperCase());
// Consumer to print length
Consumer<String> printLength = name -> System.out.println("Length: " + name.length());
System.out.println("Names in uppercase:");
processNames(names, printUpperCase);
System.out.println("\nName lengths:");
processNames(names, printLength);
System.out.println("\nChained consumers:");
processNames(names, printUpperCase.andThen(printLength));
}
public static void processNames(List<String> names, Consumer<String> consumer) {
for (String name : names) {
consumer.accept(name);
}
}
}
Output:
Names in uppercase:
ALICE
BOB
CHARLIE
Name lengths:
Length: 5
Length: 3
Length: 7
Chained consumers:
ALICE
Length: 5
BOB
Length: 3
CHARLIE
Length: 7
The Consumer<T>
interface has a single method accept(T t)
that performs an operation on the input argument without returning any result.
Method References
Method references provide a shorthand notation for lambda expressions that execute just one method. The syntax is:
ClassName::methodName
Let's look at some examples:
Example 7: Using Method References
import java.util.Arrays;
import java.util.List;
public class LambdaExample7 {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// Lambda expression
names.forEach(name -> System.out.println(name));
System.out.println("-----");
// Equivalent method reference
names.forEach(System.out::println);
}
}
Output:
Alice
Bob
Charlie
-----
Alice
Bob
Charlie
Here, System.out::println
is a method reference that is equivalent to the lambda expression name -> System.out.println(name)
.
Example 8: Different Types of Method References
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
public class LambdaExample8 {
public static void main(String[] args) {
// Static method reference
Function<String, Integer> parseInt = Integer::parseInt;
System.out.println("Parse '123': " + parseInt.apply("123"));
// Instance method reference on a specific object
String greeting = "Hello";
Supplier<Integer> lengthSupplier = greeting::length;
System.out.println("Length of '" + greeting + "': " + lengthSupplier.get());
// Instance method reference on an arbitrary object of a particular type
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.sort(String::compareToIgnoreCase);
System.out.println("Sorted names: " + names);
// Constructor reference
Supplier<StringBuilder> sbSupplier = StringBuilder::new;
StringBuilder sb = sbSupplier.get();
sb.append("Constructed via method reference");
System.out.println(sb.toString());
}
}
Output:
Parse '123': 123
Length of 'Hello': 5
Sorted names: [Alice, Bob, Charlie]
Constructed via method reference
This example demonstrates the four types of method references:
- Reference to a static method
- Reference to an instance method of a particular object
- Reference to an instance method of an arbitrary object of a particular type
- Reference to a constructor
Real-world Applications
Let's examine some practical applications of lambda expressions in real-world scenarios:
Example 9: Event Handling
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
public class LambdaExample9 {
public static void main(String[] args) {
JFrame frame = new JFrame("Lambda Button Example");
JButton button = new JButton("Click Me");
// Traditional way using anonymous class
/*
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(frame, "Button clicked!");
}
});
*/
// Using lambda expression
button.addActionListener(e -> JOptionPane.showMessageDialog(frame, "Button clicked!"));
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(button);
frame.setSize(200, 100);
frame.setVisible(true);
}
}
This example demonstrates how lambda expressions can simplify event handling in GUI applications.
Example 10: Stream Processing
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class LambdaExample10 {
public static void main(String[] args) {
List<Person> people = Arrays.asList(
new Person("Alice", 30),
new Person("Bob", 20),
new Person("Charlie", 35),
new Person("Dave", 25)
);
// Using lambdas with streams to filter, sort, and map data
List<String> sortedAdultNames = people.stream()
.filter(p -> p.getAge() > 21)
.sorted((p1, p2) -> p1.getName().compareTo(p2.getName()))
.map(p -> p.getName() + " (" + p.getAge() + ")")
.collect(Collectors.toList());
System.out.println("Sorted adults:");
sortedAdultNames.forEach(System.out::println);
}
static class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
}
}
Output:
Sorted adults:
Alice (30)
Charlie (35)
Dave (25)
This example demonstrates how lambda expressions can be used with Java Streams to process collections in a functional style.
Example 11: Spring Framework Integration
When working with Spring Framework, lambda expressions can greatly simplify your code. Here's an example using Spring's RestTemplate
:
import org.springframework.web.client.RestTemplate;
import org.springframework.http.ResponseEntity;
public class LambdaExample11 {
public static void main(String[] args) {
RestTemplate restTemplate = new RestTemplate();
String url = "https://api.example.com/users";
// Traditional way
/*
ResponseEntity<User[]> response = restTemplate.getForEntity(url, User[].class);
User[] users = response.getBody();
for (User user : users) {
System.out.println(user.getName());
}
*/
// Using lambda expressions
restTemplate.getForEntity(url, User[].class)
.getBody()
.stream()
.map(User::getName)
.forEach(System.out::println);
}
static class User {
private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
}
This example shows how lambda expressions can simplify working with REST APIs in Spring.
Best Practices for Lambda Expressions
-
Keep them short and focused: Lambda expressions should ideally be concise and perform a single, well-defined task.
-
Consider readability: While lambdas can make code shorter, they might not always make it more readable. Use them judiciously.
-
Use method references when possible: They often provide a more readable alternative to lambdas.
-
Be careful with exceptions: Handling exceptions in lambda expressions can be challenging, so consider creating a separate method if exception handling is needed.
-
Avoid side effects: Pure functions (those without side effects) are easier to test and reason about.
Summary
Java lambda expressions are a powerful feature that brings functional programming capabilities to Java. They provide a concise way to express behavior, especially when dealing with functional interfaces.
Key points to remember:
- Lambda expressions implement functional interfaces (interfaces with a single abstract method)
- Their syntax is
(parameters) -> expression
or(parameters) -> { statements; }
- They can be used wherever the target type is a functional interface
- Method references provide a shorthand notation for simple lambda expressions
- Lambda expressions are extensively used in modern Java features like the Stream API
Lambda expressions, along with functional interfaces and method references, have transformed the way we write Java code, making it more concise, expressive, and maintainable.
Additional Resources
Exercises
-
Create a functional interface called
StringTransformer
with a method that takes a string and returns a transformed string. Implement it using lambda expressions to: convert to uppercase, reverse the string, and replace spaces with underscores. -
Write a program that uses the
Predicate<T>
interface to filter a list of integers that are: even, greater than 10, and divisible by 3. -
Implement a simple calculator using lambda expressions for addition, subtraction, multiplication, and division. Add error handling for division by zero.
-
Rewrite the following anonymous inner class as a lambda expression:
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
return p1.getAge() - p2.getAge();
}
});
- Explore the Stream API by writing a program that uses lambda expressions to find the average age of all people older than 18 in a list.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)