Master lambda expressions, functional interfaces, method references, and common functional interfaces for the OCP 21 exam.
Table of Contents
1. Lambda Expressions Overview
Lambda expressions (introduced in Java 8) provide a concise way to represent anonymous functions. They enable functional programming in Java and are essential for working with the Stream API.
1.1 What are Lambda Expressions?
Lambda expressions are anonymous functions that can be passed around as values. They consist of parameters, an arrow token (->), and a body.
// Traditional anonymous inner class
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
// Lambda expression (equivalent)
Runnable r2 = () -> System.out.println("Hello");
// Both can be executed
r1.run();
r2.run();
1.2 Benefits of Lambda Expressions
- Conciseness: Reduces boilerplate code
- Readability: More readable for simple operations
- Functional Programming: Enables functional programming paradigms
- Stream API: Essential for working with streams
2. Lambda Expression Syntax
The syntax of a lambda expression is: (parameters) -> expression or (parameters) -> { statements }
2.1 Syntax Variations
// No parameters
Runnable r = () -> System.out.println("Hello");
// Single parameter (parentheses optional)
Function<String, Integer> f1 = (s) -> s.length();
Function<String, Integer> f2 = s -> s.length();
// Multiple parameters
BinaryOperator<Integer> add = (a, b) -> a + b;
// With type declarations
BinaryOperator<Integer> add2 = (Integer a, Integer b) -> a + b;
// Expression body (single expression)
Function<Integer, Integer> square = x -> x * x;
// Block body (multiple statements)
Function<Integer, Integer> square2 = x -> {
int result = x * x;
return result;
};
// Returning values
Function<String, String> upper = s -> s.toUpperCase();
Function<String, String> upper2 = s -> {
return s.toUpperCase();
};
2.2 Syntax Rules
- Parentheses required for zero or multiple parameters
- Parentheses optional for single parameter (if type not specified)
- Type declarations optional (inferred from context)
- Braces required for multiple statements
- Return statement required in block body if return type is not void
3. Functional Interfaces
A functional interface is an interface with exactly one abstract method. Lambda expressions can be used wherever a functional interface is expected.
3.1 Defining Functional Interfaces
// Functional interface (one abstract method)
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
// Can have default methods
default void printResult(int result) {
System.out.println("Result: " + result);
}
// Can have static methods
static Calculator getAddition() {
return (a, b) -> a + b;
}
}
// Using the functional interface
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;
int sum = add.calculate(5, 3); // 8
int product = multiply.calculate(5, 3); // 15
3.2 @FunctionalInterface Annotation
The @FunctionalInterface annotation is optional but recommended. It ensures the interface has exactly one abstract method and provides compile-time checking.
4. Common Functional Interfaces
Java provides several built-in functional interfaces in the java.util.function package.
4.1 Predicate
Predicate<T> represents a boolean-valued function. Method: boolean test(T t)
Predicate<String> isEmpty = s -> s.isEmpty();
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> isPositive = n -> n > 0;
System.out.println(isEmpty.test("")); // true
System.out.println(isEven.test(4)); // true
System.out.println(isPositive.test(-5)); // false
// Chaining predicates
Predicate<Integer> isEvenAndPositive = isEven.and(isPositive);
System.out.println(isEvenAndPositive.test(4)); // true
System.out.println(isEvenAndPositive.test(-4)); // false
// Negating
Predicate<Integer> isOdd = isEven.negate();
System.out.println(isOdd.test(5)); // true
4.2 Function
Function<T, R> represents a function that takes one argument and produces a result. Method: R apply(T t)
Function<String, Integer> length = s -> s.length();
Function<Integer, Integer> square = x -> x * x;
Function<String, String> upper = s -> s.toUpperCase();
System.out.println(length.apply("Hello")); // 5
System.out.println(square.apply(5)); // 25
System.out.println(upper.apply("hello")); // HELLO
// Composing functions
Function<String, Integer> lengthThenSquare = length.andThen(square);
System.out.println(lengthThenSquare.apply("Hi")); // 4 (2*2)
Function<String, Integer> squareThenLength = length.compose(square);
// This would require String input, so not practical here
4.3 Consumer
Consumer<T> represents an operation that accepts a single argument and returns no result. Method: void accept(T t)
Consumer<String> print = s -> System.out.println(s);
Consumer<Integer> squareAndPrint = n -> System.out.println(n * n);
print.accept("Hello"); // Prints: Hello
squareAndPrint.accept(5); // Prints: 25
// Chaining consumers
Consumer<String> printUpper = print.andThen(s -> System.out.println(s.toUpperCase()));
printUpper.accept("hello"); // Prints: hello\nHELLO
4.4 Supplier
Supplier<T> represents a supplier of results. Method: T get()
Supplier<String> getMessage = () -> "Hello World";
Supplier<Integer> getRandom = () -> (int)(Math.random() * 100);
Supplier<LocalDateTime> getNow = () -> LocalDateTime.now();
System.out.println(getMessage.get()); // Hello World
System.out.println(getRandom.get()); // Random number
System.out.println(getNow.get()); // Current date-time
4.5 Other Common Functional Interfaces
| Interface | Method | Description |
|---|---|---|
| BiFunction<T, U, R> | R apply(T t, U u) | Two arguments, one result |
| BiPredicate<T, U> | boolean test(T t, U u) | Two arguments, boolean result |
| BiConsumer<T, U> | void accept(T t, U u) | Two arguments, no result |
| UnaryOperator<T> | T apply(T t) | Function where input and output are same type |
| BinaryOperator<T> | T apply(T t1, T t2) | BiFunction where all types are same |
5. Method References
Method references provide a shorthand syntax for lambda expressions that call existing methods.
5.1 Types of Method References
// 1. Static method reference
Function<String, Integer> parseInt = Integer::parseInt;
// Equivalent to: s -> Integer.parseInt(s)
// 2. Instance method on specific object
String str = "Hello";
Supplier<Integer> length = str::length;
// Equivalent to: () -> str.length()
// 3. Instance method on arbitrary object
Function<String, String> upper = String::toUpperCase;
// Equivalent to: s -> s.toUpperCase()
// 4. Constructor reference
Supplier<ArrayList> listSupplier = ArrayList::new;
// Equivalent to: () -> new ArrayList()
Function<Integer, ArrayList> listWithSize = ArrayList::new;
// Equivalent to: size -> new ArrayList(size)
5.2 Using Method References
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// Using lambda
names.forEach(s -> System.out.println(s));
// Using method reference
names.forEach(System.out::println);
// Using lambda
List<String> upper = names.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
// Using method reference
List<String> upper2 = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
6. Variable Capture in Lambdas
Lambda expressions can access variables from their enclosing scope, but with restrictions.
6.1 Effectively Final Variables
int x = 10;
String message = "Hello";
// Lambda can access effectively final variables
Runnable r = () -> {
System.out.println(x); // OK - x is effectively final
System.out.println(message); // OK - message is effectively final
};
// Cannot modify captured variables
int y = 20;
Runnable r2 = () -> {
// y++; // Compilation error - cannot modify
System.out.println(y); // OK - can read
};
// Variable must be effectively final
int z = 30;
if (true) {
z = 40; // Modifying z
}
// Runnable r3 = () -> System.out.println(z); // Error - z not effectively final
6.2 Instance and Static Variables
class Example {
private int instanceVar = 10;
private static int staticVar = 20;
public void test() {
// Can access and modify instance variables
Runnable r1 = () -> {
instanceVar++; // OK
System.out.println(instanceVar);
};
// Can access and modify static variables
Runnable r2 = () -> {
staticVar++; // OK
System.out.println(staticVar);
};
// Local variables must be effectively final
int localVar = 30;
Runnable r3 = () -> {
// localVar++; // Error - cannot modify
System.out.println(localVar); // OK - can read
};
}
}
7. Exam Key Points
Critical Concepts for OCP 21 Exam:
- Lambda Syntax:
(parameters) -> expressionor(parameters) -> { statements } - Functional Interface: Interface with exactly one abstract method
- @FunctionalInterface: Optional annotation for compile-time checking
- Predicate:
boolean test(T t)- returns boolean - Function:
R apply(T t)- takes input, returns output - Consumer:
void accept(T t)- takes input, no return - Supplier:
T get()- no input, returns value - Method References:
Class::methodorinstance::method - Variable Capture: Local variables must be effectively final
- Instance/Static Variables: Can be modified in lambdas
- Parentheses: Required for zero or multiple parameters
- Type Inference: Types can be omitted if compiler can infer
- Block Body: Braces and return statement required for multiple statements
- Expression Body: Single expression, no return keyword needed
0 Comments