Java Streams API
The Java Streams API (introduced in Java 8) provides a declarative, functional-style way to process collections of data.
It allows you to perform complex data transformations without explicit loops, making code more readable, concise, and expressive.
What is a Stream?
A Stream is:
- Not a data structure
- A pipeline of operations
- Used to process data from a source (Collection, Array, I/O, etc.)
A stream does not store data.
It processes data on demand.
Key Characteristics
- ❌ No data storage
- ✅ Lazy evaluation
- ❌ Cannot be reused
- ✅ Functional-style operations
- ✅ Can be sequential or parallel
Stream Pipeline
A stream pipeline has three parts:
Source → Intermediate Operations → Terminal Operation
list.stream()
.filter(n -> n > 10)
.map(n -> n * 2)
.forEach(System.out::println);
Stream Sources
Streams can be created from:
Collections
list.stream();
list.parallelStream();
Arrays
Arrays.stream(arr);
Static Stream methods
Stream.of(1, 2, 3);
IntStream.range(1, 5);
Strings
s.chars(); // IntStream
s.codePoints(); // Unicode-safe
Types of Streams
Object Stream
Stream<T>
Primitive Streams
IntStreamLongStreamDoubleStream
Primitive streams exist to avoid boxing/unboxing overhead.
Intermediate Operations
Intermediate operations:
- Return a new stream
- Are lazy
- Do not execute until a terminal operation is called
Common Intermediate Operations
filter
stream.filter(x -> x > 5);
map
stream.map(x -> x * 2);
distinct
stream.distinct();
sorted
stream.sorted();
limit / skip
stream.limit(5);
stream.skip(3);
map vs mapToInt / mapToObj
stream.map(x -> x.toString());
Primitive streams need conversion:
IntStream.mapToObj(i -> String.valueOf(i));
mapToObj() exists only on primitive streams.
Terminal Operations
Terminal operations:
- Trigger execution
- Consume the stream
- Produce a result or side effect
Common Terminal Operations
forEach
stream.forEach(System.out::println);
collect
List<Integer> list = stream.collect(Collectors.toList());
reduce
int sum = stream.reduce(0, Integer::sum);
count
long count = stream.count();
findFirst / findAny
stream.findFirst();
anyMatch / allMatch / noneMatch
stream.anyMatch(x -> x > 10);
Collectors
Collectors are utility methods for collecting stream results.
Common Collectors
Collectors.toList()
Collectors.toSet()
Collectors.toMap()
Collectors.joining(", ")
Collectors.groupingBy()
Collectors.partitioningBy()
Example:
Map<String, List<Employee>> byDept =
employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
Lazy Evaluation
Streams execute only when needed.
stream.filter(x -> {
System.out.println(x);
return x > 5;
});
☝️ Nothing prints until a terminal operation is called.
Parallel Streams
Parallel streams split work across threads automatically.
list.parallelStream()
.map(this::process)
.collect(Collectors.toList());
When to use
- Large datasets
- CPU-bound tasks
- Stateless operations
When NOT to use
- I/O operations
- Small datasets
- Order-sensitive logic
Stream vs Collection
| Feature | Collection | Stream |
|---|---|---|
| Stores data | Yes | No |
| Can iterate multiple times | Yes | No |
| Lazy | No | Yes |
| Functional style | No | Yes |
Common Pitfalls
- Reusing a stream ❌
stream.forEach(...);
stream.count(); // IllegalStateException
- Modifying source during stream execution ❌
- Using streams for simple loops unnecessarily ❌
When to Use Streams
✅ Data transformation ✅ Filtering, mapping, grouping ✅ Readability over control
❌ Complex control flow ❌ Heavy mutation logic
Summary
- Streams provide a clean, functional approach to data processing
- Separate what you want from how it’s done
- Lazy, expressive, and powerful
- Best used for transformation-heavy logic
One-line takeaway
Streams turn loops into readable data pipelines.