Skip to content
Neelu edited this page Jul 26, 2026 · 1 revision

☕ Java 8: The Functional Programming Revolution

Release Date: March 14, 2014 | LTS: Yes (Extended Support) | Status: ✅ Still Widely Used


📋 Table of Contents

  1. Overview
  2. Core Features
  3. Code Examples
  4. Best Practices
  5. Performance Considerations
  6. Common Pitfalls

🎯 Overview

Java 8 transformed Java from a traditional imperative language into a modern functional programming powerhouse. It introduced:

  • Lambdas: Concise function syntax
  • Functional Interfaces: Single-method interfaces for functional programming
  • Streams API: Declarative data processing
  • Method References: Elegant function references
  • Default Methods: Extend interfaces without breaking code

Why Java 8 Changed Everything

Before Java 8:                          After Java 8:
Collections.sort(list,                  list.stream()
  new Comparator<Person>() {              .filter(p -> p.getAge() > 21)
    @Override                            .map(Person::getName)
    public int compare(Person a, ...) {  .sorted()
      return a.getAge() - b.getAge();    .collect(toList());
    }
  });
mindmap
  root((Java 8))
    Lambdas
      Concise anonymous functions
      Replace single-method inner classes
    Streams API
      Declarative data processing
      filter · map · reduce · collect
    Functional Interfaces
      Consumer / Supplier / Function / Predicate
      @FunctionalInterface contract
    Method References
      ClassName::method
      Instance::method
    Default Methods
      Interface evolution without breaking implementers
    Optional
      Explicit "value may be absent"
      Replaces defensive null checks
Loading

🚀 Core Features

1️⃣ Lambda Expressions

What: Anonymous functions with a concise syntax

// Traditional Anonymous Class
Comparator<Integer> comp = new Comparator<Integer>() {
    @Override
    public int compare(Integer a, Integer b) {
        return a.compareTo(b);
    }
};

// Lambda (Java 8+)
Comparator<Integer> comp = (a, b) -> a.compareTo(b);

Syntax Rules:

// No parameters
() -> System.out.println("Hello");

// Single parameter (parentheses optional)
x -> x * 2
(x) -> x * 2

// Multiple parameters
(x, y) -> x + y

// Multiple statements
(x, y) -> {
    int sum = x + y;
    return sum;
}

// Type inference
(String s) -> s.length()  // or (s) -> s.length()

Real-World Example:

public class LambdaExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
        
        // Traditional way
        List<Integer> evenNumbers = new ArrayList<>();
        for (Integer num : numbers) {
            if (num % 2 == 0) {
                evenNumbers.add(num);
            }
        }
        System.out.println("Even: " + evenNumbers); // [2, 4]
        
        // Lambda way
        List<Integer> evens = numbers.stream()
            .filter(n -> n % 2 == 0)
            .collect(Collectors.toList());
        System.out.println("Even: " + evens); // [2, 4]
    }
}

2️⃣ Functional Interfaces

What: Interfaces with exactly ONE abstract method

// Built-in Functional Interfaces

// Consumer: Accept input, no return
Consumer<String> print = s -> System.out.println(s);
print.accept("Hello"); // Output: Hello

// Supplier: No input, return value
Supplier<String> greeting = () -> "Hello, World!";
System.out.println(greeting.get()); // Hello, World!

// Function: Input → Output
Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25

// Predicate: Input → Boolean
Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // true

Custom Functional Interface:

@FunctionalInterface  // Compiler ensures only 1 abstract method
public interface Calculator {
    int calculate(int a, int b);
    
    default int add(int a, int b) {
        return calculate(a, b);
    }
}

// Usage
Calculator adder = (a, b) -> a + b;
System.out.println(adder.calculate(5, 3)); // 8

3️⃣ Streams API

What: Functional-style operations on collections

List<Person> people = Arrays.asList(
    new Person("Alice", 25, 75000),
    new Person("Bob", 30, 85000),
    new Person("Charlie", 22, 50000)
);

// Filter → Map → Collect
List<String> highEarners = people.stream()
    .filter(p -> p.getSalary() > 60000)
    .map(Person::getName)
    .sorted()
    .collect(Collectors.toList());

System.out.println(highEarners); // [Alice, Bob]
flowchart LR
    A["List&lt;Person&gt;\n(source)"] -->|"filter(p -&gt; p.salary &gt; 60000)"| B["Stream: high earners only"]
    B -->|"map(Person::getName)"| C["Stream&lt;String&gt;\n(names)"]
    C -->|"sorted()"| D["Stream&lt;String&gt;\n(alphabetical)"]
    D -->|"collect(toList())"| E["List&lt;String&gt;\n(result)"]

    style A fill:#e8f4f8,stroke:#0891b2
    style E fill:#d4f4dd,stroke:#22863a
Loading

Stream Operations:

Category Methods Purpose
Filtering filter() Keep elements matching condition
Mapping map(), flatMap() Transform elements
Sorting sorted() Order elements
Limiting limit(), skip() Restrict elements
Aggregating reduce() Combine into single value
Collecting collect() Gather into collection

Advanced Stream Examples:

// 1. Count even numbers
int evenCount = numbers.stream()
    .filter(n -> n % 2 == 0)
    .count();  // Terminal operation

// 2. Find first element matching condition
Optional<Integer> firstEven = numbers.stream()
    .filter(n -> n % 2 == 0)
    .findFirst();

// 3. Group by age
Map<Integer, List<Person>> byAge = people.stream()
    .collect(Collectors.groupingBy(Person::getAge));

// 4. Get average salary
double avgSalary = people.stream()
    .mapToDouble(Person::getSalary)
    .average()
    .orElse(0.0);

// 5. Flatten nested lists
List<List<Integer>> matrix = Arrays.asList(
    Arrays.asList(1, 2),
    Arrays.asList(3, 4)
);
List<Integer> flat = matrix.stream()
    .flatMap(List::stream)
    .collect(Collectors.toList()); // [1, 2, 3, 4]

4️⃣ Method References

What: Shorthand to call existing methods

// Lambda vs Method Reference

// 1. Static method reference
// Lambda:      numbers.forEach(n -> System.out.println(n));
// Reference:
numbers.forEach(System.out::println);

// 2. Instance method reference
// Lambda:      people.forEach(p -> print(p));
// Reference:
people.forEach(this::print);

// 3. Constructor reference
// Lambda:      names.stream().map(n -> new Person(n)).collect(toList());
// Reference:
names.stream().map(Person::new).collect(toList());

// 4. Sorting with method reference
Collections.sort(people, Comparator.comparing(Person::getAge));
// vs Lambda:
Collections.sort(people, (p1, p2) -> p1.getAge() - p2.getAge());

Types:

ClassName::staticMethod        // Static method
instance::instanceMethod       // Instance method
ClassName::new                 // Constructor
ClassName::instanceMethod      // All instances

5️⃣ Default Methods in Interfaces

What: Provide implementation in interfaces (backward compatibility)

public interface Vehicle {
    void drive();
    
    // Default method (has implementation)
    default void honk() {
        System.out.println("Beep! Beep!");
    }
}

public class Car implements Vehicle {
    @Override
    public void drive() {
        System.out.println("Car is driving");
    }
    // honk() is inherited by default
}

// Usage
Car car = new Car();
car.honk(); // Output: Beep! Beep!

6️⃣ Optional Class

What: Represents optional values (prevents NullPointerException)

flowchart TD
    A["Optional.ofNullable(getPerson())"] --> B{"Value present?"}
    B -->|Yes| C["map / flatMap / filter\nchain runs normally"]
    B -->|No| D["Chain short-circuits\n— nothing throws"]
    C --> E["ifPresent(...) runs\nor .get() returns the value"]
    D --> F["ifPresentOrElse's 2nd branch runs\nor .orElse(default) kicks in"]

    style B fill:#fff4e0,stroke:#cc8800
    style D fill:#ffe0e0,stroke:#cc3333
    style C fill:#d4f4dd,stroke:#22863a
Loading
// Before Optional
Person person = getPerson();
if (person != null) {
    String name = person.getName();
    if (name != null) {
        System.out.println(name);
    }
}

// With Optional
Optional<Person> person = Optional.ofNullable(getPerson());
person.flatMap(p -> Optional.ofNullable(p.getName()))
      .ifPresent(System.out::println);

// Common Optional methods
Optional<String> name = Optional.of("Alice");

name.isPresent();              // true
name.get();                    // "Alice"
name.orElse("Unknown");        // "Alice"
name.orElseGet(() -> "Guest"); // "Alice"
name.ifPresent(System.out::println); // Alice
name.map(String::length)       // Optional(5)
    .filter(len -> len > 3)    // Optional(5)

💡 Best Practices

✅ Do's

// 1. Keep lambdas short and readable
list.stream()
    .filter(x -> x > 10)
    .forEach(System.out::println);

// 2. Use method references when appropriate
list.forEach(System.out::println);

// 3. Use streams for data transformations
List<String> names = people.stream()
    .filter(p -> p.getAge() > 21)
    .map(Person::getName)
    .collect(toList());

// 4. Use Optional instead of null checks
Optional<Person> person = findPerson("Alice");
person.ifPresent(p -> System.out.println(p));

// 5. Use functional interfaces for callbacks
interface EventListener {
    void onEvent(Event e);
}
EventListener listener = event -> handleEvent(event);

❌ Don'ts

// 1. Don't use complex logic in lambdas
// ❌ Bad
list.stream()
    .filter(x -> {
        if (x > 10) {
            if (x < 100) {
                return true;
            }
        }
        return false;
    })

// ✅ Good
list.stream()
    .filter(x -> x > 10 && x < 100)

// 2. Don't use side effects in streams
// ❌ Bad
list.stream()
    .filter(x -> {
        System.out.println(x);  // Side effect!
        return x > 10;
    })

// ✅ Good
list.stream()
    .filter(x -> x > 10)
    .forEach(System.out::println);

// 3. Don't catch exceptions in lambdas without handling
// ❌ Bad
list.stream()
    .map(s -> Integer.parseInt(s))  // Can throw NumberFormatException

// ✅ Good
list.stream()
    .map(s -> {
        try {
            return Integer.parseInt(s);
        } catch (NumberFormatException e) {
            return 0;
        }
    })

🔧 Performance Considerations

Lambda Performance

// Lambda creation is cheap (no new object each time)
Consumer<Integer> consumer = x -> System.out.println(x);
consumer.accept(5);  // Reuses same lambda instance

// But streams have overhead for small collections
// Use only for complex transformations on large data

List<Integer> small = Arrays.asList(1, 2, 3);
small.parallelStream()  // Overhead > benefit for 3 items
    .filter(x -> x > 1)
    .forEach(System.out::println);

// Parallel streams beneficial for large datasets
List<Integer> large = IntStream.range(0, 1_000_000)
    .boxed()
    .collect(toList());

long start = System.currentTimeMillis();
long sum = large.parallelStream()
    .mapToLong(x -> x)
    .sum();
System.out.println("Time: " + (System.currentTimeMillis() - start)); // Faster!

Stream Performance Tips

Tip Example Benefit
Use findFirst() instead of filter().collect() .filter(x -> x > 100).findFirst() Early termination
Avoid unnecessary sorted() .sorted().limit(10) Can be expensive
Use primitive streams IntStream.range(0, 1000) Avoids boxing overhead
Order operations wisely Filter before map Reduce processing

⚠️ Common Pitfalls

1. Modifying External Variables

// ❌ Won't compile - lambda captures effectively final variables
int multiplier = 2;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> System.out.println(n * multiplier));
multiplier = 3;  // ❌ Error: multiplier must be effectively final

// ✅ Use final or effectively final
final int multiplier = 2;
numbers.forEach(n -> System.out.println(n * multiplier));

2. Stream Reusability

// ❌ Streams are consumed after terminal operation
Stream<Integer> stream = Arrays.asList(1, 2, 3).stream();
stream.filter(x -> x > 1).forEach(System.out::println);
stream.filter(x -> x < 3).forEach(System.out::println);  // ❌ Error: stream already consumed

// ✅ Create new stream
List<Integer> list = Arrays.asList(1, 2, 3);
list.stream().filter(x -> x > 1).forEach(System.out::println);
list.stream().filter(x -> x < 3).forEach(System.out::println);

3. NullPointerException with Streams

// ❌ NPE if any element is null
List<Person> people = Arrays.asList(person1, null, person3);
people.stream()
    .map(Person::getName)  // NPE when person is null
    .forEach(System.out::println);

// ✅ Filter nulls first
people.stream()
    .filter(Objects::nonNull)
    .map(Person::getName)
    .forEach(System.out::println);

📊 Complete Example: Word Frequency Analyzer

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

public class WordFrequencyAnalyzer {
    public static void main(String[] args) {
        String text = "Java is great Java is powerful Functional programming is Java";
        
        // Split into words and count frequency
        Map<String, Long> wordFreq = Arrays.stream(text.toLowerCase().split("\\s+"))
            .collect(Collectors.groupingBy(
                Function.identity(),
                Collectors.counting()
            ));
        
        // Sort by frequency and display
        wordFreq.entrySet().stream()
            .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue()))
            .forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));
        
        // Output:
        // java: 3
        // is: 3
        // great: 1
        // powerful: 1
        // functional: 1
        // programming: 1
    }
}

🎓 Takeaways

Lambdas make code more readable and concise
Streams API enables functional data processing
Method References provide elegant syntax
Optional helps handle null values safely
Functional Interfaces support functional programming


🔗 Next Steps

👉 Master these Java 8 concepts, then explore:

  • Java 11: Type inference with var
  • Java 14+: Records and Pattern Matching
  • Java 21: Virtual Threads and Structured Concurrency

⬅️ Back to Main Guide | ➡️ Java 9 Guide →

Made with ☕ by Neelu Sahai

Clone this wiki locally