Streams API in Java
Learn how to process data collections in a powerful, elegant way
Think of Streams like a conveyor belt in a factory! Items (data) move on the belt, and at each station, workers do something to them - sort them, filter out bad ones, transform them, etc. Streams let you process lists of data in the same way - step by step, clean and efficient!
What is a Stream?
A Stream is NOT a data structure - it's a sequence of elements that you can process. Think of it as a pipeline where data flows through and gets transformed along the way. Streams don't store data; they carry it from a source (like a List) through operations.
import java.util.*;import java.util.stream.*;public class StreamBasics { public static void main(String[] args) { // Traditional way: Process a list List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); List<Integer> doubled = new ArrayList<>(); for (Integer num : numbers) { if (num % 2 == 0) { // Filter even numbers doubled.add(num * 2); // Double them } } System.out.println("Traditional: " + doubled); // [4, 8] // Stream way: Much cleaner! List<Integer> streamDoubled = numbers.stream() // Create stream .filter(num -> num % 2 == 0) // Filter even numbers .map(num -> num * 2) // Double them .collect(Collectors.toList()); // Collect results System.out.println("Stream: " + streamDoubled); // [4, 8] // Visual representation: // numbers → [1, 2, 3, 4, 5] // ↓ .stream() // stream → [1, 2, 3, 4, 5] (flowing data) // ↓ .filter(even) // stream → [2, 4] (filtered) // ↓ .map(* 2) // stream → [4, 8] (transformed) // ↓ .collect() // result → [4, 8] (final list) // Key point: Streams are PIPELINES, not collections! }}Creating Streams
You can create streams from various sources:
import java.util.*;import java.util.stream.*;public class CreatingStreams { public static void main(String[] args) { // 1. From Collections (most common) List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); Stream<String> namesStream = names.stream(); namesStream.forEach(System.out::println); // 2. From Arrays String[] array = {"Apple", "Banana", "Cherry"}; Stream<String> arrayStream = Arrays.stream(array); arrayStream.forEach(System.out::println); // 3. Using Stream.of() Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5); numberStream.forEach(System.out::println); // 4. Empty stream Stream<String> emptyStream = Stream.empty(); // 5. Infinite streams // Generate infinite stream of random numbers Stream<Double> randomStream = Stream.generate(Math::random); randomStream.limit(5).forEach(System.out::println); // Take only 5 // Infinite stream with iteration Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 2); infiniteStream.limit(10).forEach(System.out::println); // 0, 2, 4, 6... // 6. From String IntStream charStream = "hello".chars(); charStream.forEach(ch -> System.out.print((char)ch + " ")); // h e l l o // 7. Range streams (for numbers) IntStream range = IntStream.range(1, 6); // 1 to 5 (exclusive 6) range.forEach(System.out::println); // 1, 2, 3, 4, 5 IntStream rangeClosed = IntStream.rangeClosed(1, 5); // 1 to 5 (inclusive) rangeClosed.forEach(System.out::println); // 1, 2, 3, 4, 5 // 8. From Files // Stream<String> lines = Files.lines(Paths.get("file.txt")); // lines.forEach(System.out::println); // Remember: You can only use a stream ONCE! // After terminal operation, create a NEW stream if needed. }}Intermediate Operations
These operations transform a stream into another stream. They are lazy - they don't execute until a terminal operation is called!
import java.util.*;import java.util.stream.*;public class IntermediateOperations { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 1. filter() - Keep only elements that match condition List<Integer> evenNumbers = numbers.stream() .filter(n -> n % 2 == 0) .collect(Collectors.toList()); System.out.println("Even: " + evenNumbers); // [2, 4, 6, 8, 10] // 2. map() - Transform each element List<Integer> squared = numbers.stream() .map(n -> n * n) .collect(Collectors.toList()); System.out.println("Squared: " + squared); // [1, 4, 9, 16, 25...] // 3. flatMap() - Flatten nested structures List<List<Integer>> nested = Arrays.asList( Arrays.asList(1, 2), Arrays.asList(3, 4), Arrays.asList(5, 6) ); List<Integer> flattened = nested.stream() .flatMap(list -> list.stream()) .collect(Collectors.toList()); System.out.println("Flattened: " + flattened); // [1, 2, 3, 4, 5, 6] // 4. distinct() - Remove duplicates List<Integer> duplicates = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 5, 5); List<Integer> unique = duplicates.stream() .distinct() .collect(Collectors.toList()); System.out.println("Unique: " + unique); // [1, 2, 3, 4, 5] // 5. sorted() - Sort elements List<Integer> unsorted = Arrays.asList(5, 2, 8, 1, 9, 3); List<Integer> sorted = unsorted.stream() .sorted() .collect(Collectors.toList()); System.out.println("Sorted: " + sorted); // [1, 2, 3, 5, 8, 9] // Sort in reverse List<Integer> reversed = unsorted.stream() .sorted(Comparator.reverseOrder()) .collect(Collectors.toList()); System.out.println("Reversed: " + reversed); // [9, 8, 5, 3, 2, 1] // 6. limit() - Take first N elements List<Integer> first5 = numbers.stream() .limit(5) .collect(Collectors.toList()); System.out.println("First 5: " + first5); // [1, 2, 3, 4, 5] // 7. skip() - Skip first N elements List<Integer> after5 = numbers.stream() .skip(5) .collect(Collectors.toList()); System.out.println("After 5: " + after5); // [6, 7, 8, 9, 10] // 8. peek() - Perform action without modifying stream (for debugging) List<Integer> peeked = numbers.stream() .peek(n -> System.out.println("Processing: " + n)) .filter(n -> n > 5) .peek(n -> System.out.println("After filter: " + n)) .collect(Collectors.toList()); // Chaining multiple operations List<Integer> result = numbers.stream() .filter(n -> n % 2 == 0) // Keep even .map(n -> n * 2) // Double them .sorted() // Sort .limit(3) // Take first 3 .collect(Collectors.toList()); System.out.println("\nChained result: " + result); // [4, 8, 12] // Remember: These operations are LAZY! // They don't execute until a terminal operation is called! }}Terminal Operations
These operations produce a result or side-effect and close the stream. Once you call a terminal operation, the stream can't be used again!
import java.util.*;import java.util.stream.*;public class TerminalOperations { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 1. forEach() - Perform action on each element System.out.print("Numbers: "); numbers.stream().forEach(n -> System.out.print(n + " ")); System.out.println(); // 2. collect() - Collect stream into collection List<Integer> list = numbers.stream() .filter(n -> n > 5) .collect(Collectors.toList()); System.out.println("List: " + list); Set<Integer> set = numbers.stream() .collect(Collectors.toSet()); System.out.println("Set: " + set); // 3. count() - Count elements long count = numbers.stream() .filter(n -> n % 2 == 0) .count(); System.out.println("Even count: " + count); // 5 // 4. anyMatch() - Check if ANY element matches boolean hasEven = numbers.stream() .anyMatch(n -> n % 2 == 0); System.out.println("Has even? " + hasEven); // true // 5. allMatch() - Check if ALL elements match boolean allPositive = numbers.stream() .allMatch(n -> n > 0); System.out.println("All positive? " + allPositive); // true // 6. noneMatch() - Check if NO elements match boolean noNegative = numbers.stream() .noneMatch(n -> n < 0); System.out.println("No negative? " + noNegative); // true // 7. findFirst() - Get first element Optional<Integer> first = numbers.stream() .filter(n -> n > 5) .findFirst(); System.out.println("First > 5: " + first.orElse(-1)); // 6 // 8. findAny() - Get any element (useful with parallel streams) Optional<Integer> any = numbers.stream() .filter(n -> n > 5) .findAny(); System.out.println("Any > 5: " + any.orElse(-1)); // 9. min() / max() - Find minimum/maximum Optional<Integer> min = numbers.stream().min(Integer::compareTo); Optional<Integer> max = numbers.stream().max(Integer::compareTo); System.out.println("Min: " + min.orElse(-1)); // 1 System.out.println("Max: " + max.orElse(-1)); // 10 // 10. reduce() - Combine elements into single result // Sum all numbers Optional<Integer> sum = numbers.stream() .reduce((a, b) -> a + b); System.out.println("Sum: " + sum.orElse(0)); // 55 // With initial value Integer product = numbers.stream() .reduce(1, (a, b) -> a * b); System.out.println("Product: " + product); // 11. toArray() - Convert to array Integer[] array = numbers.stream() .filter(n -> n % 2 == 0) .toArray(Integer[]::new); System.out.println("Array: " + Arrays.toString(array)); // Advanced collectors // Join strings List<String> words = Arrays.asList("Hello", "World", "Java", "Streams"); String joined = words.stream() .collect(Collectors.joining(", ")); System.out.println("\nJoined: " + joined); // Hello, World, Java, Streams // Group by length Map<Integer, List<String>> grouped = words.stream() .collect(Collectors.groupingBy(String::length)); System.out.println("Grouped by length: " + grouped); // Partition by condition Map<Boolean, List<Integer>> partitioned = numbers.stream() .collect(Collectors.partitioningBy(n -> n % 2 == 0)); System.out.println("Even: " + partitioned.get(true)); System.out.println("Odd: " + partitioned.get(false)); // Statistics IntSummaryStatistics stats = numbers.stream() .mapToInt(Integer::intValue) .summaryStatistics(); System.out.println("\nStats: " + stats); System.out.println("Average: " + stats.getAverage()); }}Common Stream Patterns
Let's see practical examples of how Streams are used:
import java.util.*;import java.util.stream.*;public class StreamPatterns { public static void main(String[] args) { // Sample data List<Person> people = Arrays.asList( new Person("Alice", 25, 50000), new Person("Bob", 30, 60000), new Person("Charlie", 35, 70000), new Person("David", 28, 55000), new Person("Eve", 32, 65000) ); // Pattern 1: Filter and collect List<Person> youngPeople = people.stream() .filter(p -> p.age < 30) .collect(Collectors.toList()); System.out.println("Young people: " + youngPeople.size()); // Pattern 2: Map to extract property List<String> names = people.stream() .map(Person::getName) .collect(Collectors.toList()); System.out.println("Names: " + names); // Pattern 3: Calculate sum/average double totalSalary = people.stream() .mapToDouble(Person::getSalary) .sum(); System.out.println("Total salary: $" + totalSalary); double avgSalary = people.stream() .mapToDouble(Person::getSalary) .average() .orElse(0); System.out.println("Average salary: $" + avgSalary); // Pattern 4: Sort by property List<Person> sortedByAge = people.stream() .sorted(Comparator.comparing(Person::getAge)) .collect(Collectors.toList()); System.out.println("\nSorted by age:"); sortedByAge.forEach(p -> System.out.println(p.name + ": " + p.age)); // Pattern 5: Find maximum/minimum Optional<Person> youngest = people.stream() .min(Comparator.comparing(Person::getAge)); youngest.ifPresent(p -> System.out.println("\nYoungest: " + p.name)); Optional<Person> highestPaid = people.stream() .max(Comparator.comparing(Person::getSalary)); highestPaid.ifPresent(p -> System.out.println("Highest paid: " + p.name)); // Pattern 6: Group by property Map<Integer, List<Person>> byAge = people.stream() .collect(Collectors.groupingBy(Person::getAge)); System.out.println("\nGrouped by age: " + byAge.keySet()); // Pattern 7: Partition by condition Map<Boolean, List<Person>> partitioned = people.stream() .collect(Collectors.partitioningBy(p -> p.salary > 60000)); System.out.println("High earners: " + partitioned.get(true).size()); System.out.println("Others: " + partitioned.get(false).size()); // Pattern 8: Convert to Map Map<String, Integer> nameToAge = people.stream() .collect(Collectors.toMap( Person::getName, Person::getAge )); System.out.println("\nName to age map: " + nameToAge); // Pattern 9: Check conditions boolean anyYoung = people.stream() .anyMatch(p -> p.age < 25); System.out.println("\nAny under 25? " + anyYoung); boolean allAdults = people.stream() .allMatch(p -> p.age >= 18); System.out.println("All adults? " + allAdults); // Pattern 10: Complex filtering and transformation List<String> richYoungNames = people.stream() .filter(p -> p.age < 35) .filter(p -> p.salary > 55000) .map(Person::getName) .sorted() .collect(Collectors.toList()); System.out.println("\nRich young people: " + richYoungNames); // Pattern 11: FlatMap for nested collections List<List<Integer>> nestedNumbers = Arrays.asList( Arrays.asList(1, 2, 3), Arrays.asList(4, 5, 6), Arrays.asList(7, 8, 9) ); List<Integer> allNumbers = nestedNumbers.stream() .flatMap(List::stream) .collect(Collectors.toList()); System.out.println("\nFlattened numbers: " + allNumbers); // Pattern 12: Distinct values List<Integer> ages = people.stream() .map(Person::getAge) .distinct() .sorted() .collect(Collectors.toList()); System.out.println("Unique ages: " + ages); }}class Person { String name; int age; double salary; Person(String name, int age, double salary) { this.name = name; this.age = age; this.salary = salary; } String getName() { return name; } int getAge() { return age; } double getSalary() { return salary; }}Parallel Streams
Parallel streams process data in parallel using multiple threads for better performance on large datasets:
import java.util.*;import java.util.stream.*;public class ParallelStreams { public static void main(String[] args) { // Create large dataset List<Integer> numbers = IntStream.rangeClosed(1, 1000000) .boxed() .collect(Collectors.toList()); // Sequential stream (single thread) long startSeq = System.currentTimeMillis(); long sumSeq = numbers.stream() .mapToLong(Integer::longValue) .sum(); long endSeq = System.currentTimeMillis(); System.out.println("Sequential sum: " + sumSeq); System.out.println("Sequential time: " + (endSeq - startSeq) + "ms"); // Parallel stream (multiple threads) long startPar = System.currentTimeMillis(); long sumPar = numbers.parallelStream() .mapToLong(Integer::longValue) .sum(); long endPar = System.currentTimeMillis(); System.out.println("\nParallel sum: " + sumPar); System.out.println("Parallel time: " + (endPar - startPar) + "ms"); // Converting to parallel stream long count1 = numbers.stream() .parallel() // Convert to parallel .filter(n -> n % 2 == 0) .count(); System.out.println("\nEven count (parallel): " + count1); // When to use parallel streams // ✓ GOOD: Large datasets (10,000+ elements) // ✓ GOOD: CPU-intensive operations // ✓ GOOD: Independent operations (no shared state) // ✗ BAD: Small datasets (overhead > benefit) // ✗ BAD: I/O operations (disk, network) // ✗ BAD: Operations that modify shared state // Example: Parallel filtering and mapping List<Integer> result = numbers.parallelStream() .filter(n -> n % 2 == 0) .map(n -> n * 2) .limit(100) .collect(Collectors.toList()); System.out.println("\nParallel result size: " + result.size()); // WARNING: Order may not be preserved in parallel streams! // Use forEachOrdered() to maintain order System.out.println("\nFirst 10 (unordered):"); numbers.parallelStream() .limit(10) .forEach(n -> System.out.print(n + " ")); // Order not guaranteed! System.out.println("\n\nFirst 10 (ordered):"); numbers.parallelStream() .limit(10) .forEachOrdered(n -> System.out.print(n + " ")); // Order maintained // Check parallelism level System.out.println("\n\nAvailable processors: " + Runtime.getRuntime().availableProcessors()); // Remember: Measure performance before using parallel streams! // They're not always faster - test with your actual data! }}Key Concepts
Lazy Evaluation
Intermediate operations don't execute until a terminal operation is called. This makes streams efficient!
No Storage
Streams don't store data - they carry data from a source through a pipeline of operations.
Functional Style
Streams use lambda expressions and functional programming style for clean, readable code.
One-Time Use
Once a stream is consumed (terminal operation called), it cannot be reused. Create a new stream if needed!
Best Practices
- ✓Use streams for complex data processing, not simple iterations
- ✓Don't modify the source collection while streaming
- ✓Use parallel streams only for large datasets (overhead for small ones)
- ✓Avoid stateful lambda expressions in stream operations
- ✓Prefer method references over lambda expressions when possible
- ✓Use appropriate collectors for better performance
Common Mistakes
✗ Reusing a stream after terminal operation
Why it's wrong: Streams can only be used once. After a terminal operation, create a new stream!
✗ Using streams for simple iterations
Why it's wrong: For simple loops, traditional for-each is often clearer and faster.
✗ Modifying source collection during streaming
Why it's wrong: This causes ConcurrentModificationException or unpredictable behavior!
✗ Overusing parallel streams
Why it's wrong: Parallel streams have overhead. Use only for large datasets or heavy computations.
Interview Tips
- 💡Explain that Streams are a Java 8 feature for functional-style data processing
- 💡Know the difference between intermediate and terminal operations
- 💡Understand lazy evaluation and how it improves performance
- 💡Be able to write common stream operations: filter, map, reduce, collect
- 💡Explain when to use parallel streams vs sequential streams
- 💡Know the common collectors: toList, toSet, toMap, groupingBy, joining