Java Streams API

Java Streams API — A Modern Approach to Data Processing

The Streams API, introduced in Java 8, offers a functional-style way to process sequences of elements (such as collections, arrays, or I/O channels) using a pipeline of operations like map, filter, and reduce.

  • Streams don’t store data — they operate on data.
  • They are non-destructive, lazy, and fluent.


 🔹 Key Characteristics of Streams

Feature

Description

Functional-style

Supports operations like map, filter, and reduce

Pipeline-based

Chainable intermediate and terminal operations

Immutable source

Original data (collection/array) is not modified

Lazy evaluation

Intermediate operations are executed only when a terminal operation runs

Not a data structure

Acts as a view or abstraction over the source, not a container itself



🔹 Stream Pipeline Structure

dataSource.stream()

          .intermediateOperation1()

          .intermediateOperation2()

          ...

          .terminalOperation();


  • Source: Collection, array, or generator (e.g., List, Set, Stream.of())
  • Intermediate operations: Return a new stream (lazy)
  • Terminal operations: Produce a result or side effect (eager)
 

Source (Stream Creation) :
Stream.of("A", "B", "C");                  // From varargs
Arrays.stream(new int[]{1, 2, 3});         // From array
List.of("X", "Y").stream();                // From collection
IntStream.range(1, 5);                     // 1 to 4
IntStream.rangeClosed(1, 5);               // 1 to 5


Intermediate Operations (Return a Stream) :

Method

Purpose

filter(Predicate)

Filters elements based on condition

map(Function)

Transforms each element

flatMap(Function)

Flattens nested structures

distinct()

Removes duplicates

sorted()

Natural sort

sorted(Comparator)

Custom sorting logic

limit(n)

Truncates to first n elements

skip(n)

Skips first n elements

peek(Consumer)

Debug/logging in the pipeline


Terminal Operations (Trigger Execution) :

Method

Purpose

forEach(Consumer)

Performs action for each element

toArray()

Collects elements into array

reduce(...)

Aggregates (sum, min, etc.)

collect(...)

Collects into collection/result

min(Comparator)

Finds minimum element

max(Comparator)

Finds maximum element

count()

Counts elements

anyMatch(Predicate)

Checks if any element matches condition

allMatch(Predicate)

Checks if all match condition

noneMatch(Predicate)

Checks if none match condition

findFirst()

Returns first element

findAny()

Returns any element (useful with parallel)


Common Collectors (Used with collect()) :

Collector

Purpose

Collectors.toList()

Collects to a List

Collectors.toSet()

Collects to a Set

Collectors.toMap(k,v)

Collects to a Map

Collectors.joining(",")

Concatenates strings

Collectors.groupingBy()

Groups by field/key

Collectors.partitioningBy()

Splits into true/false groups

Collectors.counting()

Counts elements

Collectors.summarizingInt()

Stats like min/max/avg/sum/count


Example :
List<Employee> itEmployees = employees.stream()
    .filter(e -> "IT".equals(e.getDepartment()))
    .collect(Collectors.toList());
System.out.println("IT Employees: " + itEmployees);



🔹 Parallel Streams for Performance

Enable multi-core parallelism with .parallelStream():

List<Integer> bigList = IntStream.range(1, 1_000_000)
                                 .boxed()
                                 .collect(Collectors.toList());

long evenCount = bigList.parallelStream()
                        .filter(i -> i % 2 == 0)
                        .count();

System.out.println(evenCount);  // Output: 499999



🔹 Lambda Expressions with Streams

Lambda expressions offer concise functional logic without boilerplate:

// Traditional stream.filter(new Predicate<String>() { public boolean test(String s) { return s.length() > 3; } }); // Lambda stream.filter(s -> s.length() > 3);



🔹 Lambda vs Method Reference: When to Use Which?

1. Lambda Expression E.g. (e) -> e.getSalary()
    A lambda expression is an inline anonymous function used to define custom behavior for functional interfaces. It is flexible and allows additional logic, conditions, or transformations.

Syntax: (parameters) -> expression

Example: Filter employees with salary > 50,000 and print their uppercase names

list.stream() .filter(e -> e.getSalary() > 50000) // custom logic .map(e -> e.getName().toUpperCase()) // transformation .forEach(name -> System.out.println(name)); // side effect

Use when:

  • You need conditions, transformations, or side-effects
  • The logic can’t be replaced by a single existing method
2. Method Reference E.g. Employee::getSalary
    A method reference is a concise way to refer to an existing method by its name using the :: operator, often replacing simple lambdas.

Syntax: ClassName::methodName or object::methodName

Example: Get employee names and print them

list.stream() .map(Employee::getName) // method reference to getName() .forEach(System.out::println); // method reference to println()

Use when:

  • You're simply calling an existing method
  • It enhances readability and avoids unnecessary lambda syntax

When to use Lambda Expressions vs Method References?

Prefer Lambda When...

Prefer Method Reference When...

You need logic like e -> e.getX() + 10

You just call e.getX() or System.out::println

You combine or transform values

You're passing method as-is


Which is better for field comparison: lambda expressions or method references?
The method reference version is shorter and clearer when you're just comparing fields.
// Lambda list.stream() .sorted((a, b) -> a.getName().compareToIgnoreCase(b.getName())); // Method Reference (if Comparable implemented properly) list.stream() .sorted(Comparator.comparing(Employee::getName));


🔹 Stream vs Collection

Feature

Stream

Collection

Iteration

Internal (managed by Stream)

External (for-loop, iterator)

Reusability

No (one-time use)

Yes

Evaluation

Lazy

Eager

Parallelism

Supported (parallelStream())

Not supported directly

Data Modification

Not allowed

Allowed




🔹 When to use Streams

Use Streams when:
  • You want to transform or filter data cleanly
  • You need to aggregate data (sum, groupBy, etc.)
  • You prefer declarative, readable code

Avoid Streams when:

  • You need index-based access

You require side-effects or complex mutable state



🔹 A Full Example
import java.util.*;
import java.util.stream.*; import java.nio.file.*; import java.io.IOException; class Employee { private int id; private String name; private int age; private String department; private double salary; public Employee(int id, String name, int age, String department, double salary) { this.id = id; this.name = name; this.age = age; this.department = department; this.salary = salary; } public int getId() { return id; } public String getName() { return name; } public int getAge() { return age; } public String getDepartment() { return department; } public double getSalary() { return salary; } @Override public String toString() { return name + " (" + age + " yrs, " + department + ", ₹" + salary + ")"; } } public class StreamUseCases { public static void main(String[] args) throws IOException { List<Employee> employees = Arrays.asList( new Employee(1, "Alice", 30, "IT", 50000), new Employee(2, "Bob", 28, "Finance", 45000), new Employee(3, "Charlie", 32, "IT", 60000), new Employee(4, "David", 28, "HR", 45000), new Employee(5, "Eve", 35, "IT", 70000), new Employee(6, "Frank", 24, "Finance", 40000), new Employee(7, "Grace", 29, "HR", 50000) ); // Filter by Department List<Employee> devs = employees.stream() .filter(e -> "IT".equals(e.getDepartment())) .collect(Collectors.toList()); System.out.println("IT Employees: " + devs); // Output: [Alice (30 yrs, IT, ₹50000.0), Charlie (32 yrs, IT, ₹60000.0), Eve (35 yrs, IT, ₹70000.0)] // Average Salary double avgSalary = employees.stream() .mapToDouble(Employee::getSalary) .average() .orElse(0.0); System.out.println("Average Salary: ₹" + avgSalary); // Output: ₹51428.57142857143 // Grouping by Department Map<String, List<Employee>> deptEmployees = employees.stream() .collect(Collectors.groupingBy(Employee::getDepartment)); System.out.println("Grouped by Department: " + deptEmployees); // Output: {HR=[David (28 yrs, HR, ₹45000.0), Grace (29 yrs, HR, ₹50000.0)], // IT=[Alice (30 yrs, IT, ₹50000.0), Charlie (32 yrs, IT, ₹60000.0), Eve (35 yrs, IT, ₹70000.0)], // Finance=[Bob (28 yrs, Finance, ₹45000.0), Frank (24 yrs, Finance, ₹40000.0)]} // Convert List to Map (ID -> Name) Map<Integer, String> idToName = employees.stream() .collect(Collectors.toMap(Employee::getId, Employee::getName)); System.out.println("ID to Name Map: " + idToName); // Output: {1=Alice, 2=Bob, 3=Charlie, 4=David, 5=Eve, 6=Frank, 7=Grace} // Top 3 Salaries List<Employee> topSalaries = employees.stream() .sorted(Comparator.comparing(Employee::getSalary).reversed()) .limit(3) .collect(Collectors.toList()); System.out.println("Top 3 Salaries: " + topSalaries); // Output: [Eve (35 yrs, IT, ₹70000.0), Charlie (32 yrs, IT, ₹60000.0), Alice (30 yrs, IT, ₹50000.0)] // Count employees older than 30 long countOlderThan30 = employees.stream() .filter(e -> e.getAge() > 30) .count(); System.out.println("Employees older than 30: " + countOlderThan30); // Output: 2 // Partition by age >= 30 Map<Boolean, List<Employee>> partitioned = employees.stream() .collect(Collectors.partitioningBy(e -> e.getAge() >= 30)); System.out.println("Partitioned by age >= 30: " + partitioned); // Output: {false=[Bob (28 yrs, Finance, ₹45000.0), David (28 yrs, HR, ₹45000.0),
        //          Frank (24 yrs, Finance, ₹40000.0), Grace (29 yrs, HR, ₹50000.0)], // true=[Alice (30 yrs, IT, ₹50000.0), Charlie (32 yrs, IT, ₹60000.0),
        //          Eve (35 yrs, IT, ₹70000.0)]} // Join Names String namesJoined = employees.stream() .map(Employee::getName) .collect(Collectors.joining(", ")); System.out.println("All Names: " + namesJoined); // Output: Alice, Bob, Charlie, David, Eve, Frank, Grace // Reduce - Total Salary double totalSalary = employees.stream() .map(Employee::getSalary) .reduce(0.0, Double::sum); System.out.println("Total Salary: ₹" + totalSalary); // Output: ₹360000.0 // Distinct Departments List<String> uniqueDepartments = employees.stream() .map(Employee::getDepartment) .distinct() .collect(Collectors.toList()); System.out.println("Departments: " + uniqueDepartments); // Output: [IT, Finance, HR] // Sorted by Name List<Employee> sortedByName = employees.stream() .sorted(Comparator.comparing(Employee::getName)) .collect(Collectors.toList()); System.out.println("Sorted by Name: " + sortedByName); // Output: [Alice, Bob, Charlie, David, Eve, Frank, Grace] (in alphabetical order) // Find Any from IT Optional<Employee> anyIT = employees.stream() .filter(e -> "IT".equals(e.getDepartment())) .findAny(); anyIT.ifPresent(e -> System.out.println("Any IT Employee: " + e)); // Output: Any IT Employee: Alice (or any other IT employee) // Find First HR Optional<Employee> firstHR = employees.stream() .filter(e -> "HR".equals(e.getDepartment())) .findFirst(); firstHR.ifPresent(e -> System.out.println("First HR: " + e)); // Output: David (28 yrs, HR, ₹45000.0) // Peek (debug) List<Employee> debugPeek = employees.stream() .peek(e -> System.out.println("Processing: " + e.getName())) .filter(e -> e.getSalary() > 45000) .collect(Collectors.toList()); System.out.println("High Earners: " + debugPeek); // Output: Processing logs printed, and final list of employees with salary > 45000 // FlatMap: Nested Lists -> Single List List<List<String>> nested = List.of( List.of("A", "B"), List.of("C", "D") ); List<String> flat = nested.stream() .flatMap(Collection::stream) .collect(Collectors.toList()); System.out.println("Flattened List: " + flat); // Output: [A, B, C, D] // Word Frequency Count String text = "apple banana apple orange banana apple"; Map<String, Long> wordCount = Arrays.stream(text.split(" ")) .collect(Collectors.groupingBy(word -> word, Collectors.counting())); System.out.println("Word Count: " + wordCount); // Output: {apple=3, banana=2, orange=1} // Reading and Processing File Lines (Assume file exists with proper content) List<String> lines = Files.lines(Paths.get("data.csv")) .filter(line -> !line.isBlank()) .map(String::trim) .collect(Collectors.toList()); System.out.println("File Lines: " + lines); // Output: Contents of file without blank lines and trimmed } }



🔹 Additional Stream Concepts

1. Primitive Streams

Definition: Specialized stream types for primitives (int, long, double) to improve performance by avoiding boxing.

Explanation: Use IntStream, LongStream, and DoubleStream instead of Stream<Integer>, etc., when working with numeric data for better memory and CPU efficiency.

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

2. Stream Builder

Definition: Use Stream.builder() to programmatically build a stream when you don’t have the data in advance.

Explanation: Ideal when elements are added conditionally or one by one, often in dynamic scenarios.

Stream<String> stream = Stream.<String>builder() .add("Apple").add("Banana").add("Cherry").build(); stream.forEach(System.out::println);

3. Infinite Streams

Definition: Streams that can produce endless elements using generate() or iterate().

Explanation: Useful for lazy data production, but must be limited to avoid infinite loops.

// generate random numbers Stream.generate(Math::random) .limit(3) .forEach(System.out::println); // generate odd numbers starting from 1 Stream.iterate(1, n -> n + 2) .limit(3) .forEach(System.out::println); // Output: 1 3 5

4. Short-Circuiting Operations

Definition: Operations that stop processing once a condition is met.

Explanation: These methods improve performance by not processing the entire stream if not needed.

boolean anyHigh = list.stream() .anyMatch(e -> e.getSalary() > 100000); // true/false // Other examples: allMatch(), noneMatch(), findFirst(), limit(), skip()

5. Closing I/O Streams

Definition: Streams from file or network resources must be closed to free system resources.

Explanation: Files.lines(), BufferedReader.lines() and others implement AutoCloseable, so use try-with-resources.

try (Stream<String> lines = Files.lines(Path.of("file.txt"))) { lines.forEach(System.out::println); }

6. Stream Concatenation

Definition: Combine two streams into a single stream using Stream.concat().

Explanation: Allows merging multiple sources of data into a single stream for unified processing.

Stream<String> s1 = Stream.of("A", "B"); Stream<String> s2 = Stream.of("C", "D"); Stream.concat(s1, s2).forEach(System.out::println); // Output: A B C D

7. Custom Collector

Definition: Create your own Collector for customized collection logic.

Explanation: Use Collector.of() when you need a custom way to collect or format stream output beyond standard collectors.

Collector<String, StringBuilder, String> customCollector = Collector.of( StringBuilder::new, // supplier StringBuilder::append, // accumulator StringBuilder::append, // combiner StringBuilder::toString // finisher ); String result = Stream.of("A", "B", "C").collect(customCollector); // "ABC"

8. Optional + Stream

Definition: Stream methods like findFirst() or findAny() return Optional to safely handle absence of values.

Explanation: Prevents NullPointerException by allowing safe access and chaining.

Optional<Employee> topEarner = list.stream() .filter(e -> e.getSalary() > 100000) .findFirst(); topEarner.ifPresent(System.out::println);

9. Advanced Collectors

Definition: Built-in collectors like groupingBy, partitioningBy, mapping, reducing allow powerful aggregation logic.

Explanation: Use them to group, summarize, or transform data during collection.

// Group employees by department Map<String, List<Employee>> grouped = list.stream().collect(Collectors.groupingBy(Employee::getDepartment)); // Count employees per department Map<String, Long> countPerDept = list.stream().collect(Collectors.groupingBy(Employee::getDepartment, Collectors.counting()));

Other powerful collectors:

  • mapping(Function, Collector)

  • reducing(...)

  • collectingAndThen(...)

  • teeing(...) (Java 12+)

10. Java 9+ Stream Enhancements

Definition: Java 9+ introduced new stream methods like takeWhile(), dropWhile(), and enhanced iterate().

Explanation: Enable more expressive, declarative control over stream slicing and looping.

// take elements while < 4 Stream.of(1, 2, 3, 4, 5) .takeWhile(n -> n < 4) .forEach(System.out::println); // Output: 1 2 3 // iterate with condition Stream.iterate(1, n -> n < 10, n -> n + 3) .forEach(System.out::println); // Output: 1 4 7

11. FlatMap vs Map

Definition: map() transforms each element into a single value, while flatMap() transforms and flattens nested streams or collections.

Explanation: Use flatMap() when each element may map to multiple values, and you want a flattened result.

// List of lists List<List<String>> nested = List.of(List.of("A", "B"), List.of("C", "D")); // Flatten into a single stream List<String> flat = nested.stream() .flatMap(List::stream) .collect(Collectors.toList()); // Output: [A, B, C, D]

12. Peek (for Debugging)

Definition: peek() is an intermediate operation used for debugging or inspecting elements as they flow through the pipeline.

Explanation: Do not use peek() for business logic or side effects. It’s only for observing.

list.stream() .filter(e -> e.getSalary() > 50000) .peek(e -> System.out.println("Filtered: " + e.getName())) .map(Employee::getName) .forEach(System.out::println);

13. toUnmodifiableList/Set/Map (Java 10+)

Definition: Collectors that produce read-only (immutable) collections.

Explanation: Helps create defensive, immutable collections from streams.

List<String> names = list.stream() .map(Employee::getName) .collect(Collectors.toUnmodifiableList());

14. Filtering with Collectors.filtering() (Java 9)

Definition: Use Collectors.filtering() for filtering inside grouped/partitioned collectors.

Explanation: Allows filtering within collectors, not just in the stream.

Map<String, List<Employee>> highEarnersPerDept = list.stream() .collect(Collectors.groupingBy( Employee::getDepartment, Collectors.filtering(e -> e.getSalary() > 100000, Collectors.toList()) ));

15. Joining with prefix, suffix, and delimiter

Definition: Collectors.joining() can include custom delimiter, prefix, and suffix.

Explanation: Useful for building JSON strings, HTML, CSV, etc.

String csv = Stream.of("Alice", "Bob", "Charlie") .collect(Collectors.joining(", ", "[", "]")); // Output: [Alice, Bob, Charlie]

16. Lazy Evaluation and Optimization

Definition: Streams are lazy – intermediate operations are only evaluated when a terminal operation is invoked.

Explanation: Java optimizes execution: filters early, avoids unnecessary work.

Stream<String> stream = Stream.of("one", "two", "three") .filter(s -> { System.out.println("Filtering: " + s); return s.length() > 3; }); System.out.println("Nothing happens yet!"); stream.forEach(System.out::println); // Triggers actual processing

17. Stream vs ParallelStream Performance

Definition: parallelStream() splits work across multiple threads using ForkJoinPool.

Explanation: It can speed up large CPU-bound tasks, but for small tasks it may slow down due to thread management overhead.

long count = list.parallelStream() .filter(e -> e.getSalary() > 100000) .count();

Avoid if:

  • Your stream has side effects

  • The source is unordered or I/O-bound

  • Elements need to be processed sequentially

18. Avoiding Stateful Lambda Pitfalls

Definition: Stateful lambdas depend on or modify external mutable state — which breaks stream purity and can cause bugs in parallel streams.

Bad Example (Don't do this):

List<Integer> result = new ArrayList<>(); list.stream().map(x -> { result.add(x); // Unsafe side effect! return x * 2; }).collect(Collectors.toList());

Always keep lambdas stateless and side-effect-free in streams.

19. Sorting with Comparator.comparing()

Definition: Use Comparator.comparing() to sort by object fields.

Example:

list.stream() .sorted(Comparator.comparing(Employee::getSalary).reversed()) .forEach(System.out::println);

20. Stream Terminal Operations Return Types


Method

Return Type

collect()

Depends (List, Set)

forEach()

void (side effect)

count()

long

reduce()

Optional<T> / T

findFirst()

Optional<T>

allMatch()

boolean



21. New Stream Features (Java 9–21)

Java 9

  • takeWhile() – Take elements while condition is true

    Stream.of(1, 2, 3, 4).takeWhile(n -> n < 4).forEach(System.out::println); // 1 2 3
  • dropWhile() – Skip while condition is true

    Stream.of(1, 2, 3, 4).dropWhile(n -> n < 3).forEach(System.out::println); // 3 4
  • iterate() with condition

    Stream.iterate(1, n -> n < 10, n -> n + 3).forEach(System.out::println); // 1 4 7

Java 10

  • Collectors.toUnmodifiableList() – Create immutable list

    List<String> list = Stream.of("A", "B").collect(Collectors.toUnmodifiableList());

Java 12

  • Collectors.teeing() – Combine two collectors

    var stats = Stream.of(1, 2, 3).collect(Collectors.teeing(     Collectors.minBy(Integer::compare),     Collectors.maxBy(Integer::compare),     (min, max) -> "Min: " + min.get() + ", Max: " + max.get()     ));

Java 16

  • Pattern matching with instanceof

    Stream.of("Java", 123).filter(o -> o instanceof String s && s.length() > 2)     .forEach(System.out::println); // Java

Java 21

  • SequencedCollection – Ordered access

    SequencedCollection<String> sc = new LinkedHashSet<>(List.of("A", "B"));     System.out.println(sc.getFirst()); // A
  • mapMulti() – Flatten with control

    Stream.of("Hi").mapMulti((s, c) -> s.chars().forEach(ch -> c.accept((char) ch))) .forEach(System.out::print); // Hi