Functional Programming: Lambda and Streams

Introduction

Java 8+ added functional programming features: lambda expressions, method references, and the Stream API for declarative data processing. This chapter uses one dataset throughout—student names and scores—so you see how pieces connect. @FunctionalInterface was introduced in Annotations.

Prerequisites

1) Functional Interfaces

A functional interface has exactly one abstract method (SAM).

java
@FunctionalInterface
public interface ScoreFilter {
    boolean test(int score);
}

Implement with lambda:

java
ScoreFilter passing = score -> score >= 60;
System.out.println(passing.test(75));  // true

JDK built-ins in java.util.function:

  • Predicate<T>boolean test(T t)
  • Function<T,R>R apply(T t)
  • Consumer<T>void accept(T t)
  • Supplier<T>T get()

2) Lambda Expressions

Shorthand for anonymous function objects.

java
// Before — anonymous class
ScoreFilter f1 = new ScoreFilter() {
    @Override
    public boolean test(int score) {
        return score >= 90;
    }
};
 
// Lambda
ScoreFilter f2 = score -> score >= 90;

Syntax variants:

java
(a, b) -> a + b
x -> x * 2
() -> System.out.println("Hi")

Type inference supplies parameter types when context is clear.

3) Method References

When lambda only calls an existing method:

java
List<String> names = List.of("emma", "liam");
names.forEach(System.out::println);  // instance method ref
 
names.stream()
     .map(String::toUpperCase)     // static/instance ref form
     .forEach(System.out::println);

Forms: Class::staticMethod, instance::method, Class::new.

4) Stream API Overview

A Stream processes elements from a source (collection, array) in a pipeline:

source → intermediate ops → terminal op

Same data for examples:

java
import java.util.List;
 
record Student(String name, int score) {}
 
List<Student> students = List.of(
        new Student("Emma", 95),
        new Student("Liam", 78),
        new Student("Noah", 92),
        new Student("Ava", 85)
);

filter — keep matching elements

java
List<Student> top = students.stream()
        .filter(s -> s.score() >= 90)
        .toList();
 
System.out.println(top);  // [Emma]

map — transform each element

java
List<String> names = students.stream()
        .map(Student::name)
        .map(String::toUpperCase)
        .toList();

sorted — order elements

java
List<Student> ranked = students.stream()
        .sorted((a, b) -> Integer.compare(b.score(), a.score()))
        .toList();

collect — build result container

java
import java.util.stream.Collectors;
 
String summary = students.stream()
        .filter(s -> s.score() >= 80)
        .map(Student::name)
        .collect(Collectors.joining(", "));
System.out.println(summary);

Terminal operations (must end pipeline)

forEach, count, toList, collect, reduce, findFirst, etc.
Without a terminal op, the stream does nothing (lazy intermediates).

5) Full Pipeline Example

java
import java.util.List;
 
public class StreamDemo {
 
    record Student(String name, int score) {}
 
    public static void main(String[] args) {
        List<Student> students = List.of(
                new Student("Emma", 95),
                new Student("Liam", 78),
                new Student("Noah", 92),
                new Student("Ava", 85)
        );
 
        double avgPassing = students.stream()
                .filter(s -> s.score() >= 60)
                .mapToInt(Student::score)
                .average()
                .orElse(0.0);
 
        System.out.printf("Average passing score: %.1f%n", avgPassing);
 
        students.stream()
                .sorted((a, b) -> Integer.compare(b.score(), a.score()))
                .forEach(s -> System.out.println(s.name() + " -> " + s.score()));
    }
}

mapToInt avoids boxing for numeric aggregates.

6) Parallel Streams (Brief)

java
long count = students.parallelStream()
        .filter(s -> s.score() > 80)
        .count();

Uses multiple threads—good for large CPU-bound data. Thread safety and ordering nuances belong in Multithreading. Start with sequential streams until you understand behavior.

Lambda vs Stream — When to Use

UseWhen
Lambda alonecallbacks, comparators, small handlers
Streamstransform/filter/aggregate collections declaratively
Classic loopsvery simple logic, need break/continue on complex control flow

Common Beginner Mistakes

Modifying Source During Stream

Do not add/remove from a list while streaming it—ConcurrentModificationException.

Missing Terminal Operation

java
students.stream().filter(s -> s.score() > 80);  // does nothing

Reusing Stream

Streams are single-use—create a new stream for another pipeline.

Overusing Streams for Tiny Loops

Readability matters—a three-line for may beat a long stream chain.

Mini Practice

Given List<Integer> scores, print count of scores > 80 and their sum using streams.

What’s Next

Collections OverviewList, Set, Map with generics.

FAQ

Is lambda the same as anonymous inner class?

Similar role, different bytecode—lambdas use invokedynamic and are more concise.

Can lambdas access local variables?

Yes if effectively final (not reassigned after capture).

Do streams replace collections?

No. Streams process collections; you still store data in List/Set/Map.

Why toList() vs collect(Collectors.toList())?

toList() (Java 16+) returns unmodifiable list—simpler for many cases.

When to avoid parallel streams?

Small data, IO-bound work, or when order and thread safety matter—sequential is safer default.