Skip to main content

Java Stream API

Table of Contents

  1. Introduction
  2. Stream Creation
  3. Intermediate Operations
  4. Terminal Operations
  5. Specialized Streams
  6. Collectors
  7. Advanced Patterns
  8. Parallel Streams
  9. Common DSA Patterns
  10. Best Practices
  11. Performance Considerations

Introduction

Stream API was introduced in Java 8 to provide a functional programming approach to process collections of data. Streams represent a sequence of elements that can be processed in a declarative way.

Key Concepts

  • Stream: A sequence of elements supporting sequential and parallel aggregate operations
  • Lazy Evaluation: Intermediate operations are not executed until a terminal operation is invoked
  • Immutable: Streams don't modify the original data source
  • One-time Use: A stream can only be consumed once

Stream Pipeline

Data Source → Intermediate Operations → Terminal Operation → Result

Stream Creation

From Collections

List<String> list = Arrays.asList("apple", "banana", "cherry");

// From Collection
Stream<String> stream1 = list.stream();
Stream<String> parallelStream = list.parallelStream();

// From Array
String[] array = {"a", "b", "c"};
Stream<String> stream2 = Arrays.stream(array);
Stream<String> stream3 = Stream.of("x", "y", "z");

Builder Pattern

Stream<String> stream = Stream.<String>builder()
.add("first")
.add("second")
.add("third")
.build();

Generate and Iterate

// Generate infinite stream
Stream<Double> randomNumbers = Stream.generate(Math::random)
.limit(10);

// Iterate with seed and function
Stream<Integer> evenNumbers = Stream.iterate(0, n -> n + 2)
.limit(10); // 0, 2, 4, 6, 8, 10, 12, 14, 16, 18

// Java 9+ - Iterate with predicate
Stream<Integer> numbers = Stream.iterate(1, n -> n <= 100, n -> n + 1);

Range Streams

// IntStream range
IntStream.range(1, 5) // 1, 2, 3, 4 (exclusive end)
.forEach(System.out::println);

IntStream.rangeClosed(1, 5) // 1, 2, 3, 4, 5 (inclusive end)
.forEach(System.out::println);

From Files and Other Sources

// From file lines
try (Stream<String> lines = Files.lines(Paths.get("file.txt"))) {
lines.filter(line -> line.length() > 10)
.forEach(System.out::println);
}

// From string characters
"hello".chars()
.mapToObj(c -> (char) c)
.forEach(System.out::println);

Intermediate Operations

Intermediate operations return a new Stream and are lazy - they're not executed until a terminal operation is called.

filter()

Signature: Stream<T> filter(Predicate<? super T> predicate) Purpose: Keep elements that match the predicate

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

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

// Filter strings by length
List<String> words = Arrays.asList("java", "stream", "api", "filter");
List<String> longWords = words.stream()
.filter(word -> word.length() > 3)
.collect(Collectors.toList()); // ["java", "stream", "filter"]

// Multiple filters (chaining)
List<Integer> result = numbers.stream()
.filter(n -> n > 3)
.filter(n -> n < 8)
.collect(Collectors.toList()); // [4, 5, 6, 7]

map()

Signature: <R> Stream<R> map(Function<? super T, ? extends R> mapper) Purpose: Transform each element to another type

List<String> words = Arrays.asList("apple", "banana", "cherry");

// Transform to uppercase
List<String> upperWords = words.stream()
.map(String::toUpperCase)
.collect(Collectors.toList()); // ["APPLE", "BANANA", "CHERRY"]

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

// Transform objects
class Person {
String name; int age;
Person(String name, int age) { this.name = name; this.age = age; }
String getName() { return name; }
int getAge() { return age; }
}

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

List<String> names = people.stream()
.map(Person::getName)
.collect(Collectors.toList()); // ["Alice", "Bob", "Charlie"]

// Chain transformations
List<String> processedNames = people.stream()
.map(Person::getName)
.map(String::toUpperCase)
.map(name -> "Mr/Ms " + name)
.collect(Collectors.toList());

flatMap()

Signature: <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper) Purpose: Flatten nested structures into a single stream

// Flatten list of lists
List<List<Integer>> listOfLists = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9)
);

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

// Flatten strings to characters
List<String> words = Arrays.asList("hello", "world");
List<Character> chars = words.stream()
.flatMap(word -> word.chars().mapToObj(c -> (char) c))
.collect(Collectors.toList()); // ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']

// Flatten Optional values
List<Optional<String>> optionals = Arrays.asList(
Optional.of("apple"),
Optional.empty(),
Optional.of("banana")
);

List<String> values = optionals.stream()
.flatMap(Optional::stream) // Java 9+
.collect(Collectors.toList()); // ["apple", "banana"]

distinct()

Signature: Stream<T> distinct() Purpose: Remove duplicate elements (uses equals())

List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 5);
List<Integer> unique = numbers.stream()
.distinct()
.collect(Collectors.toList()); // [1, 2, 3, 4, 5]

// Distinct by property
List<Person> people = Arrays.asList(/* ... */);
List<Person> uniqueByName = people.stream()
.collect(Collectors.toMap(
Person::getName,
p -> p,
(existing, replacement) -> existing))
.values()
.stream()
.collect(Collectors.toList());

// Or using custom distinctBy (Java 9+ or custom utility)
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}

List<Person> uniqueByAge = people.stream()
.filter(distinctByKey(Person::getAge))
.collect(Collectors.toList());

sorted()

Signature: Stream<T> sorted() or Stream<T> sorted(Comparator<? super T> comparator) Purpose: Sort elements

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

// Natural order
List<Integer> sorted = numbers.stream()
.sorted()
.collect(Collectors.toList()); // [1, 1, 2, 3, 4, 5, 6, 9]

// Reverse order
List<Integer> reverseSorted = numbers.stream()
.sorted(Collections.reverseOrder())
.collect(Collectors.toList()); // [9, 6, 5, 4, 3, 2, 1, 1]

// Custom comparator
List<String> words = Arrays.asList("apple", "pie", "banana", "cherry");
List<String> sortedByLength = words.stream()
.sorted(Comparator.comparing(String::length))
.collect(Collectors.toList()); // ["pie", "apple", "banana", "cherry"]

// Multiple criteria sorting
List<Person> people = Arrays.asList(/* ... */);
List<Person> sortedPeople = people.stream()
.sorted(Comparator.comparing(Person::getAge)
.thenComparing(Person::getName))
.collect(Collectors.toList());

peek()

Signature: Stream<T> peek(Consumer<? super T> action) Purpose: Debug/inspect elements without modifying the stream

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

List<Integer> result = numbers.stream()
.peek(n -> System.out.println("Original: " + n))
.filter(n -> n % 2 == 0)
.peek(n -> System.out.println("After filter: " + n))
.map(n -> n * 2)
.peek(n -> System.out.println("After map: " + n))
.collect(Collectors.toList());

limit() and skip()

Signatures: Stream<T> limit(long maxSize), Stream<T> skip(long n) Purpose: Pagination and slicing

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

// Take first 5 elements
List<Integer> first5 = numbers.stream()
.limit(5)
.collect(Collectors.toList()); // [1, 2, 3, 4, 5]

// Skip first 3 elements
List<Integer> skip3 = numbers.stream()
.skip(3)
.collect(Collectors.toList()); // [4, 5, 6, 7, 8, 9, 10]

// Pagination: skip 3, take 4
List<Integer> page2 = numbers.stream()
.skip(3)
.limit(4)
.collect(Collectors.toList()); // [4, 5, 6, 7]

// Infinite stream with limit
List<Integer> fibonacci = Stream.iterate(new int[]{0, 1},
arr -> new int[]{arr[1], arr[0] + arr[1]})
.map(arr -> arr[0])
.limit(10)
.collect(Collectors.toList()); // First 10 Fibonacci numbers

Terminal Operations

Terminal operations produce a result or side effect and close the stream.

forEach()

Signature: void forEach(Consumer<? super T> action) Purpose: Perform an action on each element

List<String> words = Arrays.asList("hello", "stream", "world");

// Print each element
words.stream().forEach(System.out::println);

// With index (using IntStream)
IntStream.range(0, words.size())
.forEach(i -> System.out.println(i + ": " + words.get(i)));

collect()

Signature: <R> R collect(Collector<? super T, A, R> collector) Purpose: Convert stream to collection or other data structures

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

// To List
List<Integer> list = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());

// To Set
Set<Integer> set = numbers.stream()
.collect(Collectors.toSet());

// To specific collection
LinkedList<Integer> linkedList = numbers.stream()
.collect(Collectors.toCollection(LinkedList::new));

// To Array
Integer[] array = numbers.stream()
.toArray(Integer[]::new);

// Join strings
String joined = Arrays.asList("a", "b", "c").stream()
.collect(Collectors.joining(", ")); // "a, b, c"

reduce()

Signatures:

  • Optional<T> reduce(BinaryOperator<T> accumulator)
  • T reduce(T identity, BinaryOperator<T> accumulator)
  • <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

Purpose: Combine elements into a single result

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

// Sum
Optional<Integer> sum = numbers.stream()
.reduce((a, b) -> a + b);

// Sum with identity
int sum2 = numbers.stream()
.reduce(0, (a, b) -> a + b);

// Using Integer::sum
int sum3 = numbers.stream()
.reduce(0, Integer::sum);

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

// Find maximum
Optional<Integer> max = numbers.stream()
.reduce(Integer::max);

// Concatenate strings
List<String> words = Arrays.asList("Hello", " ", "Stream", " ", "API");
String sentence = words.stream()
.reduce("", (a, b) -> a + b); // "Hello Stream API"

// Complex reduction
List<Person> people = Arrays.asList(/* ... */);
int totalAge = people.stream()
.map(Person::getAge)
.reduce(0, Integer::sum);

count()

Signature: long count() Purpose: Count elements

List<String> words = Arrays.asList("apple", "banana", "cherry", "date");

long count = words.stream()
.filter(word -> word.length() > 5)
.count(); // 2

// Count distinct elements
long distinctCount = Arrays.asList(1, 2, 2, 3, 3, 3).stream()
.distinct()
.count(); // 3

min() and max()

Signatures: Optional<T> min(Comparator<? super T> comparator) Purpose: Find minimum/maximum element

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

Optional<Integer> min = numbers.stream()
.min(Integer::compareTo);

Optional<Integer> max = numbers.stream()
.max(Integer::compareTo);

// Custom objects
List<Person> people = Arrays.asList(/* ... */);
Optional<Person> youngest = people.stream()
.min(Comparator.comparing(Person::getAge));

Optional<Person> oldest = people.stream()
.max(Comparator.comparing(Person::getAge));

anyMatch(), allMatch(), noneMatch()

Signatures: boolean anyMatch(Predicate<? super T> predicate) Purpose: Test stream elements against predicates

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

boolean hasEven = numbers.stream()
.anyMatch(n -> n % 2 == 0); // true

boolean allPositive = numbers.stream()
.allMatch(n -> n > 0); // true

boolean noneNegative = numbers.stream()
.noneMatch(n -> n < 0); // true

// Short-circuiting behavior
boolean hasLargeNumber = Stream.iterate(1, n -> n + 1)
.peek(System.out::println) // Only prints until a match is found
.anyMatch(n -> n > 1000);

findFirst() and findAny()

Signatures: Optional<T> findFirst(), Optional<T> findAny() Purpose: Find elements

List<String> words = Arrays.asList("apple", "banana", "cherry");

Optional<String> first = words.stream()
.filter(word -> word.startsWith("b"))
.findFirst(); // Optional["banana"]

Optional<String> any = words.parallelStream()
.filter(word -> word.length() > 5)
.findAny(); // Any matching element (useful in parallel streams)

// With orElse
String result = words.stream()
.filter(word -> word.startsWith("z"))
.findFirst()
.orElse("Not found");

Specialized Streams

IntStream, LongStream, DoubleStream

These specialized streams avoid boxing/unboxing overhead and provide additional methods.

IntStream

// Creation
IntStream stream1 = IntStream.of(1, 2, 3, 4, 5);
IntStream stream2 = IntStream.range(1, 10); // 1 to 9
IntStream stream3 = IntStream.rangeClosed(1, 10); // 1 to 10

// From arrays
int[] array = {1, 2, 3, 4, 5};
IntStream stream4 = Arrays.stream(array);

// Statistical operations
IntSummaryStatistics stats = IntStream.range(1, 100)
.summaryStatistics();

System.out.println("Count: " + stats.getCount());
System.out.println("Sum: " + stats.getSum());
System.out.println("Min: " + stats.getMin());
System.out.println("Max: " + stats.getMax());
System.out.println("Average: " + stats.getAverage());

// Math operations
int sum = IntStream.range(1, 10).sum(); // 45
OptionalDouble avg = IntStream.range(1, 10).average(); // 5.0
OptionalInt max = IntStream.of(1, 5, 3, 9, 2).max(); // 9

// Converting between streams
Stream<Integer> boxed = IntStream.range(1, 5).boxed();
IntStream fromStream = Stream.of(1, 2, 3, 4).mapToInt(Integer::intValue);

// Advanced operations
IntStream.range(1, 20)
.filter(n -> n % 2 == 0) // Even numbers
.map(n -> n * n) // Square them
.limit(5) // Take first 5
.forEach(System.out::println); // Print: 4, 16, 36, 64, 100

Conversion Between Stream Types

List<String> numbers = Arrays.asList("1", "2", "3", "4", "5");

// String to IntStream
IntStream intStream = numbers.stream()
.mapToInt(Integer::parseInt);

// IntStream to DoubleStream
DoubleStream doubleStream = intStream
.mapToDouble(i -> i * 1.5);

// Back to Stream<Double>
Stream<Double> boxedDoubles = doubleStream.boxed();

Collectors

The Collectors utility class provides common reduction operations.

Basic Collectors

List<String> words = Arrays.asList("apple", "banana", "cherry", "date");

// toList, toSet
List<String> list = words.stream().collect(Collectors.toList());
Set<String> set = words.stream().collect(Collectors.toSet());

// toMap
Map<String, Integer> wordLengths = words.stream()
.collect(Collectors.toMap(
word -> word, // key
String::length // value
));

// Handling duplicate keys
Map<Integer, String> lengthToWord = words.stream()
.collect(Collectors.toMap(
String::length, // key
word -> word, // value
(existing, replacement) -> existing + ", " + replacement // merge function
));

Joining

List<String> words = Arrays.asList("apple", "banana", "cherry");

String joined1 = words.stream()
.collect(Collectors.joining()); // "applebananacherry"

String joined2 = words.stream()
.collect(Collectors.joining(", ")); // "apple, banana, cherry"

String joined3 = words.stream()
.collect(Collectors.joining(", ", "[", "]")); // "[apple, banana, cherry]"

// Transform then join
String upperJoined = words.stream()
.map(String::toUpperCase)
.collect(Collectors.joining(" | ")); // "APPLE | BANANA | CHERRY"

Grouping

class Person {
String name; int age; String department;
// constructors, getters...
}

List<Person> people = Arrays.asList(
new Person("Alice", 25, "IT"),
new Person("Bob", 30, "HR"),
new Person("Charlie", 35, "IT"),
new Person("David", 28, "HR")
);

// Group by single field
Map<String, List<Person>> byDepartment = people.stream()
.collect(Collectors.groupingBy(Person::getDepartment));

// Group by age range
Map<String, List<Person>> byAgeGroup = people.stream()
.collect(Collectors.groupingBy(person ->
person.getAge() < 30 ? "Young" : "Senior"
));

// Group and count
Map<String, Long> countByDepartment = people.stream()
.collect(Collectors.groupingBy(
Person::getDepartment,
Collectors.counting()
));

// Group and collect names
Map<String, List<String>> namesByDepartment = people.stream()
.collect(Collectors.groupingBy(
Person::getDepartment,
Collectors.mapping(Person::getName, Collectors.toList())
));

// Multi-level grouping
Map<String, Map<String, List<Person>>> byDeptAndAgeGroup = people.stream()
.collect(Collectors.groupingBy(
Person::getDepartment,
Collectors.groupingBy(person ->
person.getAge() < 30 ? "Young" : "Senior"
)
));

Partitioning

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

// Partition by even/odd
Map<Boolean, List<Integer>> partitioned = numbers.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
// {false=[1, 3, 5, 7, 9], true=[2, 4, 6, 8, 10]}

// Partition and count
Map<Boolean, Long> evenOddCount = numbers.stream()
.collect(Collectors.partitioningBy(
n -> n % 2 == 0,
Collectors.counting()
));
// {false=5, true=5}

Statistical Collectors

List<Person> people = Arrays.asList(/* ... */);

// Summary statistics
IntSummaryStatistics ageStats = people.stream()
.collect(Collectors.summarizingInt(Person::getAge));

System.out.println("Average age: " + ageStats.getAverage());
System.out.println("Max age: " + ageStats.getMax());

// Average
Double averageAge = people.stream()
.collect(Collectors.averagingInt(Person::getAge));

// Sum
Integer totalAge = people.stream()
.collect(Collectors.summingInt(Person::getAge));

Custom Collectors

// Custom collector to create a StringBuilder
public static Collector<String, StringBuilder, String> toStringBuilder() {
return Collector.of(
StringBuilder::new, // supplier
StringBuilder::append, // accumulator
StringBuilder::append, // combiner
StringBuilder::toString // finisher
);
}

// Usage
String result = Stream.of("Hello", " ", "World")
.collect(toStringBuilder()); // "Hello World"

Advanced Patterns

Optional Integration

List<Person> people = Arrays.asList(/* ... */);

// Find person and transform
Optional<String> oldestPersonName = people.stream()
.max(Comparator.comparing(Person::getAge))
.map(Person::getName);

// Chaining operations
String result = people.stream()
.filter(p -> p.getAge() > 30)
.findFirst()
.map(Person::getName)
.map(String::toUpperCase)
.orElse("No person found");

// flatMap with Optional
List<Optional<String>> optionalNames = Arrays.asList(
Optional.of("Alice"),
Optional.empty(),
Optional.of("Bob")
);

List<String> names = optionalNames.stream()
.flatMap(Optional::stream) // Java 9+
.collect(Collectors.toList());

Stream Concatenation

Stream<String> stream1 = Stream.of("a", "b", "c");
Stream<String> stream2 = Stream.of("d", "e", "f");

Stream<String> concatenated = Stream.concat(stream1, stream2);
// Result: "a", "b", "c", "d", "e", "f"

// Multiple streams
Stream<String> multiConcat = Stream.of(
Stream.of("1", "2"),
Stream.of("3", "4"),
Stream.of("5", "6")
)
.flatMap(s -> s); // "1", "2", "3", "4", "5", "6"

Conditional Processing

List<String> words = Arrays.asList("apple", "banana", "cherry");
boolean processLongWords = true;

Stream<String> stream = words.stream();

if (processLongWords) {
stream = stream.filter(word -> word.length() > 5);
}

List<String> result = stream
.map(String::toUpperCase)
.collect(Collectors.toList());

Stream with Side Effects (Debugging)

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

List<Integer> result = numbers.stream()
.peek(n -> System.out.println("Original: " + n))
.filter(n -> n % 2 == 0)
.peek(n -> System.out.println("After filter: " + n))
.map(n -> n * 2)
.peek(n -> System.out.println("After map: " + n))
.collect(Collectors.toList());

Parallel Streams

When to Use Parallel Streams

  • Large datasets (thousands of elements)
  • CPU-intensive operations
  • Independent operations (no shared state)
  • Stateless operations
List<Integer> largeList = IntStream.range(1, 1000000)
.boxed()
.collect(Collectors.toList());

// Sequential processing
long sequentialSum = largeList.stream()
.mapToLong(Integer::longValue)
.sum();

// Parallel processing
long parallelSum = largeList.parallelStream()
.mapToLong(Integer::longValue)
.sum();

// Or convert to parallel
long sum = largeList.stream()
.parallel()
.mapToLong(Integer::longValue)
.sum();

// Convert back to sequential
List<Integer> result = largeList.parallelStream()
.filter(n -> n % 2 == 0)
.sequential()
.limit(10) // Sequential operations after this
.collect(Collectors.toList());

Thread Safety Considerations

// ❌ Not thread-safe - shared mutable state
List<Integer> results = new ArrayList<>();
IntStream.range(1, 1000).parallel()
.forEach(results::add); // Race condition!

// ✅ Thread-safe alternatives
List<Integer> safeResults = IntStream.range(1, 1000).parallel()
.boxed()
.collect(Collectors.toList());

// ✅ Using thread-safe collections
List<Integer> synchronizedResults = Collections.synchronizedList(new ArrayList<>());
IntStream.range(1, 1000).parallel()
.forEach(synchronizedResults::add);

// ✅ Using concurrent collectors
Map<String, List<Integer>> groupedResults = IntStream.range(1, 1000).parallel()
.boxed()
.collect(Collectors.groupingByConcurrent(
n -> n % 2 == 0 ? "even" : "odd"
));

Common DSA Patterns

Two Pointers Pattern

// Find pair with target sum
public boolean hasPairWithSum(int[] arr, int target) {
Set<Integer> seen = Arrays.stream(arr)
.boxed()
.collect(Collectors.toSet());

return Arrays.stream(arr)
.anyMatch(num -> seen.contains(target - num) && target - num != num);
}

Sliding Window Pattern

// Find maximum sum of subarray of size k
public int maxSumSubarray(int[] arr, int k) {
return IntStream.range(0, arr.length - k + 1)
.map(i -> IntStream.range(i, i + k)
.map(j -> arr[j])
.sum())
.max()
.orElse(0);
}

Frequency Counting

// Character frequency
public Map<Character, Long> getCharFrequency(String str) {
return str.chars()
.mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(
c -> c,
Collectors.counting()
));
}

// Word frequency
public Map<String, Long> getWordFrequency(List<String> words) {
return words.stream()
.collect(Collectors.groupingBy(
String::toLowerCase,
Collectors.counting()
));
}

Array/List Transformations

// Matrix operations
public List<List<Integer>> transpose(List<List<Integer>> matrix) {
return IntStream.range(0, matrix.get(0).size())
.mapToObj(col -> matrix.stream()
.map(row -> row.get(col))
.collect(Collectors.toList()))
.collect(Collectors.toList());
}

// Find all subarrays
public List<List<Integer>> getAllSubarrays(List<Integer> list) {
return IntStream.range(0, list.size())
.boxed()
.flatMap(start -> IntStream.range(start + 1, list.size() + 1)
.mapToObj(end -> list.subList(start, end)))
.collect(Collectors.toList());
}

Tree/Graph Traversal Simulation

// Level order traversal simulation using streams
class TreeNode {
int val;
List<TreeNode> children;
TreeNode(int val) { this.val = val; this.children = new ArrayList<>(); }
}

public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null) return new ArrayList<>();

List<List<TreeNode>> levels = new ArrayList<>();
levels.add(Arrays.asList(root));

return levels.stream()
.map(level -> level.stream()
.map(node -> node.val)
.collect(Collectors.toList()))
.collect(Collectors.toList());
}

String Processing Patterns

// Anagram grouping
public List<List<String>> groupAnagrams(String[] strs) {
return Arrays.stream(strs)
.collect(Collectors.groupingBy(str -> {
char[] chars = str.toCharArray();
Arrays.sort(chars);
return new String(chars);
}))
.values()
.stream()
.collect(Collectors.toList());
}

// Valid parentheses check
public boolean isValidParentheses(String s) {
Map<Character, Character> pairs = Map.of(')', '(', '}', '{', ']', '[');

return s.chars()
.mapToObj(c -> (char) c)
.reduce(new ArrayDeque<Character>(),
(stack, ch) -> {
if (pairs.containsKey(ch)) {
if (stack.isEmpty() || stack.pop() != pairs.get(ch)) {
stack.push('X'); // Invalid marker
}
} else {
stack.push(ch);
}
return stack;
},
(stack1, stack2) -> { stack1.addAll(stack2); return stack1; })
.isEmpty();
}

Number Theory Patterns

// Prime number generation
public List<Integer> sieveOfEratosthenes(int n) {
boolean[] isPrime = new boolean[n + 1];
Arrays.fill(isPrime, 2, n + 1, true);

IntStream.rangeClosed(2, (int) Math.sqrt(n))
.filter(i -> isPrime[i])
.forEach(i -> IntStream.iterate(i * i, j -> j <= n, j -> j + i)
.forEach(j -> isPrime[j] = false));

return IntStream.rangeClosed(2, n)
.filter(i -> isPrime[i])
.boxed()
.collect(Collectors.toList());
}

// Fibonacci sequence
public List<Integer> fibonacciSequence(int n) {
return Stream.iterate(new int[]{0, 1}, arr -> new int[]{arr[1], arr[0] + arr[1]})
.limit(n)
.map(arr -> arr[0])
.collect(Collectors.toList());
}

Best Practices

1. Readability Over Performance (in most cases)

// ✅ Clear and readable
List<String> result = words.stream()
.filter(word -> word.length() > 3)
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList());

// ❌ Trying to be too clever
List<String> result2 = words.stream()
.filter(((Predicate<String>) word -> word.length() <= 3).negate())
.collect(Collectors.mapping(
String::toUpperCase,
Collectors.collectingAndThen(
Collectors.toList(),
list -> { Collections.sort(list); return list; }
)
));

2. Prefer Method References

// ✅ Method references
people.stream()
.map(Person::getName)
.filter(Objects::nonNull)
.forEach(System.out::println);

// ❌ Unnecessary lambdas
people.stream()
.map(person -> person.getName())
.filter(name -> name != null)
.forEach(name -> System.out.println(name));

3. Handle Null Values Properly

List<String> words = Arrays.asList("apple", null, "banana", null, "cherry");

// ✅ Filter out nulls
List<String> nonNulls = words.stream()
.filter(Objects::nonNull)
.collect(Collectors.toList());

// ✅ Use Optional for nullable results
Optional<String> longest = words.stream()
.filter(Objects::nonNull)
.max(Comparator.comparing(String::length));

4. Choose Appropriate Collection Types

// ✅ Use Set when uniqueness is required
Set<String> uniqueWords = words.stream()
.collect(Collectors.toSet());

// ✅ Use LinkedHashSet to preserve order
Set<String> orderedUniqueWords = words.stream()
.collect(Collectors.toCollection(LinkedHashSet::new));

// ✅ Use TreeSet for sorted unique elements
Set<String> sortedUniqueWords = words.stream()
.collect(Collectors.toCollection(TreeSet::new));

5. Minimize Stream Operations

// ❌ Multiple passes
List<String> upperWords = words.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());

List<String> filteredWords = upperWords.stream()
.filter(word -> word.length() > 5)
.collect(Collectors.toList());

// ✅ Single pass
List<String> result = words.stream()
.map(String::toUpperCase)
.filter(word -> word.length() > 5)
.collect(Collectors.toList());

6. Use Appropriate Terminal Operations

// ✅ Use anyMatch instead of filter + findAny
boolean hasLongWord = words.stream()
.anyMatch(word -> word.length() > 10);

// ❌ Unnecessary filtering
boolean hasLongWord2 = words.stream()
.filter(word -> word.length() > 10)
.findAny()
.isPresent();

Performance Considerations

1. Stream Creation Overhead

// ❌ Creating streams for small operations
int sum = Stream.of(1, 2, 3).mapToInt(Integer::intValue).sum(); // Overkill

// ✅ Direct calculation for simple cases
int sum = 1 + 2 + 3;

2. Boxing/Unboxing

// ❌ Unnecessary boxing
int sum = IntStream.range(1, 1000)
.boxed() // Boxing to Integer
.mapToInt(Integer::intValue) // Unboxing back to int
.sum();

// ✅ Stay with primitives
int sum = IntStream.range(1, 1000).sum();

3. Parallel Stream Considerations

// ✅ Good candidate for parallelization
long sum = IntStream.range(1, 10_000_000)
.parallel()
.filter(n -> isPrime(n)) // CPU-intensive
.count();

// ❌ Poor candidate due to ordering requirement
List<Integer> firstTen = IntStream.range(1, 1000000)
.parallel()
.limit(10) // Requires ordering
.boxed()
.collect(Collectors.toList());

4. Memory Usage

// ❌ Materializing large intermediate collections
List<String> processed = hugeList.stream()
.collect(Collectors.toList()) // Unnecessary collection
.stream()
.filter(someCondition)
.collect(Collectors.toList());

// ✅ Single stream pipeline
List<String> processed = hugeList.stream()
.filter(someCondition)
.collect(Collectors.toList());

Error Handling in Streams

1. Using try-catch in Lambdas

public static <T, R> Function<T, Optional<R>> wrap(Function<T, R> function) {
return input -> {
try {
return Optional.of(function.apply(input));
} catch (Exception e) {
return Optional.empty();
}
};
}

// Usage
List<String> numbers = Arrays.asList("1", "2", "invalid", "4");
List<Integer> validNumbers = numbers.stream()
.map(wrap(Integer::parseInt))
.flatMap(Optional::stream)
.collect(Collectors.toList());

2. Custom Exception Handling

public static <T> Predicate<T> handleExceptions(Predicate<T> predicate) {
return input -> {
try {
return predicate.test(input);
} catch (Exception e) {
System.err.println("Error processing: " + input + ", " + e.getMessage());
return false;
}
};
}

Debugging Streams

1. Using peek() for Debugging

List<Integer> result = numbers.stream()
.peek(n -> System.out.println("Processing: " + n))
.filter(n -> n % 2 == 0)
.peek(n -> System.out.println("After filter: " + n))
.map(n -> n * 2)
.peek(n -> System.out.println("After map: " + n))
.collect(Collectors.toList());

2. Logging Stream Operations

public static <T> Stream<T> log(Stream<T> stream, String operation) {
return stream.peek(item ->
System.out.println(operation + ": " + item)
);
}

// Usage
List<String> result = log(words.stream(), "Input")
.filter(word -> word.length() > 3)
.collect(Collectors.toList());

Java 21+ Stream Features

1. Stream.gather() (Preview in Java 22+)

// Note: This is a preview feature
// Gather allows custom intermediate operations

// Example: Sliding window
Stream.of(1, 2, 3, 4, 5, 6)
.gather(Gatherers.windowSliding(3))
.forEach(System.out::println);
// Output: [1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6]

2. Enhanced Pattern Matching with Streams

// Using pattern matching in filter operations
record Person(String name, int age) {}
record Student(String name, int age, String school) {}

List<Object> people = Arrays.asList(
new Person("Alice", 25),
new Student("Bob", 20, "MIT"),
"Not a person"
);

List<String> names = people.stream()
.filter(obj -> switch (obj) {
case Person p -> true;
case Student s -> true;
default -> false;
})
.map(obj -> switch (obj) {
case Person(var name, var age) -> name;
case Student(var name, var age, var school) -> name;
default -> "";
})
.collect(Collectors.toList());

Real-World Examples

1. Processing CSV Data

public class CsvProcessor {

record Employee(String name, String department, int salary, int age) {}

public static void processCsvData(List<String> csvLines) {
List<Employee> employees = csvLines.stream()
.skip(1) // Skip header
.map(line -> line.split(","))
.map(parts -> new Employee(
parts[0].trim(),
parts[1].trim(),
Integer.parseInt(parts[2].trim()),
Integer.parseInt(parts[3].trim())
))
.collect(Collectors.toList());

// Department-wise analysis
Map<String, DoubleSummaryStatistics> deptSalaryStats = employees.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.summarizingDouble(Employee::salary)
));

// Top 5 highest paid employees
List<Employee> topEarners = employees.stream()
.sorted(Comparator.comparing(Employee::salary).reversed())
.limit(5)
.collect(Collectors.toList());

// Age groups
Map<String, Long> ageGroups = employees.stream()
.collect(Collectors.groupingBy(
emp -> emp.age() < 30 ? "Young" :
emp.age() < 50 ? "Middle" : "Senior",
Collectors.counting()
));
}
}

2. Log File Analysis

public class LogAnalyzer {

record LogEntry(LocalDateTime timestamp, String level, String message) {}

public static void analyzeLogFile(Stream<String> logLines) {
List<LogEntry> entries = logLines
.filter(line -> !line.isEmpty())
.map(LogAnalyzer::parseLogLine)
.filter(Objects::nonNull)
.collect(Collectors.toList());

// Error count by hour
Map<Integer, Long> errorsByHour = entries.stream()
.filter(entry -> "ERROR".equals(entry.level()))
.collect(Collectors.groupingBy(
entry -> entry.timestamp().getHour(),
Collectors.counting()
));

// Most common error messages
Map<String, Long> errorMessages = entries.stream()
.filter(entry -> "ERROR".equals(entry.level()))
.collect(Collectors.groupingBy(
LogEntry::message,
Collectors.counting()
));

List<Map.Entry<String, Long>> topErrors = errorMessages.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(10)
.collect(Collectors.toList());
}

private static LogEntry parseLogLine(String line) {
// Implementation depends on log format
return null; // Placeholder
}
}

3. API Response Processing

public class ApiResponseProcessor {

record User(String id, String name, String email, boolean active) {}
record Order(String userId, double amount, LocalDate date) {}

public static void processUserOrders(List<User> users, List<Order> orders) {
// Active users with orders
Set<String> usersWithOrders = orders.stream()
.map(Order::userId)
.collect(Collectors.toSet());

List<User> activeUsersWithOrders = users.stream()
.filter(User::active)
.filter(user -> usersWithOrders.contains(user.id()))
.collect(Collectors.toList());

// Monthly revenue
Map<YearMonth, Double> monthlyRevenue = orders.stream()
.collect(Collectors.groupingBy(
order -> YearMonth.from(order.date()),
Collectors.summingDouble(Order::amount)
));

// Top customers by spending
Map<String, Double> userSpending = orders.stream()
.collect(Collectors.groupingBy(
Order::userId,
Collectors.summingDouble(Order::amount)
));

List<Map.Entry<String, Double>> topCustomers = userSpending.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.limit(10)
.collect(Collectors.toList());
}
}

Stream API Practice Problems:


import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class StreamAPI {

// 1. Remove duplicates from array
public static List<Integer> removeDuplicates(int[] nums) {
return Arrays.stream(nums)
.distinct()
.boxed()
.collect(Collectors.toList());
}

// 2. Merge two arrays
public static List<Integer> mergeTwoLists(int[] nums1, int[] nums2) {
return IntStream.concat(Arrays.stream(nums1), Arrays.stream(nums2))
.boxed()
.collect(Collectors.toList());
}

// 3. Find maximum element
public static Integer findMax(int[] nums) {
return Arrays.stream(nums)
.boxed()
.max(Integer::compareTo)
.orElse(null);
}

// 4. Find minimum element
public static Integer findMin(int[] nums) {
return Arrays.stream(nums)
.boxed()
.min(Integer::compareTo)
.orElse(null);
}

// 5. Filter elements greater than k
public static List<Integer> elementsGreaterThan(int[] nums, int k) {
return Arrays.stream(nums)
.boxed()
.filter(num -> num > k)
.collect(Collectors.toList());
}

// 6. Convert strings to uppercase
public static List<String> toUpperCase(String[] words) {
return Arrays.stream(words)
.map(String::toUpperCase)
.collect(Collectors.toList());
}

// 7. Merge two arrays without duplicates
public static List<Integer> mergeTwoListsWithoutDuplicates(int[] nums1, int[] nums2) {
return IntStream.concat(Arrays.stream(nums1), Arrays.stream(nums2))
.distinct()
.boxed()
.collect(Collectors.toList());
}

// 8. Find first non-repeating character (Fixed implementation)
public static String firstNonRepeatingCharacter(String word) {
return word.chars()
.mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(Function.identity(), LinkedHashMap::new, Collectors.counting()))
.entrySet()
.stream()
.filter(entry -> entry.getValue() == 1)
.map(Map.Entry::getKey)
.findFirst()
.map(String::valueOf)
.orElse("No non-repeating character found");
}

// 9. Sum of all elements
public static int sumOfElements(int[] nums) {
return Arrays.stream(nums).sum();
}

// 10. Average of all elements
public static double averageOfElements(int[] nums) {
return Arrays.stream(nums)
.average()
.orElse(0.0);
}

// 11. Count elements greater than k
public static long countElementsGreaterThan(int[] nums, int k) {
return Arrays.stream(nums)
.filter(num -> num > k)
.count();
}

// 12. Sort array in ascending order
public static List<Integer> sortAscending(int[] nums) {
return Arrays.stream(nums)
.boxed()
.sorted()
.collect(Collectors.toList());
}

// 13. Sort array in descending order
public static List<Integer> sortDescending(int[] nums) {
return Arrays.stream(nums)
.boxed()
.sorted(Collections.reverseOrder())
.collect(Collectors.toList());
}

// 14. Find even numbers
public static List<Integer> findEvenNumbers(int[] nums) {
return Arrays.stream(nums)
.boxed()
.filter(num -> num % 2 == 0)
.collect(Collectors.toList());
}

// 15. Find odd numbers
public static List<Integer> findOddNumbers(int[] nums) {
return Arrays.stream(nums)
.boxed()
.filter(num -> num % 2 != 0)
.collect(Collectors.toList());
}

// 16. Square of all numbers
public static List<Integer> squareOfNumbers(int[] nums) {
return Arrays.stream(nums)
.map(num -> num * num)
.boxed()
.collect(Collectors.toList());
}

// 17. Find second highest element
public static Integer findSecondHighest(int[] nums) {
return Arrays.stream(nums)
.boxed()
.distinct()
.sorted(Collections.reverseOrder())
.skip(1)
.findFirst()
.orElse(null);
}

// 18. Find second lowest element
public static Integer findSecondLowest(int[] nums) {
return Arrays.stream(nums)
.boxed()
.distinct()
.sorted()
.skip(1)
.findFirst()
.orElse(null);
}

// 19. Group strings by length
public static Map<Integer, List<String>> groupByLength(String[] words) {
return Arrays.stream(words)
.collect(Collectors.groupingBy(String::length));
}

// 20. Find frequency of each element
public static Map<Integer, Long> findFrequency(int[] nums) {
return Arrays.stream(nums)
.boxed()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
}

// 21. Check if all elements are positive
public static boolean allPositive(int[] nums) {
return Arrays.stream(nums).allMatch(num -> num > 0);
}

// 22. Check if any element is negative
public static boolean anyNegative(int[] nums) {
return Arrays.stream(nums).anyMatch(num -> num < 0);
}

// 23. Find strings starting with specific character
public static List<String> stringsStartingWith(String[] words, char ch) {
return Arrays.stream(words)
.filter(word -> word.charAt(0) == ch)
.collect(Collectors.toList());
}

// 24. Join strings with delimiter
public static String joinStrings(String[] words, String delimiter) {
return Arrays.stream(words)
.collect(Collectors.joining(delimiter));
}

// 25. Find longest string
public static String findLongestString(String[] words) {
return Arrays.stream(words)
.max(Comparator.comparing(String::length))
.orElse("");
}

// 26. Find shortest string
public static String findShortestString(String[] words) {
return Arrays.stream(words)
.min(Comparator.comparing(String::length))
.orElse("");
}

// 27. Skip first n elements
public static List<Integer> skipFirstN(int[] nums, int n) {
return Arrays.stream(nums)
.skip(n)
.boxed()
.collect(Collectors.toList());
}

// 28. Take first n elements
public static List<Integer> takeFirstN(int[] nums, int n) {
return Arrays.stream(nums)
.limit(n)
.boxed()
.collect(Collectors.toList());
}

// 29. Partition even and odd numbers
public static Map<Boolean, List<Integer>> partitionEvenOdd(int[] nums) {
return Arrays.stream(nums)
.boxed()
.collect(Collectors.partitioningBy(num -> num % 2 == 0));
}

// 30. Convert to comma separated string
public static String toCommaSeparated(int[] nums) {
return Arrays.stream(nums)
.mapToObj(String::valueOf)
.collect(Collectors.joining(", "));
}

public static void main(String[] args) {
int[] num1 = {1, 2, 3, 3, 1, 1, 5, 7, 9, 4};
int[] num2 = {4, 5, 6, 8, 10, -2};
int[] mixedNums = {1, -2, 3, -4, 5, 6, 7, 8, 9};
String[] words = {"apple", "banana", "kiwi", "orange", "grape", "a"};

System.out.println("=== BASIC OPERATIONS ===");
System.out.println("1. Distinct Elements: " + removeDuplicates(num1));
System.out.println("2. Merged Lists: " + mergeTwoLists(num1, num2));
System.out.println("3. Max Element: " + findMax(num2));
System.out.println("4. Min Element: " + findMin(num2));
System.out.println("5. Elements > 2: " + elementsGreaterThan(num1, 2));
System.out.println("6. Uppercase Words: " + toUpperCase(words));
System.out.println("7. Merged Without Duplicates: " + mergeTwoListsWithoutDuplicates(num1, num2));
System.out.println("8. First Non-Repeating Char in 'hello': " + firstNonRepeatingCharacter("hello"));

System.out.println("\n=== MATHEMATICAL OPERATIONS ===");
System.out.println("9. Sum of Elements: " + sumOfElements(num1));
System.out.println("10. Average of Elements: " + averageOfElements(num1));
System.out.println("11. Count Elements > 2: " + countElementsGreaterThan(num1, 2));
System.out.println("16. Square of Numbers: " + squareOfNumbers(new int[]{1, 2, 3, 4}));

System.out.println("\n=== SORTING OPERATIONS ===");
System.out.println("12. Sort Ascending: " + sortAscending(num1));
System.out.println("13. Sort Descending: " + sortDescending(num1));
System.out.println("17. Second Highest: " + findSecondHighest(num1));
System.out.println("18. Second Lowest: " + findSecondLowest(num1));

System.out.println("\n=== FILTERING OPERATIONS ===");
System.out.println("14. Even Numbers: " + findEvenNumbers(num1));
System.out.println("15. Odd Numbers: " + findOddNumbers(num1));
System.out.println("23. Words starting with 'a': " + stringsStartingWith(words, 'a'));

System.out.println("\n=== GROUPING & FREQUENCY ===");
System.out.println("19. Group by Length: " + groupByLength(words));
System.out.println("20. Frequency Count: " + findFrequency(num1));
System.out.println("29. Partition Even/Odd: " + partitionEvenOdd(num1));

System.out.println("\n=== BOOLEAN OPERATIONS ===");
System.out.println("21. All Positive (num1): " + allPositive(num1));
System.out.println("21. All Positive (mixedNums): " + allPositive(mixedNums));
System.out.println("22. Any Negative (num1): " + anyNegative(num1));
System.out.println("22. Any Negative (mixedNums): " + anyNegative(mixedNums));

System.out.println("\n=== STRING OPERATIONS ===");
System.out.println("24. Join with ' | ': " + joinStrings(words, " | "));
System.out.println("25. Longest String: " + findLongestString(words));
System.out.println("26. Shortest String: " + findShortestString(words));
System.out.println("30. Comma Separated: " + toCommaSeparated(new int[]{1, 2, 3, 4, 5}));

System.out.println("\n=== LIMIT & SKIP OPERATIONS ===");
System.out.println("27. Skip First 3: " + skipFirstN(num1, 3));
System.out.println("28. Take First 5: " + takeFirstN(num1, 5));
}
}

Conclusion

The Java Stream API is a powerful tool for functional-style programming that can make your code more readable, maintainable, and often more efficient. Key takeaways:

  1. Understand the pipeline: Source → Intermediate Operations → Terminal Operation
  2. Leverage method references for cleaner code
  3. Choose the right collectors for your use case
  4. Handle nulls and exceptions gracefully
  5. Use specialized streams (IntStream, etc.) for primitives
  6. Consider parallel streams for large datasets and CPU-intensive operations
  7. Debug with peek() and proper logging
  8. Optimize for readability first, performance second (unless performance is critical)

Remember that streams are not always the best solution - for simple operations or small datasets, traditional loops might be more appropriate. The key is knowing when and how to use streams effectively in your DSA problems and real-world applications.