Java Notes
Master Lambda Expressions in Java 8 — syntax, functional programming paradigm, type inference, variable capture, and real-world usage patterns with comprehensive examples.
What is a Lambda Expression?
A lambda expression is a concise way to represent an anonymous function — a block of code that can be passed around as a value. Introduced in Java 8, lambdas brought functional programming capabilities to Java, enabling you to treat behavior as data.
Before Java 8, the only way to pass behavior was through anonymous inner classes:
// Before Java 8 - Anonymous Inner Class
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Running in a thread");
}
};
// Java 8 - Lambda Expression
Runnable runnable = () -> System.out.println("Running in a thread");Running in a thread
The lambda version is not just shorter — it expresses intent rather than ceremony.
How Lambdas Work Internally
Lambda vs Anonymous Class
Lambdas are NOT syntactic sugar for anonymous classes. They differ fundamentally:
| Feature | Anonymous Class | Lambda |
|---|---|---|
| Compiled to | Separate .class file | invokedynamic bytecode |
this keyword | Refers to anonymous class instance | Refers to enclosing class |
| Memory | New object every time | Can be cached/reused by JVM |
| Performance | Slower (class loading overhead) | Faster (no class loading) |
public class ThisDemo {
private String name = "Outer";
public void demonstrate() {
// Anonymous class - 'this' refers to Runnable instance
Runnable anon = new Runnable() {
private String name = "Inner";
@Override
public void run() {
System.out.println(this.name); // Prints "Inner"
}
};
// Lambda - 'this' refers to ThisDemo instance
Runnable lambda = () -> {
System.out.println(this.name); // Prints "Outer"
};
}
}Prints "Inner" Prints "Outer"
InvokeDynamic Under the Hood
When you write a lambda, the compiler generates:
- A bootstrap method using
LambdaMetafactory - The actual lambda body as a private method in the enclosing class
- An
invokedynamicinstruction that links them at runtime
// This lambda:
Function<String, Integer> f = s -> s.length();
// Generates something like:
private static Integer lambda$main$0(String s) {
return s.length();
}
// + invokedynamic linking at first callVariable Capture and Effectively Final
Lambdas can capture variables from their enclosing scope, but with restrictions:
public class CaptureDemo {
private int instanceVar = 10; // Instance variable - freely mutable
private static int staticVar = 20; // Static variable - freely mutable
public void demo() {
int localVar = 30; // Local variable - must be effectively final
final int finalVar = 40; // Explicitly final
Consumer<Integer> lambda = (x) -> {
// ✅ Can read and modify instance variables
instanceVar = x + 1;
// ✅ Can read and modify static variables
staticVar = x + 2;
// ✅ Can READ local variables (if effectively final)
System.out.println(localVar); // OK
// ❌ CANNOT modify local variables
// localVar = x + 3; // Compilation error!
// ✅ Can read final variables
System.out.println(finalVar);
};
// ❌ This would make localVar NOT effectively final
// localVar = 50; // If uncommented, the lambda above won't compile
}
}OK
Why Effectively Final?
Local variables live on the stack and are destroyed when the method exits. The lambda may outlive the method (e.g., stored in a field). Java copies the value into the lambda — if the variable could change after copying, the lambda would have a stale value.
Practical Lambda Patterns
Pattern 1: Strategy Pattern Replacement
// Traditional Strategy Pattern
interface SortStrategy {
void sort(int[] array);
}
class BubbleSort implements SortStrategy { /* ... */ }
class QuickSort implements SortStrategy { /* ... */ }
// With Lambdas - No need for concrete classes
public class Sorter {
public void sort(int[] array, Comparator<Integer> strategy) {
// Use strategy
}
public static void main(String[] args) {
Sorter sorter = new Sorter();
int[] data = {5, 2, 8, 1, 9};
// Ascending
Arrays.sort(data);
// Custom comparator as lambda
Integer[] boxed = Arrays.stream(data).boxed().toArray(Integer[]::new);
Arrays.sort(boxed, (a, b) -> b - a); // Descending
}
}Pattern 2: Event Handling
// Swing - Before Java 8
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Button clicked!");
}
});
// Swing - With Lambda
button.addActionListener(e -> System.out.println("Button clicked!"));
// JavaFX
button.setOnAction(event -> {
String text = textField.getText();
label.setText("Hello, " + text);
});Button clicked!
Pattern 3: Custom Functional Interfaces
@FunctionalInterface
interface Transformer<T, R> {
R transform(T input);
// Default method for chaining
default <V> Transformer<T, V> andThen(Transformer<R, V> after) {
return input -> after.transform(this.transform(input));
}
}
public class TransformerDemo {
public static void main(String[] args) {
Transformer<String, Integer> length = String::length;
Transformer<Integer, String> toWord = n -> n > 5 ? "long" : "short";
Transformer<String, String> classify = length.andThen(toWord);
System.out.println(classify.transform("Hello")); // "short"
System.out.println(classify.transform("Functional")); // "long"
}
}"short" "long"
Pattern 4: Lazy Evaluation
public class LazyLogger {
private boolean debugEnabled = false;
// Without lambda - string concatenation always happens
public void logExpensive(String message) {
if (debugEnabled) {
System.out.println(message);
}
}
// With lambda - computation only happens if needed
public void logLazy(Supplier<String> messageSupplier) {
if (debugEnabled) {
System.out.println(messageSupplier.get());
}
}
public static void main(String[] args) {
LazyLogger logger = new LazyLogger();
// Expensive computation ALWAYS runs (wasted if debug disabled)
logger.logExpensive("Result: " + expensiveComputation());
// Expensive computation only runs if debug is enabled
logger.logLazy(() -> "Result: " + expensiveComputation());
}
}Pattern 5: Builder with Validation
public class Validator<T> {
private final List<Predicate<T>> validators = new ArrayList<>();
public Validator<T> addRule(Predicate<T> rule) {
validators.add(rule);
return this;
}
public boolean validate(T object) {
return validators.stream().allMatch(v -> v.test(object));
}
public static void main(String[] args) {
Validator<String> passwordValidator = new Validator<String>()
.addRule(s -> s.length() >= 8)
.addRule(s -> s.chars().anyMatch(Character::isUpperCase))
.addRule(s -> s.chars().anyMatch(Character::isDigit))
.addRule(s -> s.chars().anyMatch(c -> "!@#$%".indexOf(c) >= 0));
System.out.println(passwordValidator.validate("Weak")); // false
System.out.println(passwordValidator.validate("Str0ng!Pass")); // true
}
}false true
Lambda with Collections
import java.util.*;
import java.util.stream.*;
public class LambdaCollections {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");
// forEach with lambda
names.forEach(name -> System.out.println(name));
// Sort with lambda
names.sort((a, b) -> a.length() - b.length());
// removeIf with lambda
List<String> mutable = new ArrayList<>(names);
mutable.removeIf(name -> name.length() < 4);
// replaceAll with lambda
mutable.replaceAll(String::toUpperCase);
// Map operations with lambda
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 85);
scores.put("Bob", 92);
scores.put("Charlie", 78);
scores.forEach((name, score) ->
System.out.println(name + ": " + score));
scores.replaceAll((name, score) -> score + 5); // Curve grades
scores.compute("Alice", (name, score) -> score > 90 ? score : score + 2);
scores.merge("David", 88, Integer::max);
}
}Common Mistakes and Pitfalls
1. Modifying External State (Side Effects)
// ❌ BAD - Side effects in lambda
List<Integer> results = new ArrayList<>();
IntStream.range(0, 1000).parallel().forEach(i -> results.add(i)); // Race condition!
// ✅ GOOD - Use collectors
List<Integer> results = IntStream.range(0, 1000)
.parallel()
.boxed()
.collect(Collectors.toList());2. Exception Handling
// ❌ Lambdas don't play well with checked exceptions
// list.forEach(item -> parseItem(item)); // Won't compile if parseItem throws
// ✅ Wrap checked exceptions
@FunctionalInterface
interface ThrowingConsumer<T> {
void accept(T t) throws Exception;
static <T> Consumer<T> unchecked(ThrowingConsumer<T> consumer) {
return t -> {
try {
consumer.accept(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
}
// Usage
list.forEach(ThrowingConsumer.unchecked(item -> parseItem(item)));3. Overusing Lambdas
// ❌ Too complex - hard to read and debug
Function<List<Map<String, Object>>, Map<String, List<Object>>> processor =
data -> data.stream()
.filter(m -> m.containsKey("type") && ((String)m.get("type")).startsWith("A"))
.collect(Collectors.groupingBy(
m -> (String)m.get("category"),
Collectors.mapping(m -> m.get("value"), Collectors.toList())
));
// ✅ Extract to named methods for clarity
private boolean isTypeA(Map<String, Object> m) {
return m.containsKey("type") && ((String) m.get("type")).startsWith("A");
}
private String getCategory(Map<String, Object> m) {
return (String) m.get("category");
}Performance Considerations
// Lambda creation is very cheap (no class loading)
// But avoid creating lambdas in tight loops if possible
// ❌ Creates a new Comparator instance each iteration
for (List<Integer> list : lists) {
list.sort((a, b) -> a - b);
}
// ✅ Reuse the lambda reference
Comparator<Integer> comparator = (a, b) -> a - b;
for (List<Integer> list : lists) {
list.sort(comparator);
}
// Note: JVM may optimize the first form too, but explicit reuse is clearerInterview Questions
Q1: What is the difference between a lambda expression and an anonymous class?
Answer: Lambdas use invokedynamic (faster, no class file), this refers to the enclosing class, they can only implement functional interfaces (single abstract method), and they cannot have state (instance variables).
Q2: What does "effectively final" mean?
Answer: A variable is effectively final if its value is never changed after initialization. Lambdas can capture local variables only if they are final or effectively final, because lambdas capture the value (not the variable reference).
Q3: Can a lambda throw checked exceptions?
Answer: Only if the functional interface's abstract method declares the exception. Standard interfaces like Consumer, Function don't declare checked exceptions. You must wrap checked exceptions in unchecked ones or create custom functional interfaces.
Q4: How does the JVM handle lambda expressions internally?
Answer: The compiler generates a private method for the lambda body and an invokedynamic instruction. At runtime, LambdaMetafactory creates a lightweight implementation of the target interface that calls the private method.
Q5: Can a lambda access local variables from the enclosing method?
Answer: Yes, but only if they are effectively final. Instance and static variables can be accessed and modified freely because they live on the heap.
Q6: What happens if you use a parallel stream and your lambda has side effects?
Answer: Race conditions, data corruption, and unpredictable results. Lambdas used with parallel streams must be stateless, non-interfering, and associative.
Q7: Write a lambda that takes two strings and returns the longer one.
BinaryOperator<String> longer = (a, b) -> a.length() >= b.length() ? a : b;Q8: How do you handle multiple return statements in a lambda?
Answer: Use a block body with curly braces and explicit return statements:
Function<Integer, String> classify = x -> {
if (x < 0) return "negative";
if (x == 0) return "zero";
return "positive";
};Summary
In this chapter, we learned about Lambda Expression in Java 8 in detail. Here are the key points:
- Basic introduction to the concept and why it is needed
- Syntax and structure with complete examples
- Internal working and best practices
- Real-world applications and use cases
- Common mistakes and how to avoid them
- How this topic is asked in interviews
To practice these concepts, write and run the code in your IDE and verify the output. Modify each example and experiment so that you deeply understand the concept.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Lambda Expression in Java 8.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Java Master Course topic.
Search Terms
java-master-course, java master course, java, master, course, features, lambda, expression
Related Java Master Course Topics