Lambda Expressions and Functional Interfaces in Java

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.

Example:
// 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

Example:
// 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

Example:
// 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.

Important: A functional interface can have multiple default and static methods, but must have exactly one abstract method.

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)

Example:
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)

Example:
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)

Example:
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()

Example:
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

Example:
// 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

Example:
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

Example:
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

Example:
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) -> expression or (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::method or instance::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

Post a Comment

0 Comments