Table of contents
1.
Introduction
2.
Use of Stream in Java
3.
The Rationale to using Streams.
3.1.
An example is: Traditional Loop vs. Stream
3.1.1.
Using Loop: Traditional Approach:
3.1.2.
When Stream API is used:
4.
The Way a Java Stream can be Created
5.
Java Streams Syntax
6.
Complete practicing example
7.
Features of Java Streams
7.1.
1. Lazy Evaluation
7.2.
2. No Storage
7.3.
3. Functional Style
7.4.
4. Parallel Processing
7.5.
5. Immutable Results
7.6.
6. Infinite Streams
8.
Various Stream Operations
8.1.
1. Intermediate Operations (Process Data)
8.2.
2. Produce Results (Terminal Operations)
8.3.
3. Special Numeric Streams
9.
Benefits of Java Streams
9.1.
1. Cleaner, More Readable Code
9.2.
2. Less Boilerplate Code
9.3.
3. Easy Parallel Processing
9.4.
4. Powerful Data Processing
9.5.
5. Flexible Data Handling
9.6.
6. Better Performance for Large Data
10.
Real-World Example: Processing Orders
11.
Terminal Operations in Java Streams
11.1.
1. Collecting Results (collect())
11.2.
2. Counting Elements (count())
11.3.
3. Finding Elements (findFirst/findAny)
11.4.
4. Checking Conditions (anyMatch/allMatch/noneMatch)
11.5.
5. Reducing to Single Value (reduce())
11.6.
6. Specialized Reductions
11.7.
7. Iterating (forEach())
11.8.
8. Array Conversion (toArray())
12.
Java Stream Streams Applied in the Real World
13.
Frequently Asked Questions
13.1.
Why do I use streams and when to use streams instead of traditional loops?
13.2.
Is streams necessarily faster than loops?
13.3.
May I run a stream again after terminal operation?
14.
Conclusion
Last Updated: Jun 22, 2025
Easy

Stream API in Java

Career growth poll
Do you think IIT Guwahati certified course can help you in your career?

Introduction

Java Stream API is an effective component of the Java 8 that aids in managing a collection of objects. Stream API enables you to apply filtering, sorting, and mapping to your data, among others, in a style that is easier to read, instead of manually writing long loops. It acts like a pipeline in which data passes between various operations which makes the code clean and fast.

Stream API in Java

In this article, we are going to discuss how to write a stream, stream syntax, some of the important features, various operations, and application

Use of Stream in Java

Java streams are utilized to apply a list of objects (such as lists or sets) in a declarative manner. Rather than encapsulate loops manually, you may concatenate operations to filter, manipulate, or summarise data effectively.

The Rationale to using Streams.

Readability- You do not have to write several loops instead, you can concatenate operations in one line.

Performance- Streams are able to operate data more quickly due to parallelism when dealing with large amounts of data.

Fewer Boilerplate Code- Temporary variables and involved loop conditions are not required.

An example is: Traditional Loop vs. Stream

Suppose that we have a set of numbers and we need to retrieve even numbers.

Using Loop: Traditional Approach:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = new ArrayList<>();
for (int num : numbers) {
    if (num % 2 == 0) {
        evenNumbers.add(num);
    }
}
System.out.println(evenNumbers); // Output: [2, 4, 6]
You can also try this code with Online Java Compiler
Run Code


When Stream API is used:

List<Integer> evenNumbers= numbers.stream()
filter(num -> num%2==0)
.collect(Collectors.toList());
System.out.println(evenNumbers); // [2, 4, 6]
stream() -> makes a list into a stream.

filter (num -> num % 2 == 0) → Retains only even values.

Collectors.toList()(rarr sapretina reducibili) = transforms stream into list.
You can also try this code with Online Java Compiler
Run Code


Therefore, we may say that Streams reduce the code length and its readability in comparison to the usual loops.

The Way a Java Stream can be Created

It is easy to write a Stream in Java. Streams may be produced based on various sources such as collections, arrays or even on single values. So, here are the most frequent ones:

1. Out of a Set (List, etc.)

Just call .stream() if you have a List, Set, or otherwise Collection:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");  


// Create a stream from the list  

Stream<String> nameStream = names.stream();  


2. Of an Array

To transform an array to a stream use Arrays.stream():

int[] numbers = {1, 2, 3, 4, 5};  
// Create a stream from the array  
IntStream numStream = Arrays.stream(numbers);  


3. Using Stream.of()

Stream.of() is to be used in case you possess individual elements:

Stream<String> stream = Stream.of("Java", "Python", "C++");  


4. Using Stream.generate()

Generates an infinite sequence (which may be random data, or random constants):

// Infinite stream of random numbers  
Stream<Double> randomNumbers = Stream.generate(Math::random).limit(5);  
randomNumbers.forEach(System.out::println); 


5. Using Stream.iterate()

produces a stream with a starting-point value and a function:

First 5 even numbers (0, 2, 4, 6, 8) stream

Stream <Integer> = Stream.iterate(0, n -> n + 2).limit(5);
evenNumbers.forEach(System.out::println);


6. Java NIO Files

You may read a file line by line:

try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) {  
    lines.forEach(System.out::println);  
} catch (IOException e) {  
    e.printStackTrace();  
}  


The most important things to keep in mind:

  • Streams are lazy → Operations are only done when a terminal operation (such as collect() or forEach() is invoked.
     
  • Streams are once used and cannot be reused again: You should generate a new stream when it is required.

Java Streams Syntax

The simple syntax of Java Streams is of clear structure:

  • Acquire a stream of a data source (collection, array and so forth)
     
  • Use intermediate (filter, map, etc.)
     
  • Use some terminal operation (collect, forEach, etc.)


The overall pattern is :

dataSource.stream() // 1. Create stream
(4) intermediateOperation() // 2. Process data
terminalOperation(); // 3. Get result


Now we will get to know this in detail with Examples:

1. How to make a Stream

List<String> names = List.of("John", "Mary", "Peter");
Stream<String> nameStream = names.stream();


2. Middle steps (Chained)

rList<String> result = names.stream()
    .filter(name -> name.length() > 3)  // Keep names longer than 3 chars
    .map(String::toUpperCase)           // Convert to uppercase
    .sorted()                           // Sort alphabetically
    .collect(Collectors.toList());      // Terminal operation


3. Terminal Operations (Cease the Stream)

// Print each element

names.stream().forEach(System.out::println);


// Count elements

long count = names.stream().count();


// Convert to array

String[] nameArray = names.stream().toArray(String[]::new);


Noticeable Syntax:

Streams are lazy (run until the very end)
You can also try this code with Online Java Compiler
Run Code


The use of method chaining (every operation returns a new stream) is the norm The other advantage of the streaming API is given its very core nature.

After terminal operation streams cannot be reused

Complete practicing example

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

public class StreamSyntaxExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 9, 3);
        
        List<Integer> processed = numbers.stream()
            .filter(n -> n > 3)          // Keep numbers > 3
            .map(n -> n * 2)             // Double each number
            .sorted()                    // Sort ascending
            .collect(Collectors.toList()); // Store result
            
        System.out.println(processed);    // Output: [10, 16, 18]
    }
}
You can also try this code with Online Java Compiler
Run Code


The most common pitfalls to evade

  • Making a slip of memory about terminal operations (stream cannot work without it)
     
  • Attempting to reuse a stream (throws IllegalStateException)
     
  • Pre-existing source collection alterations during streaming (may lead to errors)

Features of Java Streams

Java streams are provided with a set of strong features to distinguish them with the traditional collections. Here come the proper examples to realize it: .

1. Lazy Evaluation

Streams do not execute read until a terminal operation is called. This renders them productive.

Example:

// Nothing happens yet - just creates the pipeline
Stream<String> filteredNames = names.stream()
    .filter(name -> {
        System.out.println("Filtering: " + name);
        return name.length() > 3;
    });
// Only now the processing happens
List<String> result = filteredNames.collect(Collectors.toList());
You can also try this code with Online Java Compiler
Run Code

2. No Storage

Streams do not hold data - but instead merely work upon its source data.

Example:

List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3));
Stream<Integer> stream = numbers.stream();

numbers.add(4); // Modifying original collection
// Stream will see the modification
stream.forEach(System.out::println); // Prints 1,2,3,4
You can also try this code with Online Java Compiler
Run Code

3. Functional Style

Streams promote functional programming.

Example:

List<Integer> numbers = List.of(1, 2, 3, 4);


// Instead of this:
int sum = 0;
for (int n : numbers) {
    sum += n;
}
// You can do this:
int sum = numbers.stream()
    .reduce(0, (a, b) -> a + b);
You can also try this code with Online Java Compiler
Run Code

4. Parallel Processing

Streams can simply be run in a parallel manner to perform better.

Example:

List<Integer> bigList = /* some large list */;

// Sequential processing
long count = bigList.stream()
    .filter(n -> n % 2 == 0)
    .count();

// Parallel processing (faster for large datasets)
long parallelCount = bigList.parallelStream()
    .filter(n -> n % 2 == 0)
    .count();
You can also try this code with Online Java Compiler
Run Code

5. Immutable Results

Stream processes do not alter the original data.

Example:

List<String> original = List.of("a", "b", "c");
List<String> transformed = original.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());
System.out.println(original); // [a, b, c]
System.out.println(transformed); // [A, B, C]
You can also try this code with Online Java Compiler
Run Code

6. Infinite Streams

You may set up streams that will be endless (but set restrictions).

Example:

// Infinite stream of random numbers

Stream<Double> randoms = Stream.generate(Math::random);


// But we can take just 5

randoms.limit(5).forEach(System.out::println);

Various Stream Operations

There are two broad categories of stream operations: intermediate (process data), and terminal (produce results). Let us look at some of the most important operations: .

1. Intermediate Operations (Process Data)

These methods yield a new stream, and may be combined in chains.

a) filter() - Retains items that fit a criterion

List names =List.of("John", "Mary", "Peter", "Anna");
List<String> longNames= names.stream()
filter(name -> name.length() > 4)
.collect(Collectors.toList());
// Output: Peter


b) map() - reshapes every component

List < Integer > nameLengths = names.stream()
.map(String::length)
.collect(Collectors.toList());
Result: [4, 4, 5, 4]


c) sorted() - Sorts elements

List < String > sortedNames = names.stream ()
.sorted()
.collect(Collectors.toList());
// Output: [Anna, John, Mary, Peter]


d) distinct() - Deletes duplicates

List<Integer> numbers = List.of(1, 2, 2, 3, 3, 3);
List<Integer> unique = numbers.stream()
    .distinct()
    .collect(Collectors.toList());
// Result: [1, 2, 3]


e) limit() - Emparts initial N elements

List<Integer> firstTwo = numbers.stream()
    .limit(2)
    .collect(Collectors.toList());
// Result: [1, 2]

2. Produce Results (Terminal Operations)

These operations complete the flow and give a final outcome.

a) forEach() - An action is done on each element

names.stream()
.forEach(System.out::println);
// Prints all the names


b) collect() - Accumulates results into a collection

Set
.collect(Collectors.toSet());


c) count() - Counts items

long count = names.stream()
filter(name -name.startsWith("J"))
.count();
// Countifies names beginning with J


d) reduce() - Merges elements

Optional

reduce((a, b) -> a + ", "+ b);
// Joins the names by commas


e) anyMatch()/allMatch() - Condition checks

boolean hasLongName = names.stream().
anyMatch((name) -> (name.length() > 5));
// Returns true in case any name contains more than 5 letters

3. Special Numeric Streams

In the case of primitive types, apply IntStream, LongStream and DoubleStream.

IntStream example:

IntStream.range(1, 5)       // 1,2,3,4
    .map(n -> n * 2)        // 2,4,6,8
    .average()              // 5.0
    .ifPresent(System.out::println);


We can have a look at full Example:

List<Product> products = List.of(
    new Product("Laptop", 999.99),
    new Product("Phone", 699.99),
    new Product("Tablet", 299.99)
);

// Get names of products under $500, sorted
List<String> cheapProducts = products.stream()
    .filter(p -> p.getPrice() < 500)
    .map(Product::getName)
    .sorted()
    .collect(Collectors.toList());
// Result: ["Tablet"]


Important Notes:

  • Order is significant in chaining of operations
     
  • There are short-circuitting (such as findFirst())
     
  • What can not be re-used after terminal operation are streams

Benefits of Java Streams

Streams make working with collections in Java much easier and cleaner. Let’s discuss why they're so useful, with real examples you'll actually use in coding:

1. Cleaner, More Readable Code

Compare these two ways to filter users:

Old Way (Loops)

List<User> activeUsers = new ArrayList<>();
for (User user : allUsers) {
    if (user.isActive() && user.getAge() > 18) {
        activeUsers.add(user);
    }
}


Stream Way

List<User> activeUsers = allUsers.stream()
    .filter(User::isActive)
    .filter(user -> user.getAge() > 18)
    .collect(Collectors.toList());


The stream version is easier to read and understand at a glance.

2. Less Boilerplate Code

No need to write:

  • Initializations (new ArrayList<>())
     
  • Loop syntax
     
  • Temporary variables
     
  • If conditions
     

Just chain operations together clearly.

3. Easy Parallel Processing

Making code run faster on multiple CPU cores is simple:

// Regular stream (single thread)

List<String> names = users.stream()... 


// Parallel stream (multi-threaded)

List<String> names = users.parallelStream()...


Just change .stream() to .parallelStream()!

4. Powerful Data Processing

Do complex operations in one line:

// Get average salary of active engineers in NY
double avgSalary = employees.stream()
    .filter(e -> e.getDepartment().equals("Engineering"))
    .filter(Employee::isActive)
    .filter(e -> e.getLocation().equals("NY"))
    .mapToDouble(Employee::getSalary)
    .average()
    .orElse(0.0);

5. Flexible Data Handling

Easily convert between collection types:

// List to Set

Set<String> uniqueNames = names.stream()
    .collect(Collectors.toSet());


// List to Map

Map<Integer, User> userMap = users.stream()
    .collect(Collectors.toMap(User::getId, user -> user));

6. Better Performance for Large Data

Streams process data lazily (only when needed) and can optimize operations.

Real-World Example: Processing Orders

// Get total value of pending orders for premium customers
double total = orders.stream()
    .filter(Order::isPending)
    .filter(o -> o.getCustomer().isPremium())
    .mapToDouble(Order::getTotal)
    .sum();

Terminal Operations in Java Streams

Terminal operations are where the real action happens in streams. They produce a final result and close the stream. Let's look at the most important ones with practical examples we’ll actually use. 

1. Collecting Results (collect())

The most common terminal operation - turns streams back into collections.

List<String> names = List.of("Akash", "Gunjan", "Sinki");


// To List

List<String> list = names.stream().collect(Collectors.toList());


// To Set

Set<String> set = names.stream().collect(Collectors.toSet());


// To Map (ID -> Name)

Map<Integer, String> map = users.stream()
    .collect(Collectors.toMap(User::getId, User::getName));

// Joining strings

String joined = names.stream().collect(Collectors.joining(", "));
// Result: "John, Mary, Anna"

2. Counting Elements (count())

Simple way to count how many items match your filters.

long count = products.stream()
    .filter(p -> p.getPrice() > 100)
    .count();

3. Finding Elements (findFirst/findAny)

// Get first expensive product
Optional<Product> firstExpensive = products.stream()
    .filter(p -> p.getPrice() > 1000)
    .findFirst();
// Get any matching product (faster in parallel)
Optional<Product> anyExpensive = products.stream()
    .filter(p -> p.getPrice() > 1000)
    .findAny();

4. Checking Conditions (anyMatch/allMatch/noneMatch)

boolean hasExpensive = products.stream()
    .anyMatch(p -> p.getPrice() > 1000);

boolean allExpensive = products.stream()
    .allMatch(p -> p.getPrice() > 100);

boolean noneFree = products.stream()
    .noneMatch(p -> p.getPrice() == 0);

5. Reducing to Single Value (reduce())

Powerful way to combine all elements.

// Sum all prices
double totalPrice = products.stream()
    .map(Product::getPrice)
    .reduce(0.0, Double::sum);


// Concatenate all names
Optional<String> allNames = products.stream()
    .map(Product::getName)
    .reduce((a,b) -> a + ", " + b);

6. Specialized Reductions

For numbers, we have handy shortcuts.

// Sum

int sum = numbers.stream().mapToInt(Integer::intValue).sum();


// Average

OptionalDouble avg = numbers.stream().mapToInt(Integer::intValue).average();


// Max

OptionalInt max = numbers.stream().mapToInt(Integer::intValue).max();

7. Iterating (forEach())

When you just need to do something with each element.

// Print all products
products.stream().forEach(System.out::println);
// Update all prices
products.stream()
    .forEach(p -> p.setPrice(p.getPrice() * 1.1)); // 10% price increase

8. Array Conversion (toArray())

String[] nameArray = names.stream().toArray(String[]::new);


Complete Real-World Example

// Process order lines to get summary
OrderSummary summary = orderLines.stream()
    .filter(line -> line.getQuantity() > 0)
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),
        lines -> {
            double total = lines.stream()
                .mapToDouble(line -> line.getPrice() * line.getQuantity())
                .sum();
            int itemCount = lines.stream()
                .mapToInt(OrderLine::getQuantity)
                .sum();
            return new OrderSummary(total, itemCount);
        }
    ));
You can also try this code with Online Java Compiler
Run Code


Key Things to Remember:

  • Terminal operations close the stream - you can't reuse it
     
  • Most terminal operations are eager (execute immediately)
     
  • Some return Optional (like findFirst) to handle empty streams
     
  • Performance varies - choose the right one for your needs

Java Stream Streams Applied in the Real World

1. Stream, Data Filtering and Processing: streams are ideal when you have a large dataset. To take a real example, in e-commerce, a platform could filter out of stock products, add a discount to a particular category, or sort the products by their price or rating, all at maintainable and clean code. Complex data processing is also easy because it enables many operations executed one after the other.
 

2. Data Analysis and Reporting: Business applications frequently have to produce reports out of raw data. It is easy to use streams to compute averages, sums or maximums (such as highest sales, average order value, or total revenue) without typing up expensive loops. This can be particularly used with financial or analyses applications.
 

3. Bulk Data Operations: Streams can be used in processing records when using database results or CSV files in bulk. And you can clean data (de-duplicate, fix format), transform data (convert currencies, normalize values) or validate information on-the-fly to storage, without pushing the memory usage requirements to any great extent due to lazy evaluation.
 

4. API Response Scenario: All well-modernized applications deal with JSON/XML API answers. Streams can make it easy to extract nested information, filter the information they need, or convert API responses into domain objects. This is typical in microservices designs in which services talk extensively.
 

5. Parallel Task Processing: Parallel streams can automatically divide tasks in order to distribute across CPU cores in tasks that are CPU-heavy, such as image processing, machine learning predictions, or scientific calculations. This favors large scale computations greatly.

Frequently Asked Questions

Why do I use streams and when to use streams instead of traditional loops?

Streams should be used whenever it is necessary to have a filter, a map and a reduce applied to a collection to make the code cleaner. Use loops where you do simple iterations or where you require changing things in iteration. Streams are meant to be well-suited to read-only transformation of data.

Is streams necessarily faster than loops?

Not always. In small datasets, loops can even be more efficient because of stream set up overhead. The use of huge datasets and parallel processing makes streams sparkle. In every case the real benefit is code readability and maintainability over raw speed.

May I run a stream again after terminal operation?

No, streams are one-time pipes. After calling a terminal operation (such as collect or forEach), the stream will be consumed. In case you desire to go through some more operations, you will have to generate another stream out of the source.

Conclusion

Within this article we have come to understand how Java streams add a modern approach to collection processing. We discussed the creation of streams, syntax, main features, different operations (both intermediate and terminal) and situations used in practice. Streams aid in writing cleaner, more expressive code to do data processing work and enable parallel execution to get a higher performance. Keep in mind that streams are strong, but old style of loops can be still used in easier situations. Your decision will be based on your use case needs and your readability.

Live masterclass