1. java
  2. /advanced
  3. /lambda

Master Java Lambda Expressions and Functional Programming

Java Lambda Expressions

Lambda expressions, introduced in Java 8, provide a concise way to represent anonymous functions. They enable functional programming paradigms in Java and are particularly powerful when combined with the Streams API and functional interfaces.

What are Lambda Expressions?

Lambda expressions are anonymous functions that can be passed as arguments, stored in variables, and returned from methods. They provide a more readable and concise alternative to anonymous inner classes for single-method interfaces.

import java.util.*;
import java.util.function.*;

public class LambdaIntroduction {
    public static void main(String[] args) {
        // Traditional approach with anonymous inner class
        Runnable oldWay = new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello from anonymous class");
            }
        };
        
        // Lambda expression approach
        Runnable newWay = () -> System.out.println("Hello from lambda");
        
        // Execute both
        oldWay.run();
        newWay.run();
        
        // Lambda with parameters
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        
        // Old way
        names.forEach(new Consumer<String>() {
            @Override
            public void accept(String name) {
                System.out.println("Hello, " + name);
            }
        });
        
        // Lambda way
        names.forEach(name -> System.out.println("Hello, " + name));
        
        // Even more concise with method reference
        names.forEach(System.out::println);
    }
}

Lambda Syntax

Lambda expressions have several syntax forms depending on complexity:

import java.util.function.*;

public class LambdaSyntax {
    public static void main(String[] args) {
        // 1. No parameters
        Runnable noParams = () -> System.out.println("No parameters");
        
        // 2. Single parameter (parentheses optional)
        Consumer<String> singleParam = name -> System.out.println("Hello " + name);
        Consumer<String> singleParamWithParens = (name) -> System.out.println("Hello " + name);
        
        // 3. Multiple parameters
        BinaryOperator<Integer> multipleParams = (a, b) -> a + b;
        
        // 4. Explicit parameter types
        BinaryOperator<Integer> explicitTypes = (Integer a, Integer b) -> a + b;
        
        // 5. Block body with multiple statements
        Function<String, String> blockBody = (input) -> {
            String processed = input.toUpperCase();
            processed = processed.trim();
            return "Processed: " + processed;
        };
        
        // 6. Single expression (return is implicit)
        Function<Integer, Integer> singleExpression = x -> x * 2;
        
        // Test the lambdas
        noParams.run();
        singleParam.accept("Alice");
        System.out.println("Sum: " + multipleParams.apply(5, 3));
        System.out.println(blockBody.apply("  hello world  "));
        System.out.println("Double: " + singleExpression.apply(10));
    }
}

Functional Interfaces

Lambda expressions work with functional interfaces - interfaces with exactly one abstract method:

Built-in Functional Interfaces

import java.util.*;
import java.util.function.*;

public class FunctionalInterfacesDemo {
    public static void main(String[] args) {
        // Predicate<T> - boolean test(T t)
        Predicate<String> isEmpty = String::isEmpty;
        Predicate<Integer> isEven = n -> n % 2 == 0;
        Predicate<String> startsWithA = s -> s.startsWith("A");
        
        System.out.println("Is empty: " + isEmpty.test(""));
        System.out.println("Is even: " + isEven.test(4));
        System.out.println("Starts with A: " + startsWithA.test("Apple"));
        
        // Function<T, R> - R apply(T t)
        Function<String, Integer> stringLength = String::length;
        Function<Integer, String> intToString = Object::toString;
        Function<String, String> toUpperCase = String::toUpperCase;
        
        System.out.println("Length: " + stringLength.apply("Hello"));
        System.out.println("String: " + intToString.apply(42));
        System.out.println("Uppercase: " + toUpperCase.apply("hello"));
        
        // Consumer<T> - void accept(T t)
        Consumer<String> printer = System.out::println;
        Consumer<List<String>> listPrinter = list -> list.forEach(System.out::println);
        
        printer.accept("Hello World");
        listPrinter.accept(Arrays.asList("A", "B", "C"));
        
        // Supplier<T> - T get()
        Supplier<Double> randomValue = Math::random;
        Supplier<String> greeting = () -> "Hello";
        Supplier<List<String>> listSupplier = ArrayList::new;
        
        System.out.println("Random: " + randomValue.get());
        System.out.println("Greeting: " + greeting.get());
        System.out.println("New list: " + listSupplier.get());
        
        // BinaryOperator<T> - T apply(T t1, T t2)
        BinaryOperator<Integer> add = (a, b) -> a + b;
        BinaryOperator<Integer> multiply = (a, b) -> a * b;
        BinaryOperator<String> concat = (a, b) -> a + " " + b;
        
        System.out.println("Add: " + add.apply(5, 3));
        System.out.println("Multiply: " + multiply.apply(5, 3));
        System.out.println("Concat: " + concat.apply("Hello", "World"));
        
        // UnaryOperator<T> - T apply(T t)
        UnaryOperator<String> toUpper = String::toUpperCase;
        UnaryOperator<Integer> square = x -> x * x;
        
        System.out.println("Upper: " + toUpper.apply("hello"));
        System.out.println("Square: " + square.apply(5));
        
        // BiFunction<T, U, R> - R apply(T t, U u)
        BiFunction<String, String, String> stringConcat = (a, b) -> a + " " + b;
        BiFunction<Integer, Integer, Integer> max = Math::max;
        
        System.out.println("Concat: " + stringConcat.apply("Hello", "World"));
        System.out.println("Max: " + max.apply(10, 5));
    }
}

Custom Functional Interfaces

@FunctionalInterface
interface Calculator {
    double calculate(double a, double b);
    
    // Default methods are allowed
    default void printResult(double a, double b) {
        System.out.println("Result: " + calculate(a, b));
    }
    
    // Static methods are allowed
    static Calculator getAdder() {
        return (a, b) -> a + b;
    }
}

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

@FunctionalInterface
interface TriFunction<T, U, V, R> {
    R apply(T t, U u, V v);
}

public class CustomFunctionalInterfaces {
    public static void main(String[] args) {
        // Calculator examples
        Calculator adder = (a, b) -> a + b;
        Calculator multiplier = (a, b) -> a * b;
        Calculator divider = (a, b) -> b != 0 ? a / b : 0;
        
        adder.printResult(10, 5);
        multiplier.printResult(10, 5);
        divider.printResult(10, 5);
        
        Calculator staticAdder = Calculator.getAdder();
        staticAdder.printResult(7, 3);
        
        // StringProcessor examples
        StringProcessor upperCase = String::toUpperCase;
        StringProcessor reverse = s -> new StringBuilder(s).reverse().toString();
        StringProcessor addPrefix = s -> "Processed: " + s;
        
        System.out.println(upperCase.process("hello"));
        System.out.println(reverse.process("hello"));
        System.out.println(addPrefix.process("data"));
        
        // TriFunction example
        TriFunction<String, Integer, Boolean, String> formatter = 
            (text, number, flag) -> flag ? text.toUpperCase() + ":" + number : text + ":" + number;
        
        System.out.println(formatter.apply("hello", 42, true));
        System.out.println(formatter.apply("world", 24, false));
    }
}

Method References

Method references provide a shorthand for lambda expressions that call a single method:

import java.util.*;
import java.util.function.*;

public class MethodReferences {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
        
        // 1. Static method references
        // Lambda: x -> Integer.parseInt(x)
        Function<String, Integer> parseInt = Integer::parseInt;
        System.out.println("Parsed: " + parseInt.apply("123"));
        
        // Lambda: (a, b) -> Math.max(a, b)
        BinaryOperator<Integer> max = Math::max;
        System.out.println("Max: " + max.apply(10, 5));
        
        // 2. Instance method references on particular object
        String prefix = "Hello ";
        // Lambda: s -> prefix.concat(s)
        Function<String, String> addPrefix = prefix::concat;
        System.out.println(addPrefix.apply("World"));
        
        // Lambda: s -> System.out.println(s)
        Consumer<String> printer = System.out::println;
        names.forEach(printer);
        
        // 3. Instance method references on arbitrary object
        // Lambda: s -> s.length()
        Function<String, Integer> stringLength = String::length;
        names.stream()
             .map(stringLength)
             .forEach(System.out::println);
        
        // Lambda: s -> s.toUpperCase()
        Function<String, String> toUpper = String::toUpperCase;
        names.stream()
             .map(toUpper)
             .forEach(System.out::println);
        
        // Lambda: (s1, s2) -> s1.compareToIgnoreCase(s2)
        Comparator<String> caseInsensitiveComparator = String::compareToIgnoreCase;
        names.sort(caseInsensitiveComparator);
        System.out.println("Sorted: " + names);
        
        // 4. Constructor references
        // Lambda: () -> new ArrayList<>()
        Supplier<List<String>> listSupplier = ArrayList::new;
        List<String> newList = listSupplier.get();
        
        // Lambda: size -> new ArrayList<>(size)
        Function<Integer, List<String>> listWithCapacity = ArrayList::new;
        List<String> listWith10Capacity = listWithCapacity.apply(10);
        
        // Lambda: s -> new Person(s)
        Function<String, Person> personCreator = Person::new;
        Person person = personCreator.apply("John");
        System.out.println("Created person: " + person.getName());
        
        // Array constructor reference
        // Lambda: size -> new String[size]
        IntFunction<String[]> stringArrayCreator = String[]::new;
        String[] stringArray = stringArrayCreator.apply(5);
        System.out.println("Created array of length: " + stringArray.length);
    }
    
    static class Person {
        private String name;
        
        public Person(String name) {
            this.name = name;
        }
        
        public String getName() {
            return name;
        }
    }
}

Variable Capture and Closures

Lambda expressions can capture variables from their enclosing scope:

import java.util.*;
import java.util.function.*;

public class VariableCapture {
    private String instanceField = "Instance field";
    private static String staticField = "Static field";
    
    public static void main(String[] args) {
        new VariableCapture().demonstrateClosure();
    }
    
    public void demonstrateClosure() {
        String localVariable = "Local variable";
        final String finalLocal = "Final local";
        
        // Effectively final variable
        String effectivelyFinal = "Effectively final";
        
        // Lambda can access:
        // 1. Static fields
        // 2. Instance fields (if lambda is in instance method)
        // 3. Final or effectively final local variables
        Runnable lambda = () -> {
            System.out.println("Static field: " + staticField);
            System.out.println("Instance field: " + instanceField);
            System.out.println("Final local: " + finalLocal);
            System.out.println("Effectively final: " + effectivelyFinal);
            // System.out.println("Local variable: " + localVariable); // Error!
        };
        
        lambda.run();
        
        // This would make effectivelyFinal not effectively final
        // effectivelyFinal = "Changed"; // Uncommenting this causes compilation error
        
        // Demonstrate closure with method parameters
        processWithCallback("Hello", message -> {
            System.out.println("Processing: " + message);
            System.out.println("Instance field in callback: " + instanceField);
        });
        
        // Closure with collections
        String prefix = "Item: ";
        List<String> items = Arrays.asList("A", "B", "C");
        items.stream()
             .map(item -> prefix + item) // Captures prefix
             .forEach(System.out::println);
        
        // Counter example showing mutable closure
        createCounters();
    }
    
    public void processWithCallback(String message, Consumer<String> callback) {
        callback.accept(message);
    }
    
    public void createCounters() {
        List<Supplier<Integer>> counters = new ArrayList<>();
        
        // This won't work - local variables in lambda must be final/effectively final
        /*
        int count = 0;
        for (int i = 0; i < 3; i++) {
            counters.add(() -> count++); // Error: count is not effectively final
        }
        */
        
        // Solution 1: Use array (mutable container)
        int[] count = {0};
        for (int i = 0; i < 3; i++) {
            counters.add(() -> ++count[0]);
        }
        
        // Solution 2: Use AtomicInteger
        java.util.concurrent.atomic.AtomicInteger atomicCount = 
            new java.util.concurrent.atomic.AtomicInteger(0);
        List<Supplier<Integer>> atomicCounters = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            atomicCounters.add(atomicCount::incrementAndGet);
        }
        
        // Test counters
        System.out.println("Array-based counters:");
        counters.forEach(counter -> System.out.println(counter.get()));
        
        System.out.println("Atomic counters:");
        atomicCounters.forEach(counter -> System.out.println(counter.get()));
    }
}

Lambda Expressions with Collections

Lambdas are particularly powerful when working with collections:

import java.util.*;
import java.util.stream.*;

public class LambdaWithCollections {
    public static void main(String[] args) {
        List<Person> people = Arrays.asList(
            new Person("Alice", 30, "Engineer"),
            new Person("Bob", 25, "Designer"),
            new Person("Charlie", 35, "Manager"),
            new Person("David", 28, "Engineer"),
            new Person("Eve", 32, "Designer")
        );
        
        // Sorting with lambdas
        System.out.println("=== Sorting ===");
        
        // Sort by age
        people.sort((p1, p2) -> Integer.compare(p1.getAge(), p2.getAge()));
        System.out.println("Sorted by age: " + people);
        
        // Sort by name using method reference
        people.sort(Comparator.comparing(Person::getName));
        System.out.println("Sorted by name: " + people);
        
        // Complex sorting
        people.sort(Comparator.comparing(Person::getProfession)
                             .thenComparing(Person::getAge));
        System.out.println("Sorted by profession then age: " + people);
        
        // Filtering with lambdas
        System.out.println("\n=== Filtering ===");
        
        // Filter engineers
        List<Person> engineers = people.stream()
            .filter(p -> p.getProfession().equals("Engineer"))
            .collect(Collectors.toList());
        System.out.println("Engineers: " + engineers);
        
        // Filter people over 30
        List<Person> over30 = people.stream()
            .filter(p -> p.getAge() > 30)
            .collect(Collectors.toList());
        System.out.println("Over 30: " + over30);
        
        // Transform with lambdas
        System.out.println("\n=== Transforming ===");
        
        // Get all names in uppercase
        List<String> upperNames = people.stream()
            .map(p -> p.getName().toUpperCase())
            .collect(Collectors.toList());
        System.out.println("Uppercase names: " + upperNames);
        
        // Get name and age strings
        List<String> nameAge = people.stream()
            .map(p -> p.getName() + " (" + p.getAge() + ")")
            .collect(Collectors.toList());
        System.out.println("Name and age: " + nameAge);
        
        // Complex operations
        System.out.println("\n=== Complex Operations ===");
        
        // Group by profession
        Map<String, List<Person>> byProfession = people.stream()
            .collect(Collectors.groupingBy(Person::getProfession));
        System.out.println("Grouped by profession: " + byProfession);
        
        // Average age by profession
        Map<String, Double> avgAgeByProfession = people.stream()
            .collect(Collectors.groupingBy(
                Person::getProfession,
                Collectors.averagingInt(Person::getAge)
            ));
        System.out.println("Average age by profession: " + avgAgeByProfession);
        
        // Find operations
        Optional<Person> youngest = people.stream()
            .min(Comparator.comparing(Person::getAge));
        youngest.ifPresent(p -> System.out.println("Youngest: " + p));
        
        // Check if any engineer exists
        boolean hasEngineer = people.stream()
            .anyMatch(p -> p.getProfession().equals("Engineer"));
        System.out.println("Has engineer: " + hasEngineer);
        
        // Custom collectors
        String allNames = people.stream()
            .map(Person::getName)
            .collect(Collectors.joining(", "));
        System.out.println("All names: " + allNames);
    }
    
    static class Person {
        private String name;
        private int age;
        private String profession;
        
        public Person(String name, int age, String profession) {
            this.name = name;
            this.age = age;
            this.profession = profession;
        }
        
        public String getName() { return name; }
        public int getAge() { return age; }
        public String getProfession() { return profession; }
        
        @Override
        public String toString() {
            return name + "(" + age + ", " + profession + ")";
        }
    }
}

Lambda Expressions with Streams

Lambdas are the foundation of the powerful Streams API:

import java.util.*;
import java.util.stream.*;

public class LambdaWithStreams {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        
        // Basic stream operations
        System.out.println("=== Basic Operations ===");
        
        // Filter even numbers and square them
        List<Integer> evenSquares = numbers.stream()
            .filter(n -> n % 2 == 0)
            .map(n -> n * n)
            .collect(Collectors.toList());
        System.out.println("Even squares: " + evenSquares);
        
        // Sum of all numbers
        int sum = numbers.stream()
            .reduce(0, (a, b) -> a + b);
        System.out.println("Sum: " + sum);
        
        // Alternative sum using method reference
        int sum2 = numbers.stream()
            .reduce(0, Integer::sum);
        System.out.println("Sum (method ref): " + sum2);
        
        // Find first number greater than 5
        Optional<Integer> firstGreaterThan5 = numbers.stream()
            .filter(n -> n > 5)
            .findFirst();
        firstGreaterThan5.ifPresent(n -> System.out.println("First > 5: " + n));
        
        // Parallel processing
        System.out.println("\n=== Parallel Processing ===");
        
        long start = System.nanoTime();
        double result = numbers.parallelStream()
            .mapToDouble(n -> Math.sqrt(n))
            .map(d -> Math.sin(d))
            .sum();
        long end = System.nanoTime();
        System.out.println("Parallel result: " + result + " (time: " + (end - start) / 1_000_000 + " ms)");
        
        // Complex stream pipeline
        System.out.println("\n=== Complex Pipeline ===");
        
        List<String> words = Arrays.asList(
            "apple", "banana", "cherry", "date", "elderberry", "fig", "grape"
        );
        
        Map<Integer, List<String>> wordsByLength = words.stream()
            .filter(word -> word.length() > 4)
            .map(String::toUpperCase)
            .sorted()
            .collect(Collectors.groupingBy(String::length));
        
        System.out.println("Words by length: " + wordsByLength);
        
        // FlatMap example
        List<List<String>> listOfLists = Arrays.asList(
            Arrays.asList("a", "b"),
            Arrays.asList("c", "d", "e"),
            Arrays.asList("f", "g", "h", "i")
        );
        
        List<String> flattened = listOfLists.stream()
            .flatMap(Collection::stream)
            .collect(Collectors.toList());
        System.out.println("Flattened: " + flattened);
        
        // Custom collectors
        String concatenated = words.stream()
            .collect(StringBuilder::new,
                    (sb, s) -> sb.append(s).append(" "),
                    StringBuilder::append)
            .toString();
        System.out.println("Concatenated: " + concatenated.trim());
        
        // Infinite streams
        System.out.println("\n=== Infinite Streams ===");
        
        // Generate first 10 fibonacci numbers
        List<Integer> fibonacci = Stream.iterate(new int[]{0, 1}, 
                                               pair -> new int[]{pair[1], pair[0] + pair[1]})
            .limit(10)
            .map(pair -> pair[0])
            .collect(Collectors.toList());
        System.out.println("Fibonacci: " + fibonacci);
        
        // Random numbers
        List<Double> randomNumbers = Stream.generate(Math::random)
            .limit(5)
            .collect(Collectors.toList());
        System.out.println("Random numbers: " + randomNumbers);
        
        // Number range operations
        int sumOfSquares = IntStream.rangeClosed(1, 10)
            .map(n -> n * n)
            .sum();
        System.out.println("Sum of squares 1-10: " + sumOfSquares);
    }
}

Advanced Lambda Patterns

Higher-Order Functions

import java.util.function.*;

public class HigherOrderFunctions {
    public static void main(String[] args) {
        // Function that returns a function
        Function<String, Function<String, String>> createPrefixer = prefix -> 
            text -> prefix + text;
        
        Function<String, String> addHello = createPrefixer.apply("Hello ");
        Function<String, String> addGoodbye = createPrefixer.apply("Goodbye ");
        
        System.out.println(addHello.apply("World"));
        System.out.println(addGoodbye.apply("World"));
        
        // Function composition
        Function<Integer, Integer> multiplyBy2 = x -> x * 2;
        Function<Integer, Integer> add3 = x -> x + 3;
        
        Function<Integer, Integer> multiplyThenAdd = multiplyBy2.andThen(add3);
        Function<Integer, Integer> addThenMultiply = multiplyBy2.compose(add3);
        
        System.out.println("Multiply then add: " + multiplyThenAdd.apply(5)); // (5*2)+3 = 13
        System.out.println("Add then multiply: " + addThenMultiply.apply(5)); // (5+3)*2 = 16
        
        // Predicate composition
        Predicate<String> startsWithA = s -> s.startsWith("A");
        Predicate<String> endsWithE = s -> s.endsWith("e");
        
        Predicate<String> startsWithAAndEndsWithE = startsWithA.and(endsWithE);
        Predicate<String> startsWithAOrEndsWithE = startsWithA.or(endsWithE);
        Predicate<String> notStartsWithA = startsWithA.negate();
        
        String test = "Apple";
        System.out.println("Starts with A and ends with e: " + startsWithAAndEndsWithE.test(test));
        System.out.println("Starts with A or ends with e: " + startsWithAOrEndsWithE.test(test));
        System.out.println("Not starts with A: " + notStartsWithA.test(test));
        
        // Currying
        demonstrateCurrying();
    }
    
    public static void demonstrateCurrying() {
        // Non-curried function
        BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
        
        // Curried version
        Function<Integer, Function<Integer, Integer>> curriedAdd = 
            a -> b -> a + b;
        
        Function<Integer, Integer> add5 = curriedAdd.apply(5);
        
        System.out.println("Add 5 to 10: " + add5.apply(10));
        System.out.println("Direct curried call: " + curriedAdd.apply(3).apply(7));
        
        // Triple function currying
        Function<Integer, Function<Integer, Function<Integer, Integer>>> tripleAdd =
            a -> b -> c -> a + b + c;
        
        System.out.println("Triple add: " + tripleAdd.apply(1).apply(2).apply(3));
    }
}

Lambda Utilities and Patterns

import java.util.*;
import java.util.function.*;

public class LambdaUtilities {
    public static void main(String[] args) {
        // Memoization pattern
        Function<Integer, Integer> expensiveFunction = memoize(n -> {
            System.out.println("Computing for " + n);
            // Simulate expensive computation
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            return n * n;
        });
        
        System.out.println("First call: " + expensiveFunction.apply(5));
        System.out.println("Second call: " + expensiveFunction.apply(5)); // Cached
        
        // Safe execution pattern
        List<Integer> numbers = Arrays.asList(1, 2, 0, 4, 5);
        
        Function<Integer, Integer> safeDivide = safely(n -> 100 / n, -1);
        numbers.stream()
               .map(safeDivide)
               .forEach(System.out::println);
        
        // Conditional execution
        Consumer<String> conditionalPrint = when(
            s -> s.length() > 3,
            System.out::println
        );
        
        conditionalPrint.accept("Hi");      // Won't print
        conditionalPrint.accept("Hello");   // Will print
        
        // Retry pattern
        Supplier<String> unreliableOperation = retry(() -> {
            if (Math.random() < 0.7) {
                throw new RuntimeException("Random failure");
            }
            return "Success!";
        }, 3);
        
        try {
            System.out.println(unreliableOperation.get());
        } catch (Exception e) {
            System.out.println("Failed after retries: " + e.getMessage());
        }
    }
    
    // Memoization utility
    public static <T, R> Function<T, R> memoize(Function<T, R> function) {
        Map<T, R> cache = new HashMap<>();
        return input -> cache.computeIfAbsent(input, function);
    }
    
    // Safe execution utility
    public static <T, R> Function<T, R> safely(Function<T, R> function, R defaultValue) {
        return input -> {
            try {
                return function.apply(input);
            } catch (Exception e) {
                return defaultValue;
            }
        };
    }
    
    // Conditional execution utility
    public static <T> Consumer<T> when(Predicate<T> condition, Consumer<T> action) {
        return input -> {
            if (condition.test(input)) {
                action.accept(input);
            }
        };
    }
    
    // Retry utility
    public static <T> Supplier<T> retry(Supplier<T> supplier, int maxAttempts) {
        return () -> {
            Exception lastException = null;
            for (int i = 0; i < maxAttempts; i++) {
                try {
                    return supplier.get();
                } catch (Exception e) {
                    lastException = e;
                    System.out.println("Attempt " + (i + 1) + " failed: " + e.getMessage());
                }
            }
            throw new RuntimeException("All attempts failed", lastException);
        };
    }
}

Best Practices

1. Keep Lambdas Short and Readable

public class LambdaBestPractices {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("apple", "banana", "cherry");
        
        // Good: Short and readable
        words.stream()
             .filter(word -> word.length() > 5)
             .forEach(System.out::println);
        
        // Bad: Too complex for lambda
        words.stream()
             .filter(word -> {
                 boolean startsWithVowel = "aeiou".contains(word.substring(0, 1));
                 boolean hasRepeatedChar = false;
                 for (int i = 0; i < word.length() - 1; i++) {
                     if (word.charAt(i) == word.charAt(i + 1)) {
                         hasRepeatedChar = true;
                         break;
                     }
                 }
                 return startsWithVowel && !hasRepeatedChar && word.length() > 3;
             })
             .forEach(System.out::println);
        
        // Better: Extract to method
        words.stream()
             .filter(LambdaBestPractices::isComplexCondition)
             .forEach(System.out::println);
    }
    
    private static boolean isComplexCondition(String word) {
        boolean startsWithVowel = "aeiou".contains(word.substring(0, 1));
        boolean hasRepeatedChar = false;
        for (int i = 0; i < word.length() - 1; i++) {
            if (word.charAt(i) == word.charAt(i + 1)) {
                hasRepeatedChar = true;
                break;
            }
        }
        return startsWithVowel && !hasRepeatedChar && word.length() > 3;
    }
}

2. Prefer Method References When Appropriate

import java.util.*;
import java.util.stream.Collectors;

public class MethodReferencePreference {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("alice", "bob", "charlie");
        
        // Good: Method reference
        List<String> upperNames = names.stream()
            .map(String::toUpperCase)
            .collect(Collectors.toList());
        
        // Less preferable: Lambda when method reference is available
        List<String> upperNames2 = names.stream()
            .map(s -> s.toUpperCase())
            .collect(Collectors.toList());
        
        // Good: Static method reference
        List<Integer> lengths = names.stream()
            .map(String::length)
            .collect(Collectors.toList());
        
        // Good: Constructor reference
        List<StringBuilder> builders = names.stream()
            .map(StringBuilder::new)
            .collect(Collectors.toList());
    }
}

3. Be Mindful of Performance

import java.util.*;
import java.util.stream.IntStream;

public class LambdaPerformance {
    public static void main(String[] args) {
        // Avoid boxing/unboxing with primitive streams
        int[] numbers = IntStream.range(1, 1000000).toArray();
        
        // Good: Use primitive streams
        long sum1 = Arrays.stream(numbers).sum();
        
        // Less efficient: Boxing to Integer stream
        long sum2 = Arrays.stream(numbers)
            .mapToObj(Integer::valueOf)
            .mapToInt(Integer::intValue)
            .sum();
        
        // Consider parallel streams for CPU-intensive operations
        long parallelSum = Arrays.stream(numbers)
            .parallel()
            .map(n -> n * n)
            .sum();
        
        System.out.println("Sums: " + sum1 + ", " + sum2 + ", " + parallelSum);
    }
}

Summary

Lambda expressions in Java provide:

  • Concise Syntax: Replace verbose anonymous inner classes
  • Functional Programming: Enable functional programming paradigms
  • Stream Integration: Power the Streams API for data processing
  • Method References: Even more concise notation for method calls
  • Higher-Order Functions: Functions that operate on other functions

Key Benefits:

  • Readability: More expressive and readable code
  • Maintainability: Easier to understand and modify
  • Performance: Better optimization opportunities
  • Flexibility: Enable functional programming patterns

Best Practices:

  • Keep lambdas short and readable
  • Use method references when appropriate
  • Extract complex logic to named methods
  • Be mindful of performance with primitive streams
  • Leverage functional interfaces effectively

Lambda expressions revolutionize Java programming by enabling more expressive, functional, and maintainable code, especially when working with collections and streams.