Skip to main content
Ajay Dhangar
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.

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

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
Finished reading? Mark this topic as complete.