Skip to main content

Java Streams API and Lambda Expressions

Ayesha
EditReport
Who is this for?

Developers who know basic Java (loops, collections) and want to write cleaner, modern code using lambda expressions and the Streams API. This guide focuses on functional-style programming with hands-on examples. For the full Collections reference, see Collections and Streams in Java.

Java Streams API and Lambda Expressions

Java 8 introduced two features that fundamentally changed how we write everyday Java code:

  • Lambda expressions โ€” Compact ways to pass behavior (functions) as arguments.
  • Streams API โ€” A pipeline for processing collections in a declarative, functional style.

Together, they let you express what you want to do with data instead of how to loop over it step-by-step.

Why Use Lambdas and Streams?โ€‹

Before (Imperative Style)โ€‹

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
List<Integer> evensDoubled = new ArrayList<>();

for (int n : numbers) {
if (n % 2 == 0) {
evensDoubled.add(n * 2);
}
}

After (Functional Style)โ€‹

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);

List<Integer> evensDoubled = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * 2)
.collect(Collectors.toList());

Both produce [4, 8, 12]. The stream version reads like a sentence: filter evens, double each, collect to a list.


Lambda Expressionsโ€‹

A lambda expression is an anonymous function โ€” a block of code you can pass around without declaring a full class or method.

Video Explanationโ€‹

Basic Syntaxโ€‹

(parameters) -> expression
(parameters) -> { statements; }

ExampleMeaning
x -> x * 2One parameter, returns x * 2
(a, b) -> a + bTwo parameters, returns their sum
() -> 42No parameters, returns 42
s -> { System.out.println(s); return s.length(); }Multiple statements with explicit return
// Runnable โ€” no parameters, no return value
Runnable task = () -> System.out.println("Hello from a lambda!");
task.run();

// Comparator โ€” two parameters, returns int
String prefix = "Hello, ";
Comparator<String> byLength = (a, b) -> Integer.compare(a.length(), b.length());

// Custom logic using Built-in Functional Interface
BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b;
System.out.println(multiply.apply(3, 4)); // 12

Functional Interfacesโ€‹

A functional interface has exactly one abstract method. Lambdas implement that method implicitly.

import java.util.function.*;

// Predicate<T> โ†’ boolean test(T t)
Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // true
System.out.println(isEven.test(7)); // false

// Function<T, R> โ†’ R apply(T t)
Function<String, Integer> length = s -> s.length();
System.out.println(length.apply("hello")); // 5

// Consumer<T> โ†’ void accept(T t)
Consumer<String> printer = s -> System.out.println(s);
printer.accept("Streams are cool!");

// Supplier<T> โ†’ T get()
Supplier<Double> random = () -> Math.random();

Common interfaces from java.util.function:

InterfaceMethodUse Case
Predicate<T>test(T)Filter conditions
Function<T, R>apply(T)Transform values
Consumer<T>accept(T)Side effects (print, log)
Supplier<T>get()Lazy value creation
BiFunction<T, U, R>apply(T, U)Combine two inputs

Method Referencesโ€‹

When a lambda only calls an existing method, use a method reference (::) for cleaner syntax.

List<String> words = List.of("java", "streams", "lambda");

// Lambda form
words.forEach(s -> System.out.println(s));

// Method reference form
words.forEach(System.out::println);

// Static method reference
Function<String, Integer> parse = Integer::parseInt;

// Instance method reference on a specific object
String prefix = "Hello, ";
Function<String, String> greet = prefix::concat;

// Constructor reference
Supplier<List<String>> listFactory = ArrayList::new;

SyntaxExampleEquivalent Lambda
Class::staticMethodInteger::parseInts -> Integer.parseInt(s)
object::instanceMethodprefix::concats -> prefix.concat(s)
Class::instanceMethodString::toUpperCases -> s.toUpperCase()
Class::newArrayList::new() -> new ArrayList<>()

Streams API Overviewโ€‹

A Stream is a sequence of elements supporting functional-style operations. Think of it as a pipeline:

Source  โ†’  [Filter, Map, Sorted, ...]  โ†’  Terminal Operation
List Intermediate (Lazy) collect, reduce, forEach

Video Explanationโ€‹

Key Propertiesโ€‹

  • Lazy โ€” Intermediate operations run only when a terminal operation is called.
  • Non-reusable โ€” Once consumed, a stream cannot be used or traversed again.
  • Does not modify the source โ€” The original collection stays entirely unchanged.
import java.util.*;
import java.util.stream.*;

Creating Streamsโ€‹

// From a List
List<Integer> nums = List.of(1, 2, 3, 4, 5);
Stream<Integer> fromList = nums.stream();

// From an array
int[] arr = {10, 20, 30};
IntStream fromArray = Arrays.stream(arr);

// Direct values
Stream<String> direct = Stream.of("a", "b", "c");

// Range of integers (very useful in DSA)
IntStream range = IntStream.range(0, 5); // 0, 1, 2, 3, 4
IntStream closed = IntStream.rangeClosed(1, 5); // 1, 2, 3, 4, 5

// From a Map
Map<String, Integer> scores = Map.of("Alice", 90, "Bob", 75);
Stream<Map.Entry<String, Integer>> entries = scores.entrySet().stream();

Intermediate Operationsโ€‹

Intermediate operations return a new Stream and are lazy. No processing happens until a terminal operation triggers the pipeline.

filter โ€” Keep elements that match a conditionโ€‹

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8);

List<Integer> evens = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
// Output: [2, 4, 6, 8]

// Chain multiple filters
List<Integer> inRange = numbers.stream()
.filter(n -> n > 2)
.filter(n -> n < 7)
.collect(Collectors.toList());
// Output: [3, 4, 5, 6]

map โ€” Transform each elementโ€‹

List<String> words = List.of("hello", "world", "java");

List<String> upper = words.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// Output: ["HELLO", "WORLD", "JAVA"]

List<Integer> lengths = words.stream()
.map(String::length)
.collect(Collectors.toList());
// Output: [5, 5, 4]

flatMap โ€” Map and flatten one levelโ€‹

Useful when each element maps to multiple elements (e.g., nested lists, splitting sentences into individual words).

List<List<Integer>> nested = List.of(
List.of(1, 2, 3),
List.of(4, 5),
List.of(6, 7, 8)
);

List<Integer> flat = nested.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
// Output: [1, 2, 3, 4, 5, 6, 7, 8]

List<String> sentences = List.of("hello world", "java streams");
List<String> allWords = sentences.stream()
.flatMap(s -> Arrays.stream(s.split(" ")))
.collect(Collectors.toList());
// Output: ["hello", "world", "java", "streams"]

Other useful intermediate operationsโ€‹

List<Integer> nums = List.of(5, 2, 8, 2, 1, 9, 3);

// distinct โ€” remove duplicates
List<Integer> unique = nums.stream().distinct().collect(Collectors.toList());
// Output: [5, 2, 8, 1, 9, 3]

// sorted โ€” natural or custom order
List<Integer> sorted = nums.stream().sorted().collect(Collectors.toList());
// Output: [1, 2, 2, 3, 5, 8, 9]

// limit & skip โ€” pagination
List<Integer> page = nums.stream().skip(2).limit(3).collect(Collectors.toList());
// Output: [8, 2, 1]

Terminal Operationsโ€‹

Terminal operations trigger the stream pipeline execution and produce a final, non-stream result.

collect โ€” Gather results into a collectionโ€‹

List<Integer> list = Stream.of(1, 2, 3).collect(Collectors.toList());
Set<Integer> set = Stream.of(1, 2, 2, 3).collect(Collectors.toSet());

String joined = Stream.of("Java", "Streams", "API")
.collect(Collectors.joining(", "));
// Output: "Java, Streams, API"

reduce โ€” Fold elements into a single valueโ€‹

List<Integer> numbers = List.of(1, 2, 3, 4, 5);

int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
// Output: 15

int product = numbers.stream()
.reduce(1, (a, b) -> a * b);
// Output: 120

Optional<Integer> max = numbers.stream()
.reduce(Integer::max);
// Output: Optional[5]

forEach โ€” Perform an action on each elementโ€‹

List.of("apple", "banana", "cherry")
.forEach(fruit -> System.out.println(fruit));

// Method reference version
List.of("apple", "banana", "cherry")
.forEach(System.out::println);

Matching and findingโ€‹

List<Integer> nums = List.of(2, 4, 6, 8);

boolean allEven = nums.stream().allMatch(n -> n % 2 == 0); // true
boolean anyOdd = nums.stream().anyMatch(n -> n % 2 != 0); // false
boolean noneNegative = nums.stream().noneMatch(n -> n < 0); // true

Optional<Integer> first = nums.stream().filter(n -> n > 5).findFirst();
// Output: Optional[6]


Putting It All Togetherโ€‹

Here is a complete runnable example showcasing a complete processing pipeline:

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

public class StreamsDemo {
public static void main(String[] args) {
List<String> names = List.of(
"Alice", "bob", "Charlie", "diana", "Eve"
);

List<String> result = names.stream()
.filter(name -> name.length() > 3) // Keep names longer than 3 chars
.map(String::toUpperCase) // Convert to uppercase
.sorted() // Sort alphabetically
.collect(Collectors.toList());

System.out.println(result);
// Output: [ALICE, CHARLIE, DIANA]
}
}

Step-by-Step Breakdownโ€‹

  1. stream() initializes a stream configuration from the source list.
  2. filter(...) isolates values meeting the conditional parameter.
  3. map(...) applies object mutations sequentially.
  4. sorted() triggers natural comparison reorganization.
  5. collect(...) terminates the stream, generating a concrete output list.

DSA-Style Examplesโ€‹

Find All Primes in a Rangeโ€‹

static boolean isPrime(int n) {
if (n < 2) return false;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) return false;
}
return true;
}

List<Integer> primes = IntStream.rangeClosed(1, 50)
.filter(StreamsDemo::isPrime)
.boxed() // Converts IntStream to Stream<Integer>
.collect(Collectors.toList());
// Output: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]

Sum of Squares of Even Numbersโ€‹

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

int sumOfSquares = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.reduce(0, Integer::sum);
// 4 + 16 + 36 + 64 + 100 = 220

Group Anagramsโ€‹

List<String> words = List.of("eat", "tea", "tan", "ate", "nat", "bat");

Map<String, List<String>> anagrams = words.stream()
.collect(Collectors.groupingBy(w -> {
char[] chars = w.toCharArray();
Arrays.sort(chars);
return new String(chars);
}));
// Output: {aet=[eat, tea, ate], ant=[tan, nat], abt=[bat]}

Frequency Countโ€‹

List<String> fruits = List.of("apple", "banana", "apple", "cherry", "banana", "apple");

Map<String, Long> frequency = fruits.stream()
.collect(Collectors.groupingBy(f -> f, Collectors.counting()));
// Output: {apple=3, banana=2, cherry=1}


Best Practices and Gotchasโ€‹

Doโ€‹

  • Prioritize Readability: Use streams when transformations or complex object updates look confusing inside nested loops.
  • Leverage Method References: When a lambda purely executes an explicit target method, replace s -> s.toLowerCase() with String::toLowerCase.
  • Avoid Unnecessary Boxing: Use optimized primitive streams (IntStream, LongStream, DoubleStream) along with specialized maps like mapToInt() to eliminate object reference allocation overhead.
  • Order Operations Wisely: Apply filter() clauses early in pipelines to drop redundant records before executing expensive operations like map() or sorted().

Avoidโ€‹

  • Reusing a Stream Object: Attempting a subsequent operation on an already evaluated stream triggers an error.
Stream<Integer> stream = List.of(1, 2, 3).stream();
stream.forEach(System.out::println); // OK
stream.count(); // Throws IllegalStateException

  • Modifying the Underlying Source: Mutating the original data structure mid-stream leads to runtime unpredictability or ConcurrentModificationException.
  • Performance Bottlenecks in Tight DSA Loops: Pure imperative for loops have significantly lower stack overhead and execute faster. In time-critical competitive programming loops, avoid streams. See the Java Code Style Guide.
  • Side-Effects in Parallel Operations: Avoid using non-thread-safe state collections inside .parallelStream() blocks.

Quick Referenceโ€‹

OperationTypeDescription
filter(predicate)IntermediateKeep elements matching a condition
map(function)IntermediateTransform each element type or value
flatMap(function)IntermediateFlatten nested structural streams into a single list
distinct()IntermediateRemove identical objects from the pipeline
sorted()IntermediateReorder items natively or via a Comparator
limit(n)IntermediateTruncate data evaluation after n elements
skip(n)IntermediateIgnore the first n elements in the stream
collect(collector)TerminalGather resulting stream contents into concrete formats
reduce(init, accumulator)TerminalCompute sequence down to a singular wrapped object
forEach(action)TerminalApply a safe consumer logic on elements without returning a value
count()TerminalReturn a primitive long total of stream items
findFirst()TerminalReturn an Optional containing the initial valid pipeline value
anyMatch(predicate)TerminalCheck if at least one element matches a condition
allMatch(predicate)TerminalCheck if every element matches a condition
noneMatch(predicate)TerminalCheck if no elements match a condition
Telemetry Integration

Completed working through this block? Sync progress to workspace.