Advanced Java August 19 ,2025

Java Stream API – 

1. Introduction to Java Stream API

The Stream API was introduced in Java 8 to process collections of data in a functional programming style.
It allows performing bulk operations (like filtering, mapping, reducing, etc.) on data with ease.

Key Points:

  • A Stream represents a sequence of elements.
  • It does not store data; it provides a pipeline to process data.
  • Stream operations are functional (use lambda expressions, functional interfaces).
  • Stream is different from java.io.Stream (input/output streams).

2. Why Use Streams?

Before Java 8, to filter or transform data, developers had to use loops and iterators, making code verbose and less readable.

With Stream API:

  • Less boilerplate code
  • Readability (code looks closer to SQL queries)
  • Parallel processing (multi-core CPUs utilized)
  • Declarative style (focus on what to do, not how to do)

3. Stream API Workflow

A Stream operation generally has 3 stages:

  1. Source – Where data comes from
    Examples: Collection, Arrays, I/O channels, or Stream.of()
  2. Intermediate Operations – Transform the stream (return a new stream)
    Examples: filter(), map(), sorted()
  3. Terminal Operations – Produce a result (end the stream)
    Examples: collect(), forEach(), reduce()

You’re right to check that — in my earlier draft I explained what Streams are, their operations, and examples, but I did not explicitly include a “How to Create a Java Stream?” section.

That’s an important part, because before performing operations we must know how to instantiate a Stream. Let me add that section for you in a detailed theory + practical style.

4. How to Create a Java Stream?

Streams in Java can be created in multiple ways depending on the data source. Here are the main approaches:

1. From Collections

Every collection class (List, Set, etc.) in Java has a built-in method stream() that creates a sequential stream, and parallelStream() that creates a parallel stream.

Example:

import java.util.*;
import java.util.stream.*;

public class CollectionStreamExample {
    public static void main(String[] args) {
        List names = Arrays.asList("Rahul", "Amit", "Neha", "Priya");

        // Sequential stream
        Stream sequentialStream = names.stream();
        sequentialStream.forEach(System.out::println);

        // Parallel stream
        Stream parallelStream = names.parallelStream();
        parallelStream.forEach(System.out::println);
    }
}

2. Using Arrays

The Arrays.stream() method or Stream.of() can be used to create streams from arrays.

Example:

import java.util.stream.*;

public class ArrayStreamExample {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3, 4, 5};

        // Stream from array
        IntStream intStream = Arrays.stream(numbers);
        intStream.forEach(System.out::println);

        // Another way
        Stream numberStream = Stream.of(10, 20, 30, 40);
        numberStream.forEach(System.out::println);
    }
}

3. Using Stream.of()

Directly create a stream from given values.

Example:

import java.util.stream.*;

public class StreamOfExample {
    public static void main(String[] args) {
        Stream stream = Stream.of("Java", "Python", "C++", "Go");
        stream.forEach(System.out::println);
    }
}

4. Using Stream.generate()

Used to create infinite streams by supplying a Supplier function.

Example:

import java.util.stream.*;

public class StreamGenerateExample {
    public static void main(String[] args) {
        // Generates random numbers
        Stream randomNumbers = Stream.generate(Math::random);

        // Limiting to 5 elements
        randomNumbers.limit(5).forEach(System.out::println);
    }
}

5. Using Stream.iterate()

Also creates infinite streams but by applying a function iteratively.

Example:

import java.util.stream.*;

public class StreamIterateExample {
    public static void main(String[] args) {
        // Start with 1, keep adding 2
        Stream oddNumbers = Stream.iterate(1, n -> n + 2);

        // Limiting to 5 elements
        oddNumbers.limit(5).forEach(System.out::println);
    }
}

6. Using Files.lines() (Stream from a File)

The Files.lines() method can create a stream of lines from a text file.

Example:

import java.nio.file.*;
import java.io.IOException;
import java.util.stream.*;

public class FileStreamExample {
    public static void main(String[] args) throws IOException {
        Stream lines = Files.lines(Paths.get("example.txt"));

        lines.forEach(System.out::println);
        lines.close();
    }
}

 

5. Intermediate Operations (Transformations)

Intermediate operations are operations performed on a Stream that transform it into another stream.
They are lazy, meaning they don’t execute immediately. They only get executed when a terminal operation (like forEach, collect, reduce) is called.
Multiple intermediate operations can be chained together, forming a pipeline.

Intermediate operations return a new stream, allowing method chaining. They are lazy, meaning they don’t run immediately — execution happens only when a terminal operation (like forEach(), collect()) is called.

(a) filter()

  • Used to select elements that match a given condition.
  • Returns a stream containing only those elements that satisfy the predicate.
  • Often used for conditional processing.

Key Point: Doesn’t modify elements, only reduces the stream based on condition.

List nums = Arrays.asList(10, 20, 30, 40);
nums.stream()
    .filter(n -> n > 20)  
    .forEach(System.out::println);  // Output: 30, 40

(b) map()

  • Used to transform each element into another form.
  • Accepts a function and applies it to every element.
  • Most commonly used for data conversion (e.g., lowercase → uppercase, number → square).

Key Point: The number of elements usually remains the same, but their type or value changes.

List names = Arrays.asList("john", "doe");
names.stream()
     .map(String::toUpperCase)  
     .forEach(System.out::println);  // Output: JOHN, DOE

(c) flatMap()

  • Used when elements themselves are collections (or streams).
  • Converts each sub-collection into a stream, then flattens them into a single stream.
  • Helps avoid nested collections.

Key Point: map() → returns stream of collections, flatMap() → merges them into one stream.

List> list = Arrays.asList(
    Arrays.asList("A", "B"),
    Arrays.asList("C", "D")
);

list.stream()
    .flatMap(Collection::stream)  
    .forEach(System.out::println);  // Output: A, B, C, D

(d) distinct()

  • Removes duplicate elements.
  • Uses equals() method for comparison.
  • Ensures uniqueness in the stream.
Stream.of(1, 2, 2, 3, 3, 4)
      .distinct()
      .forEach(System.out::println);  // Output: 1, 2, 3, 4

(e) sorted()

  • Sorts stream elements.
  • By default, sorts in natural order.
  • Can also accept a custom Comparator.
Stream.of(5, 1, 3, 2)
      .sorted()
      .forEach(System.out::println);  // Output: 1, 2, 3, 5

Custom Comparator:

Stream.of("apple", "banana", "cherry")
      .sorted(Comparator.reverseOrder())
      .forEach(System.out::println);  // Output: cherry, banana, apple

(f) limit() & skip()

  • limit(n): restricts stream to first n elements.
  • skip(n): ignores the first n elements and processes the rest.
  • Useful in pagination or sampling.
Stream.of(10, 20, 30, 40, 50)
      .limit(3)
      .forEach(System.out::println);  // Output: 10, 20, 30
Stream.of(10, 20, 30, 40, 50)
      .skip(2)
      .forEach(System.out::println);  // Output: 30, 40, 50

6. Terminal Operations (Results)

Terminal operations are the final operations on a stream. They produce a result (non-stream value like int, List, Optional, etc.) or a side-effect (like printing).
After a terminal operation, the stream is consumed and cannot be reused.

(a) forEach()

  • Performs an action for each element of the stream.
  • Usually used for printing or applying side effects.
Stream.of("A", "B", "C")
      .forEach(System.out::println);  
// Output: A B C

(b) toArray()

  • Collects elements into an array.
String[] arr = Stream.of("X", "Y", "Z")
                     .toArray(String[]::new);
System.out.println(Arrays.toString(arr));  
// Output: [X, Y, Z]

(c) reduce()

  • Reduces elements to a single value (aggregation).
int sum = Stream.of(1, 2, 3, 4)
                .reduce(0, (a, b) -> a + b);
System.out.println(sum);  // 10

(d) collect()

  • Collects results into a collection or map.
  • Most powerful terminal operation.
List list = Stream.of("apple", "banana", "apple")
                          .collect(Collectors.toList());
System.out.println(list);  
// Output: [apple, banana, apple]
Set set = Stream.of("apple", "banana", "apple")
                        .collect(Collectors.toSet());
System.out.println(set);  
// Output: [apple, banana]

(e) min() and max()

  • Finds the minimum or maximum element based on a comparator.
int min = Stream.of(5, 9, 1, 7)
                .min(Integer::compare)
                .get();
System.out.println(min);  // 1
int max = Stream.of(5, 9, 1, 7)
                .max(Integer::compare)
                .get();
System.out.println(max);  // 9

(f) count()

  • Returns the number of elements in the stream.
long count = Stream.of(10, 20, 30, 40)
                   .count();
System.out.println(count);  // 4

(g) anyMatch(), allMatch(), noneMatch()

  • Used for checking conditions.
boolean any = Stream.of(1, 2, 3, 4)
                    .anyMatch(n -> n > 3);
System.out.println(any);  // true

boolean all = Stream.of(1, 2, 3, 4)
                    .allMatch(n -> n > 0);
System.out.println(all);  // true

boolean none = Stream.of(1, 2, 3, 4)
                     .noneMatch(n -> n < 0);
System.out.println(none);  // true

(h) findFirst() and findAny()

  • Find first or any element in the stream (returns Optional).
Optional first = Stream.of(10, 20, 30)
                                .findFirst();
System.out.println(first.get());  // 10

Optional any = Stream.of(10, 20, 30)
                              .findAny();
System.out.println(any.get());  // could be 10, 20, or 30

 

7. Collectors Utility Class

The Collectors class (in java.util.stream.Collectors) is a utility class that provides a variety of static methods to perform reduction operations on stream elements and return them in a desired collection or result type. It is part of the java.util.stream package and works hand-in-hand with the collect() terminal operation.

Collectors are essential when you want to transform a stream into a data structure (like a List, Set, or Map), aggregate values (like counting or averaging), or perform grouping and partitioning.

7.1. Key Features of Collectors

  1. Immutable Results – The result of a collector operation is often an immutable collection or final value.
  2. Flexible Data Reduction – Provides wide support for reduction into collections, strings, maps, numeric aggregates, etc.
  3. Custom Collectors – Developers can build custom collectors using Collector.of(...).
  4. Parallel Friendly – Collectors are designed to work in both sequential and parallel stream pipelines.

7.2. Commonly Used Collectors Methods

a) toList(), toSet(), toMap()

  • Collect elements into a List, Set, or Map.
  • toList() → Collects into a List (e.g., ArrayList).
  • toSet() → Collects into a Set (eliminates duplicates).
  • toMap(keyMapper, valueMapper) → Collects into a Map with custom key/value extraction functions.

b) joining()

  • Concatenates the stream of CharSequence elements into a single String.
  • Overloaded methods allow specifying a delimiter, prefix, and suffix.

Example:

List names = Arrays.asList("John", "Jane", "Jack");
String result = names.stream()
                     .collect(Collectors.joining(", ", "[", "]"));
System.out.println(result); 
// Output: [John, Jane, Jack]

c) groupingBy()

  • Groups stream elements based on a classification function and returns a Map> by default.
  • Overloads allow grouping into different collection types and applying downstream collectors (e.g., counting, summing).

Example:

Map> grouped = 
    names.stream().collect(Collectors.groupingBy(name -> name.charAt(0)));

d) partitioningBy()

  • Partitions stream elements into two groups (true/false) based on a given predicate.
  • Returns a Map>.
  • Useful for binary classification.

Example:

Map> partitioned = 
    names.stream().collect(Collectors.partitioningBy(name -> name.length() > 3));

7.3. Advanced Collectors

  1. counting() → Counts elements.
  2. summingInt(), summingDouble(), summingLong() → Summation of numerical values.
  3. averagingInt(), averagingDouble(), averagingLong() → Computes average.
  4. maxBy(comparator), minBy(comparator) → Finds max/min element.
  5. collectingAndThen(collector, finisher) → Wraps another collector and applies a finishing function.
  6. mapping(mapper, downstream) → Applies a mapping function before reduction.

7.4. Practical Importance

  • Data Transformation → Transform streams into collections/maps for further processing.
  • Aggregation → Easily calculate sum, average, count, min, and max.
  • Grouping and Partitioning → Simplifies classification and categorization of data.
  • Readable and Declarative → Makes code concise compared to imperative looping.

 

8. Parallel Streams

The Stream API not only provides sequential stream processing but also supports parallel execution to take advantage of multi-core processors. A parallel stream divides the given data source into multiple chunks, processes them in parallel across different threads, and then combines the results.

Key Concepts of Parallel Streams:

  1. Creation of Parallel Stream

    • You can obtain a parallel stream in two ways:
      • By calling .parallelStream() on a Collection.
      • By calling .parallel() on an existing sequential stream.
    List numbers = Arrays.asList(1,2,3,4,5,6,7,8);
    numbers.parallelStream().forEach(System.out::println);
    

    Here, elements may be printed in any order because execution happens concurrently.

  2. Work Splitting (Fork/Join Framework)
    • Internally, Java uses the Fork/Join framework (introduced in Java 7) to split the data source into sub-tasks.
    • Each sub-task is processed by a different worker thread in the common ForkJoinPool.
    • Once all sub-tasks finish execution, results are merged.
  3. Performance Considerations
    • Large Datasets → Parallel streams perform better when dealing with large collections.
    • Small Datasets → Parallel overhead (splitting + thread management) can make it slower than sequential streams.
    • CPU-Intensive Operations → Best suited when tasks are computationally heavy.
    • I/O-Bound Operations → Not beneficial, as threads may block.
  4. Ordering Issues

    • Parallel streams do not guarantee order unless you explicitly use forEachOrdered().
    numbers.parallelStream()
           .forEachOrdered(System.out::println); // Maintains order
    
  5. Thread-Safety Concerns
    • Since execution happens concurrently, shared mutable state must be avoided.
    • For example, updating a global ArrayList inside a parallel stream can cause race conditions.
    • Always use collectors or thread-safe data structures (ConcurrentHashMap, CopyOnWriteArrayList) for parallel operations.
  6. When to Use Parallel Streams
    ✅ Suitable for:

    • Large data collections
    • CPU-bound operations
    • Stateless and non-blocking tasks

    ❌ Avoid in:

    • Small collections (overhead > benefit)
    • I/O heavy tasks (blocking threads)
    • Tasks requiring strict ordering
  7. Example: Summing with Parallel Streams
List list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);

int sum = list.parallelStream()
              .mapToInt(Integer::intValue)
              .sum();

System.out.println("Sum: " + sum); // Output: 55

 

9. Practical Use Case – Employee Filtering

class Employee {
    String name;
    double salary;

    Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }
}

public class EmployeeStream {
    public static void main(String[] args) {
        List employees = Arrays.asList(
            new Employee("John", 50000),
            new Employee("Jane", 60000),
            new Employee("Jack", 40000)
        );

        // Find employees with salary > 45,000
        employees.stream()
                 .filter(e -> e.salary > 45000)
                 .map(e -> e.name)
                 .forEach(System.out::println);  // John, Jane
    }
}

10. Key Differences – Stream vs Collection

FeatureCollectionStream
StorageStores elementsDoes not store elements
IterationExternal (loops, iterators)Internal (stream pipeline)
MutabilityMutableImmutable
EvaluationEagerLazy
ProcessingSequentialSequential or Parallel

11. Advantages of Stream API

  • Clean & declarative code
  • Supports functional programming
  • Lazy evaluation (efficient)
  • Parallel execution support
  • Rich set of built-in operations

 Key Takeaways from Java Stream API

  1. Streams are not Collections – They provide a pipeline for data processing, not a data structure for storage.
  2. Functional Programming in Java – Streams bring declarative, functional-style operations (map, filter, reduce) to collections.
  3. Lazy Evaluation – Intermediate operations (like map, filter) are lazy and executed only when a terminal operation is invoked.
  4. Powerful Intermediate Operations – Enable transformation, filtering, mapping, sorting, distinct values, and more.
  5. Terminal Operations Produce Results – Such as aggregation (reduce, count), collection (collect), or side-effects (forEach).
  6. Collectors Utility Class – Provides advanced collection capabilities like groupingBy, partitioningBy, joining, and mapping results into lists, sets, or maps.
  7. Parallel Streams – Allow multi-threaded execution but should be used cautiously due to non-deterministic ordering and potential overhead.
  8. Readable & Concise Code – Complex data processing tasks can be written in a clean and maintainable way using streams.
  9. Performance Considerations – While streams improve readability, they don’t always outperform traditional loops; careful use is required in performance-critical applications.
  10. Modern Java Development – Mastery of the Stream API is essential for writing efficient, elegant, and modern Java code.

     

Next Blog-  JDBC (Java Database Connectivity)

Sanjiv
0

You must logged in to post comments.

Get In Touch

123 Street, New York, USA

+012 345 67890

techiefreak87@gmail.com

© Design & Developed by HW Infotech