Java 8 introduced Streams API, which changed the way developers process collections. Instead of writing long loops and temporary variables, streams allow us to write declarative, functional-style code.
Most developers are comfortable with basic operations like filter, map, and collect. However, Java Streams offer many advanced techniques that can make code more expressive, efficient, and readable.
In this article, we will explore some advanced stream techniques every Java developer should know.
Understanding Lazy Evaluation
One of the most powerful characteristics of streams is lazy evaluation.
Intermediate operations like filter, map, and sorted are not executed until a terminal operation is called.
List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe");
names.stream()
.filter(name -> {
System.out.println("Filtering: " + name);
return name.startsWith("J");
})
.map(name -> {
System.out.println("Mapping: " + name);
return name.toUpperCase();
})
.findFirst();
Output
Filtering: John
Mapping: John
Even though there are multiple elements, only the first match is processed due to short-circuiting.
Why this matters
Lazy evaluation improves performance because the stream processes only what is necessary.
Using flatMap for Nested Collections
When working with nested collections, flatMap is extremely useful.
Example Problem
Suppose we have a list of students and each student has multiple courses.
class Student {
String name;
List<String> courses;
}
We want a single list of all courses.
List<Student> students = getStudents();
List<String> courses =
students.stream()
.flatMap(student -> student.getCourses().stream())
.collect(Collectors.toList());
What flatMap does
Instead of producing:
Stream<List<String>>
It flattens the structure into:
Stream<String>
This is extremely powerful when working with nested data structures.
Parallel Streams
Streams can be parallelized easily using parallelStream().
List<Integer> list = Arrays.asList(10, 20, 60, 30, 80, 90);
list.parallelStream()
.filter(n -> n > 50)
.map(n -> n * 2)
.forEach(System.out::println);
Output
160
120
180
Parallel streams use the ForkJoinPool internally.
When to use parallel streams
Good for:
- CPU intensive tasks
- Large datasets
- Independent operations
Avoid when:
- Operations involve shared mutable state
- Tasks are very small
- Order matters heavily
Avoiding Common Stream Mistakes
1 Modifying external variables
Bad practice:
List<Integer> result = new ArrayList<>();
list.stream().forEach(n -> result.add(n * 2));
Better
List<Integer> result =
list.stream()
.map(n -> n * 2)
.collect(Collectors.toList());
2 Overusing Streams
Streams are powerful, but sometimes simple loops are more readable.
Always prioritize clarity over cleverness.
Combining Streams
You can concatenate streams easily.
Stream<Integer> stream1 = Stream.of(1,2,3);
Stream<Integer> stream2 = Stream.of(4,5,6);
Stream<Integer> combined = Stream.concat(stream1, stream2);
combined.forEach(System.out::println);
Output:
1 2 3 4 5 6
Conclusion
Java 8 Streams introduced a powerful functional programming model to Java. While basic operations are widely used, mastering advanced techniques can greatly improve your code quality and performance.
Key takeaways:
- Streams use lazy evaluation
- flatMap helps flatten nested collections
- Advanced collectors enable powerful data transformations
- Parallel streams can improve performance for large workloads
- Custom collectors provide flexibility for specialized tasks
- When used correctly, streams allow you to write clean, concise, and expressive code.
What's next?
In the next part we will see the best practices and some short comings of java streams.
Top comments (0)