Java 8 – Lambda Expressions Explained with Examples
Lambda expressions are the most important feature of Java 8. They allow you to write a block of code that can be passed around as data — assigned to a variable, passed as a method argument, or returned from a method — without creating a full anonymous class. This tutorial explains lambda syntax, how it works, and when to use it.
What Problem Do Lambdas Solve?
Before Java 8, passing behaviour (a function) as an argument required creating an anonymous class. This was verbose for simple cases like button listeners or thread tasks:
// Java 7 — anonymous class just to sort a list
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
With a lambda, this collapses to one line:
// Java 8 — lambda expression
Collections.sort(names, (a, b) -> a.compareTo(b));
// Even shorter — method reference
Collections.sort(names, String::compareTo);
Lambda Syntax
// Full syntax
(Type param1, Type param2) -> { statements; return value; }
// Type-inferred parameters (most common)
(param1, param2) -> { statements; return value; }
// Single-expression body — no braces, no return keyword
(param1, param2) -> expression
// No parameters
() -> expression
// Single parameter — parentheses optional
param -> expression
Example 1 – No Parameters
// Runnable — no args, no return value
Runnable r = () -> System.out.println("Hello from lambda!");
r.run(); // Hello from lambda!
// Thread
new Thread(() -> System.out.println("Thread running")).start();
Example 2 – One Parameter
import java.util.function.Consumer;
Consumer<String> print = name -> System.out.println("Hello, " + name + "!");
print.accept("Ravi"); // Hello, Ravi!
print.accept("Java 8"); // Hello, Java 8!
Example 3 – Two Parameters
import java.util.function.*;
// BinaryOperator — two args of same type, same return type
BinaryOperator<Integer> add = (a, b) -> a + b;
BinaryOperator<Integer> multiply = (a, b) -> a * b;
System.out.println(add.apply(3, 4)); // 7
System.out.println(multiply.apply(3, 4)); // 12
// Comparator
Comparator<String> byLength = (a, b) -> a.length() - b.length();
List<String> words = Arrays.asList("banana", "kiwi", "apple", "fig");
words.sort(byLength);
System.out.println(words); // [fig, kiwi, apple, banana]
Example 4 – Return Value with Multi-line Body
import java.util.function.Function;
Function<Integer, String> classify = n -> {
if (n < 0) return "negative";
if (n == 0) return "zero";
if (n < 10) return "small";
return "large";
};
System.out.println(classify.apply(-5)); // negative
System.out.println(classify.apply(0)); // zero
System.out.println(classify.apply(7)); // small
System.out.println(classify.apply(42)); // large
Lambda with Collections
import java.util.*;
List<String> products = Arrays.asList("Laptop", "Smartphone", "Tablet", "Monitor", "Keyboard");
// forEach — iterate
products.forEach(p -> System.out.println(p));
// removeIf — remove elements matching condition
products = new ArrayList<>(products);
products.removeIf(p -> p.length() > 7);
System.out.println(products); // [Laptop, Tablet, Monitor]
// sort
products.sort((a, b) -> a.compareTo(b));
System.out.println(products); // [Laptop, Monitor, Tablet]
// replaceAll
products.replaceAll(p -> p.toUpperCase());
System.out.println(products); // [LAPTOP, MONITOR, TABLET]
Lambda vs Anonymous Class — When the Compiler Sees Them
// These are equivalent at the bytecode level:
// Anonymous class
Comparator<Integer> comp1 = new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return a - b;
}
};
// Lambda — cleaner, same result
Comparator<Integer> comp2 = (a, b) -> a - b;
List<Integer> nums = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6);
nums.sort(comp2);
System.out.println(nums); // [1, 1, 2, 3, 4, 5, 6, 9]
Capturing Variables (Closures)
Lambdas can use variables from the enclosing scope, but those variables must be effectively final (not modified after the lambda captures them):
String prefix = "Product: "; // effectively final
Consumer<String> print = name -> System.out.println(prefix + name);
print.accept("Laptop"); // Product: Laptop
// This would cause a compile error:
// prefix = "Item: "; // ← can't reassign — lambda already captured it
Lambda Syntax Quick Reference
| Parameters | Body | Example |
|---|---|---|
| None | Expression | () -> "hello" |
| None | Block | () -> { System.out.println("hi"); } |
| One (typed) | Expression | (String s) -> s.length() |
| One (inferred) | Expression | s -> s.length() |
| Multiple | Expression | (a, b) -> a + b |
| Multiple | Block | (a, b) -> { int r = a + b; return r; } |
Where Lambdas Can Be Used
A lambda can be used anywhere a functional interface is expected — any interface with exactly one abstract method:
Runnable,Callable— thread tasksComparator— sortingEventListener— GUI eventsPredicate,Function,Consumer,Supplier— stream operations- Any custom
@FunctionalInterface
Summary
Lambda expressions replace anonymous class boilerplate whenever you need to pass a single method as an argument. The syntax (params) -> body is concise, and the compiler infers parameter types from the target functional interface. Lambdas work with all standard functional interfaces in java.util.function and with the entire Stream API. Use them freely for collection operations, event handling, and threading — but use a named method or method reference when the logic is complex enough to deserve a proper name.
Comments