Lambda expressions are a fundamental feature introduced in Java 8 that enable functional programming and simplify the implementation of interfaces with a single abstract method (functional interfaces).
1. Basic Syntax
// Traditional way using anonymous class
Runnable traditionalRunnable = new Runnable() {
@Override
public void run() {
System.out.println("Traditional way");
}
};
// Lambda expression way
Runnable lambdaRunnable = () -> {
System.out.println("Lambda way");
};
// Even simpler for single statements
Runnable simpleLambda = () -> System.out.println("Simple lambda");
2. Lambda Syntax Variations
import java.util.*;
import java.util.function.*;
public class LambdaSyntax {
public static void main(String[] args) {
// 1. No parameters
Supplier<String> supplier = () -> "Hello World";
// 2. Single parameter (parentheses optional)
Consumer<String> consumer1 = (msg) -> System.out.println(msg);
Consumer<String> consumer2 = msg -> System.out.println(msg); // No parentheses
// 3. Multiple parameters
BinaryOperator<Integer> adder = (a, b) -> a + b;
// 4. Multiple parameters with explicit types
BinaryOperator<Integer> typedAdder = (Integer a, Integer b) -> a + b;
// 5. Multiple statements in body
Function<String, String> processor = (str) -> {
String trimmed = str.trim();
return trimmed.toUpperCase();
};
}
}
3. Common Functional Interfaces with Lambdas
Predicate
import java.util.function.Predicate;
import java.util.*;
public class PredicateExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("John", "Alice", "Bob", "Diana");
// Lambda with Predicate
Predicate<String> startsWithA = name -> name.startsWith("A");
Predicate<String> lengthGreaterThan3 = name -> name.length() > 3;
// Using predicates
names.stream()
.filter(startsWithA)
.forEach(System.out::println);
// Combined predicates
names.stream()
.filter(startsWithA.and(lengthGreaterThan3))
.forEach(System.out::println);
}
}
Function
import java.util.function.Function;
import java.util.*;
public class FunctionExample {
public static void main(String[] args) {
List<String> words = Arrays.asList("hello", "world", "java", "lambda");
// Lambda with Function
Function<String, Integer> lengthMapper = str -> str.length();
Function<String, String> upperCaseMapper = str -> str.toUpperCase();
// Using functions
words.stream()
.map(lengthMapper)
.forEach(len -> System.out.println("Length: " + len));
words.stream()
.map(upperCaseMapper)
.forEach(System.out::println);
}
}
Consumer
import java.util.function.Consumer;
import java.util.*;
public class ConsumerExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// Lambda with Consumer
Consumer<Integer> squarePrinter = num -> {
int square = num * num;
System.out.println(num + "² = " + square);
};
Consumer<Integer> doublePrinter = num ->
System.out.println("Double: " + (num * 2));
// Using consumers
numbers.forEach(squarePrinter);
// Chaining consumers
numbers.forEach(squarePrinter.andThen(doublePrinter));
}
}
Supplier
import java.util.function.Supplier;
import java.util.Random;
public class SupplierExample {
public static void main(String[] args) {
// Lambda with Supplier
Supplier<Double> randomSupplier = () -> Math.random();
Supplier<String> greetingSupplier = () -> "Hello, World!";
// Using suppliers
System.out.println("Random number: " + randomSupplier.get());
System.out.println("Greeting: " + greetingSupplier.get());
// More complex supplier
Supplier<String> otpSupplier = () -> {
Random random = new Random();
return String.format("%06d", random.nextInt(1000000));
};
System.out.println("OTP: " + otpSupplier.get());
}
}
4. Method References with Lambdas
import java.util.*;
import java.util.function.*;
public class MethodReferenceExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("John", "Alice", "Bob", "Diana");
// Static method reference
Function<String, Integer> parser = Integer::parseInt;
// Instance method reference
Consumer<String> printer = System.out::println;
// Arbitrary object method reference
names.forEach(String::toUpperCase);
// Constructor reference
Supplier<List<String>> listSupplier = ArrayList::new;
Function<Integer, String[]> arraySupplier = String[]::new;
}
// Custom method for method reference
public static boolean isEven(Integer number) {
return number % 2 == 0;
}
}
5. Real-World Examples
Sorting with Lambdas
import java.util.*;
public class SortingExample {
public static void main(String[] args) {
List<Person> people = Arrays.asList(
new Person("John", 25),
new Person("Alice", 30),
new Person("Bob", 20)
);
// Traditional sorting with anonymous class
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
return p1.getName().compareTo(p2.getName());
}
});
// Sorting with lambda
Collections.sort(people, (p1, p2) -> p1.getName().compareTo(p2.getName()));
// Even simpler with Comparator.comparing
people.sort(Comparator.comparing(Person::getName));
people.sort(Comparator.comparing(Person::getAge));
// Reverse sorting
people.sort(Comparator.comparing(Person::getAge).reversed());
// Multiple criteria
people.sort(Comparator.comparing(Person::getName)
.thenComparing(Person::getAge));
}
}
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; }
@Override
public String toString() {
return name + " (" + age + ")";
}
}
Threading with Lambdas
public class ThreadExample {
public static void main(String[] args) {
// Traditional way
Thread traditionalThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Traditional thread");
}
});
// Lambda way
Thread lambdaThread = new Thread(() -> {
System.out.println("Lambda thread");
// Multiple operations
for (int i = 0; i < 5; i++) {
System.out.println("Count: " + i);
}
});
traditionalThread.start();
lambdaThread.start();
}
}
Collection Processing with Streams and Lambdas
import java.util.*;
import java.util.stream.*;
public class StreamLambdaExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Filter even numbers and square them
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0) // Lambda in filter
.map(n -> n * n) // Lambda in map
.collect(Collectors.toList());
System.out.println("Squared evens: " + result);
// Group by even/odd
Map<String, List<Integer>> grouped = numbers.stream()
.collect(Collectors.groupingBy(
n -> n % 2 == 0 ? "EVEN" : "ODD" // Lambda classifier
));
System.out.println("Grouped: " + grouped);
// Sum using reduce with lambda
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b); // Lambda accumulator
System.out.println("Sum: " + sum);
}
}
6. Custom Functional Interfaces
// Custom functional interface
@FunctionalInterface
interface StringProcessor {
String process(String input);
// Can have default methods
default StringProcessor andThen(StringProcessor after) {
return input -> after.process(this.process(input));
}
// Can have static methods
static StringProcessor toUpperCase() {
return String::toUpperCase;
}
}
public class CustomFunctionalInterface {
public static void main(String[] args) {
// Using custom functional interface with lambda
StringProcessor trimmer = str -> str.trim();
StringProcessor upperCaser = str -> str.toUpperCase();
StringProcessor reverser = str -> new StringBuilder(str).reverse().toString();
String input = " hello world ";
// Chain processors
StringProcessor pipeline = trimmer.andThen(upperCaser).andThen(reverser);
String result = pipeline.process(input);
System.out.println("Original: '" + input + "'");
System.out.println("Processed: '" + result + "'");
// Using static method
StringProcessor staticProcessor = StringProcessor.toUpperCase();
System.out.println(staticProcessor.process("hello"));
}
}
7. Variable Capture in Lambdas
import java.util.function.*;
public class VariableCaptureExample {
private String instanceVariable = "Instance variable";
public void demonstrateCapture() {
String localVariable = "Local variable";
final String finalLocal = "Final local";
// Capturing instance variable
Supplier<String> instanceCapture = () -> this.instanceVariable;
// Capturing local variable (effectively final)
Supplier<String> localCapture = () -> localVariable;
// Capturing final local variable
Supplier<String> finalCapture = () -> finalLocal;
// Cannot modify captured variables
// localVariable = "modified"; // This would make lambda invalid
System.out.println(instanceCapture.get());
System.out.println(localCapture.get());
System.out.println(finalCapture.get());
}
public static void main(String[] args) {
new VariableCaptureExample().demonstrateCapture();
}
}
8. Exception Handling in Lambdas
import java.util.function.*;
import java.io.*;
public class ExceptionHandlingLambda {
// Functional interface that throws exception
@FunctionalInterface
interface FileProcessor {
String process(File file) throws IOException;
}
public static void main(String[] args) {
List<File> files = Arrays.asList(new File("file1.txt"), new File("file2.txt"));
// Approach 1: Handle exception in lambda
files.forEach(file -> {
try {
if (file.exists()) {
System.out.println("File: " + file.getCanonicalPath());
}
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
});
// Approach 2: Wrap in unchecked exception
Function<File, String> safeProcessor = file -> {
try {
return file.getCanonicalPath();
} catch (IOException e) {
throw new RuntimeException(e);
}
};
// Approach 3: Create wrapper method
files.forEach(ExceptionHandlingLambda::processFileSafely);
}
private static void processFileSafely(File file) {
try {
System.out.println("Processing: " + file.getCanonicalPath());
} catch (IOException e) {
System.err.println("Failed to process: " + file.getName());
}
}
}
Key Benefits of Lambda Expressions
- Concise Code - Reduces boilerplate code
- Readability - More expressive and readable code
- Functional Programming - Enables functional programming paradigms
- Parallel Processing - Easier parallel execution with streams
- API Design - Enables more flexible API designs
Best Practices
- Keep Lambdas Short - Prefer single-purpose lambdas
- Use Method References - When appropriate for better readability
- Avoid Complex Logic - Move complex logic to separate methods
- Consider Performance - Be mindful of variable capture overhead
- Use Descriptive Parameters - Meaningful parameter names in multi-parameter lambdas
Lambda expressions fundamentally changed Java programming by making code more concise, readable, and enabling functional programming styles while maintaining backward compatibility.