Java 8 Feature

    Lambda Expressions

    A powerful feature that brings functional programming to Java

    λ
    λ

    See the Magic: Code Transformation

    Old Way (7 lines)
    Runnable r = new Runnable() {
    @Override
    public void run() {
    System.out.println("Hi");
    }
    };
    New Way (1 line)
    Runnable r = () -> System.out.println("Hi");
    85% less code!

    What is a Lambda Expression?

    Think of a lambda expression as a shortcut for writing small, throwaway functions. Before Java 8, if you wanted to pass behavior (like sorting logic or a click handler) to a method, you had to create an entire class or use verbose anonymous classes.

    Lambda expressions let you write that same logic in just one line. They're called "lambda" after the Greek letter λ, which is used in mathematics to represent functions.

    💡 Simple Definition:

    A lambda is an anonymous function – a function without a name that you can pass around like a value. It's just: parameters → body

    The Basic Syntax:

    (parameters) → expression
    (parameters) → { statements; }

    Why Should You Use Lambda Expressions?

    ✅ Benefits:

    • Less Code – Write in 1 line what used to take 5-10 lines
    • More Readable – Focus on WHAT you're doing, not HOW
    • Enables Functional Style – Use map, filter, reduce patterns
    • Better APIs – Works seamlessly with Streams, Collections
    • Parallel Processing – Easier to write thread-safe code

    ⚠️ When NOT to Use:

    • Complex logic with multiple statements (use regular methods)
    • When you need to reuse the same logic multiple times
    • When debugging – lambdas can be harder to debug
    • When you need meaningful stack traces

    Before vs After Lambda: See the Difference

    Let's see how lambdas simplify code. Here's the same functionality written both ways:

    BeforeLambda.java (Anonymous Class)
    // OLD WAY: Anonymous Inner Class (Java 7 and before)// This takes 7 lines just to define a simple action!Runnable oldWay =newRunnable(){@Overridepublicvoidrun(){System.out.println("Hello World");}};// Sorting with Comparator - also verboseCollections.sort(names,newComparator<String>(){@Overridepublicintcompare(String a,String b){return a.compareTo(b);}});
    AfterLambda.java (Clean & Simple)
    // NEW WAY: Lambda Expression (Java 8+)// Same thing in just 1 line!Runnable newWay =()->System.out.println("Hello World");// Sorting is now crystal clearCollections.sort(names,(a, b)-> a.compareTo(b));// Or even simpler with method referenceCollections.sort(names,String::compareTo);

    Lambda Syntax Explained Step by Step

    Let's break down every part of lambda syntax so you fully understand it:

    LambdaSyntax.java
    // PATTERN 1: No parameters// When your function takes nothingRunnable r =()->System.out.println("Hello");// The () means "no input needed"// PATTERN 2: One parameter (parentheses optional!)Consumer<String> print = s ->System.out.println(s);Consumer<String> print2 =(s)->System.out.println(s);// Also valid// When there's exactly ONE parameter, you can skip ()// PATTERN 3: Multiple parameters (parentheses required)BiFunction<Integer,Integer,Integer> add =(a, b)-> a + b;// Both parameters go inside ()// PATTERN 4: With explicit types (optional but sometimes helpful)BiFunction<Integer,Integer,Integer> multiply =(Integer a,Integer b)-> a * b;// Usually Java can figure out types, but you can specify them// PATTERN 5: Multiple statements (use curly braces + return)BiFunction<Integer,Integer,Integer> divide =(a, b)->{if(b ==0){thrownewArithmeticException("Cannot divide by zero");}return a / b;};// When you have multiple lines, you MUST use {} and return

    🎯 Quick Rules to Remember:

    • Zero params: () required
    • One param: () optional
    • Multiple params: () required
    • One expression: no {} or return needed
    • Multiple statements: {} and return required

    Variable Capture: Using Outside Variables in Lambdas

    Lambdas can "capture" and use variables from the surrounding code. But there's an important rule: those variables must be effectively final – meaning you can't change them after the lambda is created.

    ❓ Why This Rule?

    Lambdas might run on different threads at different times. If variables could change, the lambda wouldn't know which version of the variable to use. Making them effectively final prevents this confusion and threading bugs.

    VariableCapture.java
    publicclassVariableCapture{privateString instanceVar ="instance";// Instance variables are finepublicvoiddemonstrate(){String localVar ="local";// This is effectively final (never reassigned)int counter =0;// ✅ WORKS: Using instance variableRunnable r1 =()->System.out.println(instanceVar);// ✅ WORKS: Using effectively final local variableRunnable r2 =()->System.out.println(localVar);// ❌ ERROR: Can't modify and then use in lambda// counter++;  // If you uncomment this...// Runnable r3 = () -> System.out.println(counter);  // This fails!// 💡 WORKAROUND: Use an array or AtomicIntegerint[] mutableCounter ={0};// Arrays are objects, reference doesn't changeRunnable r4 =()->{
    mutableCounter[0]++;// Modifying content is OK!System.out.println(mutableCounter[0]);};// 💡 BETTER WORKAROUND: AtomicInteger (thread-safe)AtomicInteger safeCounter =newAtomicInteger(0);Runnable r5 =()->System.out.println(safeCounter.incrementAndGet());}}

    Real-World Use Cases

    Here are the most common places you'll use lambda expressions in everyday Java code:

    LambdaUseCases.java
    List<String> names =Arrays.asList("Alice","Bob","Charlie","David");// 1. SORTING - Most common use case
    names.sort((a, b)-> a.length()- b.length());// Sort by length
    names.sort((a, b)-> b.compareTo(a));// Reverse alphabetical// 2. FILTERING COLLECTIONS
    names.removeIf(name -> name.startsWith("A"));// Remove names starting with A// 3. ITERATING (forEach)
    names.forEach(name ->System.out.println("Hello, "+ name));// 4. STREAM OPERATIONS (filter, map, reduce)List<String> longNames = names.stream().filter(name -> name.length()>3)// Keep only names > 3 chars.map(name -> name.toUpperCase())// Convert to uppercase.collect(Collectors.toList());// Collect results// 5. EVENT HANDLING (Swing/JavaFX)
    button.addActionListener(e ->handleClick());
    button.setOnAction(event ->openDialog());// 6. THREADINGnewThread(()->{System.out.println("Running in background thread");doHeavyWork();}).start();// 7. OPTIONAL OPERATIONSOptional<String> name =Optional.of("Alice");
    name.ifPresent(n ->System.out.println("Found: "+ n));String result = name.orElseGet(()->getDefaultName());

    💡 Tips, Tricks & Best Practices

    ✅ DO: Keep Lambdas Short

    If your lambda is more than 2-3 lines, extract it to a regular method and use a method reference instead.

    Java Example
    // Instead of this complex lambda:
    names.forEach(name ->{validate(name);transform(name);save(name);});// Do this:
    names.forEach(this::processName);// Method reference to a well-named method

    ✅ DO: Use Method References When Possible

    Method references are even cleaner than lambdas for simple cases.

    Java Example
    // Lambda
    names.forEach(name ->System.out.println(name));// Method reference (cleaner!)
    names.forEach(System.out::println);

    ❌ DON'T: Use Lambdas for Side Effects in Streams

    Avoid modifying external state inside stream lambdas - it can cause bugs in parallel streams.

    Java Example
    // BAD: Modifying external listList<String> results =newArrayList<>();
    names.stream().forEach(n -> results.add(n.toUpperCase()));// Don't!// GOOD: Use collect insteadList<String> results = names.stream().map(String::toUpperCase).collect(Collectors.toList());

    💡 TIP: Debugging Lambdas

    Use peek() to debug stream operations:

    Java Example
    names.stream().peek(n ->System.out.println("Before filter: "+ n)).filter(n -> n.length()>3).peek(n ->System.out.println("After filter: "+ n)).collect(Collectors.toList());

    ⚠️ Common Mistakes to Avoid

    1.

    Forgetting that lambdas need a Functional Interface

    Lambdas can only be used where a functional interface (interface with exactly one abstract method) is expected.

    2.

    Trying to use non-final variables

    Variables used in lambdas must be effectively final. Use AtomicInteger or arrays as workarounds.

    3.

    Ignoring exception handling

    Lambdas can't throw checked exceptions directly. Wrap in try-catch or create helper methods.

    4.

    Making lambdas too complex

    If your lambda needs more than 3 lines, it's a sign you should extract it to a named method.

    📝 Quick Summary

    Lambda Expression Is:

    • An anonymous function
    • A shortcut for implementing functional interfaces
    • Syntax: (params) → expression

    Remember:

    • Keep lambdas short and simple
    • Variables must be effectively final
    • Use method references when possible
    • Great for Streams, sorting, event handlers