Java Stream API
Table of Contents
- Introduction
- Stream Creation
- Intermediate Operations
- Terminal Operations
- Specialized Streams
- Collectors
- Advanced Patterns
- Parallel Streams
- Common DSA Patterns
- Best Practices
- 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:
- Understand the pipeline: Source → Intermediate Operations → Terminal Operation
- Leverage method references for cleaner code
- Choose the right collectors for your use case
- Handle nulls and exceptions gracefully
- Use specialized streams (IntStream, etc.) for primitives
- Consider parallel streams for large datasets and CPU-intensive operations
- Debug with peek() and proper logging
- 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.