Introduction
In this tutorial, we’ll learn one of the most exciting features introduced in Java 8 — lambda expressions. Lambda expressions provide a clear and concise way to represent anonymous functions. They allow us to write less code while making our code easier to understand, especially when dealing with functional interfaces and collections. Let’s dive in and learn more about lambda expressions and how they can simplify your code.
What is a Lambda Expression?
A lambda expression is simply a function without a name. It can even be used as a parameter in a function. Lambda Expressions facilitate functional programming and simplify development. The main use of Lambda expression is to provide an implementation for a functional interface.
How is Lambda Related to Functional Interfaces?
Lambda expressions work with functional interfaces. A functional interface contains exactly one abstract method. It can have multiple default or static methods but only one abstract method. Lambda expressions provide a way to implement the single abstract method of a functional interface using an expression.
Basic Syntax of Lambda Expressions
The syntax of a lambda expression consists of three parts:
- Parameter List: Enclosed in parentheses. It can be empty or contain multiple parameters.
- Arrow Token:
->
separates the parameter list from the body. - Body: Contains a single expression or a block of statements.
Syntax:
(parameters) -> expression
(parameters) -> { statements; }
Lambda Expresssion Rules based on Syntax
Here are simple lambda expressions that demonstrate the syntax rules of lambdas in Java:
1. No Parameters
- If your lambda expression doesn’t take any parameters, you can use empty parentheses
()
.
() -> System.out.println("Hello, World!");
This lambda expression doesn’t take any arguments and prints “Hello, World!” to the console.
2. Single Parameter without Parentheses
- You can omit the parentheses around the parameter name if there is a single parameter.
name -> System.out.println("Hello, " + name);
This lambda expression takes a single argument name
and prints it alongside “Hello,”.
3. Single Parameter with Parentheses (Optional)
- You can also include parentheses around a single parameter, which is also correct.
(name) -> System.out.println("Hello, " + name);
This is equivalent to the previous example but includes parentheses around the parameter.
4. Multiple Parameters
- If there are multiple parameters, parentheses are required.
(a, b) -> System.out.println(a + b);
This lambda expression takes two parameters (a
and b
) and prints their sum.
5. Single-Line Expressions (Without Return Statement)
- If the body of the lambda expression consists of a single expression, you can omit curly braces
{}
and thereturn
keyword.
(x, y) -> x + y;
This lambda expression adds two numbers x
and y
. The return statement is implicit here since there’s a single expression.
6. Multiple Statements (Requires Curly Braces)
- If the lambda body contains more than one statement, you need to enclose it in curly braces
{}
and explicitly usereturn
(if needed).
(x, y) -> {
int sum = x + y;
return sum;
};
This lambda expression adds x
and y
, stores the result in sum
, and then returns sum
.
7. Returning Values (Implicit Return)
- If a lambda expression is a single line, the return is implicit.
(x) -> x * x;
This lambda expression returns the square of x
. There’s no need to explicitly write return
.
8. Returning Values (Explicit Return in Multi-Line Block)
- If the lambda body contains multiple lines, you need to explicitly return the result.
(x, y) -> {
int sum = x + y;
System.out.println("Sum: " + sum);
return sum;
};
This lambda expression prints the sum of x
and y
, and then returns the sum. Since there are multiple statements, curly braces and an explicit return
are needed.
9. No Parameters, Returning Value
- A lambda can also return a value even if it doesn’t accept any parameters.
() -> "Hello, Lambda!";
This lambda expression takes no parameters and returns the string "Hello, Lambda!"
.
Lambda Expression Syntax Rules Recap:
- Parentheses around a single parameter are optional.
- Curly braces
{}
are required when the body contains multiple statements. - Return keyword is optional for single-line expressions, but required in multi-line expressions that return a value.
- Type inference: Java automatically infers the type of the parameter in most cases, so you usually don’t need to specify parameter types in lambda expressions.
Lambda Expressions Example
Without Lambda Expressions (Anonymous Classes)
Let’s first look at how code was traditionally written before Java 8, using anonymous inner classes. Suppose we want to implement an interface with a single method that adds two numbers. We’ll have to create an entire anonymous class to implement the interface.
// Calculator interface with a method to add two numbers
interface Calculator {
int add(int a, int b);
}
public class WithoutLambdaExample {
public static void main(String[] args) {
// Implementing the Calculator interface using an anonymous class
Calculator calculator = new Calculator() {
@Override
public int add(int a, int b) {
return a + b;
}
};
// Using the implemented method
int result = calculator.add(5, 10);
System.out.println("Result: " + result);
}
}
With Lambda Expressions
Let’s see how we can use a lambda expression to achieve the same thing but with a much cleaner, shorter, and more readable syntax. Lambda expressions eliminate the need for writing an entire anonymous class.”
// Calculator interface with a method to add two numbers
interface Calculator {
int add(int a, int b);
}
public class WithLambdaExample {
public static void main(String[] args) {
// Lambda expression to implement the Calculator interface
Calculator calculator = (a, b) -> a + b;
// Using the lambda expression
int result = calculator.add(5, 10);
System.out.println("Result: " + result);
}
}
- The lambda expression
(a, b) -> a + b
replaces the anonymous class. It provides a much simpler and more concise implementation of theCalculator
interface. - We no longer need to write the
new Calculator()
or override methods—just specify the behavior directly.
Functional Interfaces and Lambda Expressions
A functional interface has a single abstract method. Lambda expressions can instantiate these interfaces. Understanding the relationship between lambda expressions and functional interfaces is essential to understanding their usage in Java.
Example:
@FunctionalInterface
interface MyFunctionalInterface {
void execute();
}
public class LambdaExample {
public static void main(String[] args) {
MyFunctionalInterface lambda = () -> System.out.println("Executing...");
lambda.execute();
}
}
Common Functional Interfaces:
- Runnable:
void run()
- Callable:
V call()
- Comparator:
int compare(T o1, T o2)
- Consumer:
void accept(T t)
- Supplier:
T get()
- Function:
R apply(T t)
- Predicate:
boolean test(T t)
Use Cases of Lambda Expressions
Implementing Runnable
Lambda expressions can simplify the creation of threads by implementing the Runnable
interface.
Without Lambda:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread is running...");
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // Start the thread
}
}
With Lambda:
public class LambdaRunnableExample {
public static void main(String[] args) {
Runnable r = () -> System.out.println("Running...");
new Thread(r).start();
}
}
Implementing Comparator
Lambda expressions can be used to implement the Comparator
interface for custom sorting logic.
Without Lambda:
import java.util.Arrays;
import java.util.Comparator;
public class ComparatorExample {
public static void main(String[] args) {
String[] words = {"apple", "banana", "cherry"};
Arrays.sort(words, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
for (String word : words) {
System.out.println(word);
}
}
}
With Lambda:
import java.util.Arrays; public class LambdaComparatorExample { public static void main(String[] args) { String[] words = {"apple", "banana", "cherry"}; // Using lambda expression to define a Comparator Arrays.sort(words, (a, b) -> a.length() - b.length()); for (String word : words) { System.out.println(word); } } }
Iterating Collections
Lambda expressions simplify the iteration over collections.
Without Lambda:
import java.util.Arrays;
import java.util.List;
public class CollectionIterationExample {
public static void main(String[] args) {
List<String> words = Arrays.asList("apple", "banana", "cherry");
for (String word : words) {
System.out.println(word);
}
}
}
With Lambda:
import java.util.Arrays;
import java.util.List;
public class LambdaForEachExample {
public static void main(String[] args) {
List<String> words = Arrays.asList("apple", "banana", "cherry");
words.forEach(word -> System.out.println(word));
}
}
Stream API
Lambda expressions are frequently used with the Stream API for performing operations on collections.
Without Lambda:
import java.util.Arrays;
import java.util.List;
public class StreamAPIExample {
public static void main(String[] args) {
List<String> words = Arrays.asList("apple", "banana", "cherry");
for (String word : words) {
if (word.startsWith("a")) {
System.out.println(word);
}
}
}
}
With Lambda:
import java.util.Arrays;
import java.util.List;
public class LambdaStreamExample {
public static void main(String[] args) {
List<String> words = Arrays.asList("apple", "banana", "cherry");
words.stream()
.filter(word -> word.startsWith("a"))
.forEach(System.out::println);
}
}
Custom Functional Interfaces
Lambda expressions can be used to define custom functional interfaces for specific tasks.
Without Lambda:
@FunctionalInterface
interface StringProcessor {
String process(String s);
}
class UpperCaseProcessor implements StringProcessor {
@Override
public String process(String s) {
return s.toUpperCase();
}
}
public class CustomFunctionalInterfaceExample {
public static void main(String[] args) {
StringProcessor processor = new UpperCaseProcessor();
System.out.println(processor.process("Hello, World!"));
}
}
With Lambda:
@FunctionalInterface
interface StringProcessor {
String process(String s);
}
public class LambdaCustomInterfaceExample {
public static void main(String[] args) {
StringProcessor toUpperCase = s -> s.toUpperCase();
StringProcessor toLowerCase = s -> s.toLowerCase();
System.out.println(toUpperCase.process("Hello, World!"));
System.out.println(toLowerCase.process("Hello, World!"));
}
}
4. Best Practices
- Use Descriptive Names: Use descriptive parameter names in lambda expressions to improve readability.
- Keep it Simple: Keep lambda expressions simple and concise.
- Avoid Complex Logic: Avoid placing complex logic inside lambda expressions. Instead, use method references or extract the logic into separate methods.
- Use Method References: Use method references where appropriate to make the code cleaner and more readable.
5. Conclusion
Lambda expressions in Java are powerful for writing concise and readable code. They enable functional programming, simplify the syntax for anonymous classes, and work seamlessly with the Stream API. You can write more expressive and maintainable Java code by understanding and effectively using lambda expressions.