Understanding Stream API Fundamentals in Java
Course: Core Java Training
Topic: Understanding Stream API Fundamentals
Course Link: Core Java Training
Demo Link: Register for Course Demo
Website: JustAcademy
1. Introduction to the Java Stream API
The Java Stream API was introduced in Java 8 to simplify the processing of data from collections, arrays, and other data sources. It provides a functional programming approach that allows developers to filter, transform, sort, and aggregate data using concise and readable operations.
Traditional Java programs often use loops and conditional statements to process collections. The Stream API allows developers to describe what should happen to the data instead of manually controlling every iteration.
For example, if a list contains several numbers and the requirement is to select only even numbers, a stream can perform this operation using the filter() method.
Example: Traditional Approach
import java.util.Arrays;
import java.util.List;
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
List<Integer> numbers =
Arrays.asList(10, 15, 20, 25, 30);
List<Integer> evenNumbers = new ArrayList<>();
for (Integer number : numbers) {
if (number % 2 == 0) {
evenNumbers.add(number);
}
}
System.out.println(evenNumbers);
}
}
Output:
[10, 20, 30]
Example: Using the Stream API
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<Integer> numbers =
Arrays.asList(10, 15, 20, 25, 30);
List<Integer> evenNumbers = numbers.stream()
.filter(number -> number % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers);
}
}
Output:
[10, 20, 30]
The stream version expresses the same logic in a compact pipeline. It uses a lambda expression to specify the filtering condition.
2. Learning Objectives
- Understand what the Java Stream API is and why it is used.
- Understand the difference between collections and streams.
- Learn how to create streams from different data sources.
- Understand stream pipelines and their three main stages.
- Use intermediate operations such as
filter(), map(), and sorted().
- Use terminal operations such as
forEach(), count(), and collect().
- Understand lazy evaluation and single-use stream behavior.
- Apply streams to practical data-processing tasks.
3. What Is a Stream?
A stream is a sequence of elements that supports operations for processing data. A stream does not act as a storage container. Instead, it provides a way to process elements obtained from a source such as a collection, array, or generated sequence.
A stream can perform multiple operations in a pipeline, allowing data to be filtered, transformed, sorted, and collected into a result.
Important Characteristics of Streams
- No storage: A stream processes elements from a source rather than storing them as a collection does.
- Functional operations: Streams commonly use lambda expressions and method references.
- Pipeline processing: Multiple operations can be connected in sequence.
- Lazy intermediate operations: Intermediate operations generally execute only when a terminal operation initiates processing.
- Single use: A stream cannot be reused after a terminal operation consumes it.
- Sequential or parallel processing: Streams support both execution modes, depending on how they are created.
4. Difference Between Collections and Streams
| Feature |
Collection |
Stream |
| Purpose |
Stores and manages data. |
Processes data. |
| Data storage |
Contains elements. |
Does not independently store the source elements. |
| Iteration |
Can be traversed using loops and iterators. |
Uses operations such as filter, map, and forEach. |
| Modification |
Some collections allow adding, removing, or changing elements. |
Stream operations generally produce processing results rather than modifying the source. |
| Reuse |
Can usually be traversed multiple times. |
A consumed stream cannot be reused. |
| Processing |
Often uses explicit iteration. |
Supports declarative processing pipelines. |
5. Stream API Architecture
A stream pipeline generally contains three stages: a source, zero or more intermediate operations, and a terminal operation.
5.1 Stream Source
The source provides the elements to be processed. Common sources include lists, sets, arrays, and values supplied through stream factory methods.
List<String> names =
Arrays.asList("Amit", "Priya", "Rahul");
Stream<String> stream = names.stream();
5.2 Intermediate Operations
Intermediate operations transform or filter elements and return another stream. They are usually lazy.
names.stream()
.filter(name -> name.startsWith("A"))
.map(name -> name.toUpperCase());
This code builds a pipeline, but without a terminal operation, the pipeline is not processed.
5.3 Terminal Operations
A terminal operation initiates processing and produces a result or side effect.
names.stream()
.filter(name -> name.startsWith("A"))
.map(name -> name.toUpperCase())
.forEach(System.out::println);
Output:
AMIT
6. Creating Streams in Java
Java provides several ways to create streams depending on the source of the data.
6.1 Creating a Stream From a Collection
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> languages =
Arrays.asList("Java", "Python", "C++");
languages.stream()
.forEach(System.out::println);
}
}
The stream() method creates a sequential stream from the collection.
6.2 Creating a Stream Using Stream.of()
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream<String> languages =
Stream.of("Java", "Python", "JavaScript");
languages.forEach(System.out::println);
}
}
The Stream.of() method creates a stream from the supplied values.
6.3 Creating a Stream From an Array
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] numbers = {10, 20, 30, 40};
Arrays.stream(numbers)
.forEach(System.out::println);
}
}
Output:
10
20
30
40
6.4 Creating an Empty Stream
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream<String> emptyStream = Stream.empty();
System.out.println(emptyStream.count());
}
}
Output:
0
6.5 Creating a Stream of Numbers Using IntStream
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
IntStream.range(1, 6)
.forEach(System.out::println);
}
}
Output:
1
2
3
4
5
The range() method includes the starting value but excludes the ending value. The rangeClosed() method includes both endpoints.
7. Intermediate Operations
Intermediate operations return a new stream and allow developers to build processing pipelines. Most intermediate operations are lazy and do not process elements until a terminal operation is invoked.
7.1 filter()
The filter() method selects elements that satisfy a condition. It accepts a predicate that returns a boolean value.
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers =
Arrays.asList(5, 10, 15, 20, 25, 30);
numbers.stream()
.filter(number -> number > 15)
.forEach(System.out::println);
}
}
Output:
20
25
30
7.2 map()
The map() method transforms each element into another value. The output type can be different from the input type.
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names =
Arrays.asList("amit", "priya", "rahul");
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
}
}
Output:
AMIT
PRIYA
RAHUL
7.3 sorted()
The sorted() method sorts elements in their natural order or according to a supplied comparator.
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers =
Arrays.asList(50, 10, 40, 20, 30);
numbers.stream()
.sorted()
.forEach(System.out::println);
}
}
Output:
10
20
30
40
50
7.4 distinct()
The distinct() method removes duplicate elements according to their equality semantics.
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
Arrays.asList(10, 20, 10, 30, 20, 40)
.stream()
.distinct()
.forEach(System.out::println);
}
}
Output:
10
20
30
40
7.5 limit() and skip()
The limit() method restricts the maximum number of elements, while skip() ignores a specified number of elements.
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
Arrays.asList(10, 20, 30, 40, 50)
.stream()
.skip(1)
.limit(3)
.forEach(System.out::println);
}
}
Output:
20
30
40
7.6 flatMap()
The flatMap() method transforms each input element into a stream and combines the resulting streams into a single stream. It is useful for processing nested collections.
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<List<String>> groups = Arrays.asList(
Arrays.asList("A", "B"),
Arrays.asList("C", "D")
);
groups.stream()
.flatMap(List::stream)
.forEach(System.out::println);
}
}
Output:
A
B
C
D
8. Terminal Operations
Terminal operations complete the stream pipeline. They produce a result, such as a number, an optional value, a collection, or no direct return value.
8.1 forEach()
Executes an action for each element.
Stream.of("Java", "Python", "C++")
.forEach(System.out::println);
8.2 count()
Returns the number of elements in the stream.
long count = Stream.of(10, 20, 30, 40).count();
System.out.println(count);
Output: 4
8.3 collect()
The collect() operation gathers stream elements into a result such as a list, set, or map.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<String> result = Arrays.asList(
"Java", "Python", "JavaScript"
).stream()
.filter(language -> language.length() > 4)
.collect(Collectors.toList());
System.out.println(result);
}
}
Output:
[Python, JavaScript]
8.4 reduce()
The reduce() operation combines stream elements into a single result. It is often used for sums, products, or other aggregate calculations.
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int sum = Arrays.asList(10, 20, 30, 40)
.stream()
.reduce(0, Integer::sum);
System.out.println(sum);
}
}
Output:
100
8.5 min() and max()
These operations find the smallest or largest element according to a comparator and return an Optional.
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers =
Arrays.asList(45, 12, 78, 23, 56);
int minimum = numbers.stream()
.min(Comparator.naturalOrder())
.orElse(0);
int maximum = numbers.stream()
.max(Comparator.naturalOrder())
.orElse(0);
System.out.println("Minimum: " + minimum);
System.out.println("Maximum: " + maximum);
}
}
Output:
Minimum: 12
Maximum: 78
8.6 anyMatch(), allMatch(), and noneMatch()
These methods test whether elements satisfy a condition.
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers =
Arrays.asList(2, 4, 6, 8);
System.out.println(numbers.stream()
.anyMatch(n -> n > 5));
System.out.println(numbers.stream()
.allMatch(n -> n % 2 == 0));
System.out.println(numbers.stream()
.noneMatch(n -> n < 0));
}
}
Output:
true
true
true
9. Understanding Lazy Evaluation
Lazy evaluation means that intermediate operations do not normally process elements immediately. Processing begins when a terminal operation is invoked.
Example
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names =
Arrays.asList("Amit", "Anita", "Rahul");
names.stream()
.filter(name -> {
System.out.println("Checking: " + name);
return name.startsWith("A");
});
System.out.println("Pipeline created");
}
}
Output:
Pipeline created
The filter is never executed because the pipeline has no terminal operation.
Adding forEach() initiates the processing:
names.stream()
.filter(name -> {
System.out.println("Checking: " + name);
return name.startsWith("A");
})
.forEach(System.out::println);
This time, the elements are checked and matching names are printed.
10. Stream API and Optional
Some stream operations, such as findFirst(), findAny(), min(), and max(), return an Optional. Optional represents a value that may or may not be present and helps avoid directly accessing a missing result.
Example
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers =
Arrays.asList(10, 20, 30, 40);
int result = numbers.stream()
.filter(number -> number > 25)
.findFirst()
.orElse(-1);
System.out.println(result);
}
}
Output:
30
If no number matches the condition, orElse(-1) supplies -1 as the fallback value.
11. Primitive Streams
Java provides specialized stream types for primitive numeric values. They can avoid some boxing and unboxing overhead and provide convenient numeric operations.
| Stream Type |
Purpose |
Example Operation |
IntStream |
Processes int values. |
sum() |
LongStream |
Processes long values. |
count() |
DoubleStream |
Processes double values. |
average() |
Example: Calculating an Average
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
double average = Arrays.stream(
new int[]{10, 20, 30, 40, 50})
.average()
.orElse(0.0);
System.out.println(average);
}
}
Output:
30.0
12. Sequential Streams vs Parallel Streams
A sequential stream processes its pipeline in sequential mode. A parallel stream can divide work across multiple threads, subject to the stream source, operations, and runtime environment.
| Feature |
Sequential Stream |
Parallel Stream |
| Creation |
collection.stream() |
collection.parallelStream() |
| Processing |
Sequential execution mode. |
Can process partitions concurrently. |
| Overhead |
Usually lower for small tasks. |
Includes splitting and coordination overhead. |
| Use cases |
Ordinary data processing and order-sensitive work. |
Suitable independent computations on sufficiently large datasets. |
| Considerations |
Often easier to reason about. |
Requires care with shared state, ordering, and thread safety. |
Example
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers =
Arrays.asList(1, 2, 3, 4, 5);
numbers.parallelStream()
.map(number -> number * number)
.forEach(System.out::println);
}
}
The program prints the squares of the numbers, but their display order is not guaranteed when using forEach() on a parallel stream. Use forEachOrdered() when encounter-order output is required.
Parallel processing is not automatically faster. Measure performance and avoid unsafe shared-state modifications.
13. Common Mistakes in Stream API
| Mistake |
Why It Is a Problem |
Recommended Solution |
| Reusing a consumed stream |
A stream cannot be operated on again after consumption. |
Create a new stream from the source. |
| Forgetting a terminal operation |
The pipeline may never execute. |
Add a suitable terminal operation. |
| Modifying the source during processing |
Can lead to unpredictable or erroneous behavior. |
Avoid interfering with the source while it is being processed. |
| Overusing parallel streams |
Overhead may outweigh any performance benefit. |
Benchmark before adopting parallel processing. |
| Using side effects unnecessarily |
Can make execution harder to understand and parallel operations unsafe. |
Prefer stateless transformations and terminal results. |
| Ignoring Optional results |
A result may be absent. |
Use methods such as orElse(), orElseGet(), or ifPresent(). |
14. Best Practices for Using the Stream API
- Use streams when they make data-processing logic clearer than a traditional loop.
- Keep lambda expressions simple and readable.
- Prefer
filter() for selecting elements and map() for transforming them.
- Use
flatMap() to flatten nested streams or collections.
- Avoid modifying the original data source while the stream is processing it.
- Avoid relying on side effects inside intermediate operations.
- Use primitive streams for numeric operations where appropriate.
- Handle potentially absent results from
findFirst(), min(), and similar operations.
- Use parallel streams only after considering ordering, thread safety, and performance.
- Remember that a stream is single-use.
15. Practical Project: Student Marks Analysis
Project Objective: Create a Java program that processes student marks, filters students who passed, sorts their marks, calculates the average, and displays the results using the Stream API.
Program
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<Integer> marks =
Arrays.asList(45, 78, 32, 90, 65, 28, 85);
List<Integer> passedMarks = marks.stream()
.filter(mark -> mark >= 40)
.sorted()
.collect(Collectors.toList());
double average = marks.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
long passedCount = marks.stream()
.filter(mark -> mark >= 40)
.count();
System.out.println("All Marks: " + marks);
System.out.println("Passed Marks: " + passedMarks);
System.out.println("Passed Students: " + passedCount);
System.out.println("Average Marks: " + average);
}
}
Output:
All Marks: [45, 78, 32, 90, 65, 28, 85]
Passed Marks: [45, 65, 78, 85, 90]
Passed Students: 5
Average Marks: 60.42857142857143
Project Explanation
- The program stores student marks in a list.
- The first stream filters out marks below 40.
- The
sorted() operation arranges the passing marks in ascending order.
- The
collect() operation stores the results in a new list.
- A second stream calculates the average using
mapToInt() and average().
- A third stream uses
count() to calculate how many students passed.
Project Enhancements
- Display the highest and lowest marks.
- Calculate the number of students who failed.
- Assign grades based on marks using
map().
- Find whether any student scored 95 or higher using
anyMatch().
- Collect passing and failing marks into separate lists.
16. Practice Exercises
- Create a stream from a list of integers and print each number.
- Filter all even numbers from a list.
- Convert a list of names to uppercase using
map().
- Remove duplicate values using
distinct().
- Sort a list of strings alphabetically.
- Calculate the sum of integers using
reduce() or IntStream.sum().
- Find the first number greater than 100 using
filter() and findFirst().
- Flatten a list of lists using
flatMap().
- Calculate the average salary of a list of employees.
- Build a student marks report showing passing marks, average marks, and the highest mark.
17. Interview Questions and Answers
Q1. What is the Java Stream API?
The Stream API is a Java feature introduced in Java 8 for processing sequences of data using operations such as filtering, mapping, sorting, and aggregation.
Q2. Does a stream store data?
No. A stream processes data from a source and does not function as a data-storage collection.
Q3. What are the three stages of a stream pipeline?
The stages are the source, zero or more intermediate operations, and a terminal operation.
Q4. What is the difference between intermediate and terminal operations?
Intermediate operations return another stream and are generally lazy. Terminal operations initiate processing and produce a result or side effect.
Q5. What is lazy evaluation?
Lazy evaluation means intermediate operations generally do not execute until a terminal operation initiates the pipeline.
Q6. What is the difference between map() and filter()?
map() transforms elements, while filter() selects elements that satisfy a condition.
Q7. What is flatMap() used for?
flatMap() transforms elements into streams and flattens those streams into a single stream.
Q8. Can a stream be reused?
No. After a terminal operation consumes a stream, create a new stream for another pipeline.
Q9. What is the difference between stream() and parallelStream()?
stream() creates a sequential stream, while parallelStream() creates a stream capable of parallel processing.
Q10. What is reduce() used for?
reduce() combines elements into a single result, such as a sum or product.
Q11. What is the purpose of Optional in stream processing?
Optional represents a result that may be absent, such as when a search finds no matching element.
Q12. What are primitive streams?
IntStream, LongStream, and DoubleStream are specialized streams for primitive numeric values.
18. Quick Revision Checklist
- The Stream API was introduced in Java 8.
- Streams process data rather than store it.
- A stream pipeline consists of a source, intermediate operations, and a terminal operation.
filter() selects elements based on a condition.
map() transforms elements.
sorted() sorts elements.
distinct() removes duplicates.
flatMap() flattens nested streams.
collect() gathers elements into a result.
reduce() combines elements into one value.
- Intermediate operations are generally lazy.
- A consumed stream cannot be reused.
- Parallel streams require careful consideration of ordering, side effects, and performance.
19. Conclusion
The Java Stream API provides a powerful and expressive way to process data in modern Java applications. It reduces repetitive iteration code and makes common tasks such as filtering, transformation, sorting, and aggregation easier to express.
By understanding stream sources, intermediate operations, terminal operations, lazy evaluation, Optional results, and primitive streams, developers can build readable and maintainable programs. Practice these concepts with collections and small projects before applying them to more complex business applications.
Learn Core Java With JustAcademy
Strengthen your Java programming skills by exploring core concepts such as object-oriented programming, collections, exception handling, file handling, lambda expressions, and the Stream API.
Course Link: Explore Core Java Training
Demo Registration: Register for a Course Demo
Official Website: Visit JustAcademy