Skip to content

10.3. Lambda Expressions

Lambda expressions provide a clear and concise way to represent instances of functional interfaces using an expression syntax. They enable functional programming features in Java.

Lambda Expression Syntax

java
(parameters) -> expression

or

java
(parameters) -> { statements; }

Basic Lambda Examples

Simple Lambda Expressions

java
import java.util.function.Supplier;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;

public class BasicLambdaExpressions {
    public static void main(String[] args) {
        // Supplier - no parameters, returns value
        Supplier<String> helloSupplier = () -> "Hello, World!";
        System.out.println(helloSupplier.get());  // Output: Hello, World!

        // Consumer - one parameter, no return value
        Consumer<String> printer = message -> System.out.println(message);
        printer.accept("Lambda expressions are powerful!");

        // Function - one parameter, returns value
        Function<Integer, String> converter = number -> "Number: " + number;
        System.out.println(converter.apply(42));  // Output: Number: 42

        // Predicate - one parameter, returns boolean
        Predicate<Integer> isEven = num -> num % 2 == 0;
        System.out.println("Is 10 even? " + isEven.test(10));  // Output: Is 10 even? true
    }
}

Lambda with Multiple Parameters

java
import java.util.function.BiFunction;
import java.util.function.BiPredicate;

public class MultiParamLambda {
    public static void main(String[] args) {
        // BiFunction - two parameters, returns value
        BiFunction<Integer, Integer, Integer> adder = (a, b) -> a + b;
        System.out.println("Sum: " + adder.apply(5, 3));  // Output: Sum: 8

        // BiPredicate - two parameters, returns boolean
        BiPredicate<String, String> stringComparator = (s1, s2) -> s1.equals(s2);
        System.out.println("Strings equal? " + stringComparator.test("hello", "hello"));

        // Complex lambda with multiple statements
        BiFunction<Integer, Integer, String> complexOperation = (x, y) -> {
            int sum = x + y;
            int product = x * y;
            return "Sum: " + sum + ", Product: " + product;
        };
        System.out.println(complexOperation.apply(4, 5));  // Output: Sum: 9, Product: 20
    }
}

Lambda Features

Type Inference

java
import java.util.function.Supplier;
import java.util.function.Function;

public class TypeInference {
    public static void main(String[] args) {
        // Java can infer types from context
        Supplier<String> supplier = () -> "Inferred type";
        Function<Integer, String> function = num -> "Number: " + num;

        // Explicit types (optional)
        Supplier<String> explicitSupplier = () -> "Explicit";
        Function<Integer, String> explicitFunction = (Integer num) -> "Number: " + num;

        System.out.println(supplier.get());
        System.out.println(function.apply(100));
    }
}

Capturing Variables

java
import java.util.function.Supplier;
import java.util.function.Consumer;

public class VariableCapture {
    public static void main(String[] args) {
        final String constant = "Constant";
        String effectivelyFinal = "Effectively Final";
        int counter = 0;

        // Can capture final and effectively final variables
        Supplier<String> constantSupplier = () -> constant;
        Supplier<String> effectivelyFinalSupplier = () -> effectivelyFinal;

        // Cannot capture and modify non-final variables
        // Consumer<String> invalid = s -> counter++;  // Compilation error

        System.out.println(constantSupplier.get());
        System.out.println(effectivelyFinalSupplier.get());
    }
}

Practical Lambda Examples

Custom Functional Interfaces

java
@FunctionalInterface
interface StringProcessor {
    String process(String input);

    // Can have default methods
    default StringProcessor andThen(StringProcessor after) {
        return input -> after.process(this.process(input));
    }
}

@FunctionalInterface
interface Validator {
    boolean validate(String input);

    default Validator and(Validator other) {
        return input -> this.validate(input) && other.validate(input);
    }
}

public class CustomFunctionalInterfaces {
    public static void main(String[] args) {
        // Using custom functional interface
        StringProcessor toUpper = s -> s.toUpperCase();
        StringProcessor addExclamation = s -> s + "!";

        // Method chaining with default methods
        StringProcessor processor = toUpper.andThen(addExclamation);
        System.out.println(processor.process("hello"));  // Output: HELLO!

        // Validator composition
        Validator notEmpty = s -> s != null && !s.trim().isEmpty();
        Validator minLength = s -> s.length() >= 3;

        Validator combinedValidator = notEmpty.and(minLength);
        System.out.println("Valid: " + combinedValidator.validate("abc"));  // Output: Valid: true
        System.out.println("Valid: " + combinedValidator.validate("a"));    // Output: Valid: false
    }
}

Event Handling with Lambdas

java
import java.util.function.Consumer;
import java.util.ArrayList;
import java.util.List;

class Button {
    private String text;
    private List<Consumer<Button>> clickListeners = new ArrayList<>();

    public Button(String text) {
        this.text = text;
    }

    public void addClickListener(Consumer<Button> listener) {
        clickListeners.add(listener);
    }

    public void click() {
        System.out.println("Button '" + text + "' clicked!");
        clickListeners.forEach(listener -> listener.accept(this));
    }

    public String getText() { return text; }
}

public class EventHandlingLambda {
    public static void main(String[] args) {
        Button saveButton = new Button("Save");
        Button cancelButton = new Button("Cancel");

        // Add click listeners using lambdas
        saveButton.addClickListener(button ->
            System.out.println("Saving data for: " + button.getText()));

        saveButton.addClickListener(button ->
            System.out.println("Logging save action"));

        cancelButton.addClickListener(button ->
            System.out.println("Cancelling operation"));

        // Simulate button clicks
        saveButton.click();
        cancelButton.click();
    }
}

Lambda vs Anonymous Class

Anonymous Class (Pre-Java 8)

java
Supplier<String> anonymousSupplier = new Supplier<String>() {
    @Override
    public String get() {
        return "Hello from anonymous class";
    }
};

Lambda Expression (Java 8+)

java
Supplier<String> lambdaSupplier = () -> "Hello from lambda";

Best Practices

  1. Keep lambdas short and focused: Single responsibility
  2. Use method references when possible: More readable
  3. Avoid complex logic: Extract to methods if needed
  4. Use meaningful parameter names: Better than single letters
java
import java.util.function.Function;
import java.util.function.Predicate;

public class LambdaBestPractices {
    public static void main(String[] args) {
        // Good - clear and concise
        Function<String, Integer> stringLength = s -> s.length();
        Predicate<String> isValidEmail = email -> email.contains("@") && email.contains(".");

        // Better - with meaningful parameter names
        Function<String, Integer> stringLengthGood = inputString -> inputString.length();
        Predicate<String> isValidEmailGood = emailAddress ->
            emailAddress.contains("@") && emailAddress.contains(".");

        // Avoid - too complex, should be a method
        Function<String, String> complexFormatter = input -> {
            if (input == null) return "Unknown";
            String trimmed = input.trim();
            if (trimmed.isEmpty()) return "Empty";
            return trimmed.substring(0, 1).toUpperCase() +
                   trimmed.substring(1).toLowerCase();
        };

        // Better - extract complex logic to method
        Function<String, String> betterFormatter = LambdaBestPractices::formatString;
    }

    private static String formatString(String input) {
        if (input == null) return "Unknown";
        String trimmed = input.trim();
        if (trimmed.isEmpty()) return "Empty";
        return trimmed.substring(0, 1).toUpperCase() +
               trimmed.substring(1).toLowerCase();
    }
}