Stream Best Practices and Common Mistakes
Streams make data processing short and readable, but only when they are used well. A badly written stream can be harder to read than a loop, slower than necessary, or even wrong. In this lesson, you will learn the best practices for writing streams, the most common mistakes and how to fix them, and then test yourself with ten questions on an employee list.
Best Practices
- Keep lambdas pure. A lambda in a pipeline should only compute a result from its input, not change anything outside.
- One operation per line. Start each line with the dot so the pipeline reads like a list of steps.
- Use method references where they read well:
map(Employee::name). - Filter early. Cheap
filtersteps first, expensivemapsteps later. - Use the most specific terminal operation:
count,sum,anyMatch,maxsay more thanreduceorcollect. - Prefer
toList()(Java 16) for read-only results. - Return streams rarely. Return a
Listfrom a method unless the caller really needs laziness. - Know when to use a loop. Loops are clearer when you need
break, several outputs at once, checked exceptions, or index-heavy logic.
Common Mistakes and Fixes
| Mistake | Fix |
|---|---|
stream.forEach(x -> list.add(x)) | List<T> list = stream.toList(); |
filter(...).count() > 0 | anyMatch(...) |
filter(...).findFirst().isPresent() | anyMatch(...) |
map(...).filter(x -> x != null) | map(...).filter(Objects::nonNull) |
sorted().findFirst() | min(comparator) |
collect(Collectors.toList()) for read-only lists | toList() |
reduce(0, Integer::sum) | mapToInt(...).sum() |
Optional.get() without a check | orElse, orElseThrow, ifPresent |
| Reusing a stream variable | Create a new stream each time |
parallel() on small data | Remove it; measure first |
Example: Before and After
Let's see an example of a stream that works but breaks several rules, and its clean version.
import java.util.ArrayList;
import java.util.List;
public class Cleanup {
record Student(String name, int marks) { }
public static void main(String[] args) {
List<Student> students = List.of(
new Student("Navika", 91), new Student("Srihitha", 38),
new Student("Advitha", 85));
List<String> messy = new ArrayList<>();
students.stream().forEach(s -> {
if (s.marks() >= 50) {
messy.add(s.name().toUpperCase());
}
});
boolean anyFailMessy =
students.stream().filter(s -> s.marks() < 50).count() > 0;
List<String> clean = students.stream()
.filter(s -> s.marks() >= 50)
.map(Student::name)
.map(String::toUpperCase)
.toList();
boolean anyFail = students.stream().anyMatch(s -> s.marks() < 50);
System.out.println(messy + " " + anyFailMessy);
System.out.println(clean + " " + anyFail);
}
}[NAVIKA, ADVITHA] true
[NAVIKA, ADVITHA] trueExplanation: Both versions give the same answer. The messy one hides a loop inside forEach, changes an outside list (which would break in parallel), and counts every failure just to ask "is there any?". The clean one states each step, has no side effects, and anyMatch stops at the first failure.
The Checkpoint: Ten Questions on an Employee List
This program answers ten questions about a list of employees, each with a single stream pipeline and no loops. Try to write each answer yourself before reading the code.
- Names of everyone in Engineering, sorted.
- Number of employees in each department, sorted by department.
- Average salary in each department.
- Names of the 3 highest-paid employees.
- Name of the highest-paid employee in each department.
- Names split into age 35 and over, and under 35.
- Total salary bill.
- All distinct cities, sorted.
- Names of employees in Mumbai or Delhi, as one comma-separated string.
- The second-highest salary.
import java.util.Comparator;
import java.util.List;
import java.util.TreeMap;
import static java.util.stream.Collectors.*;
public class EmployeeQuestions {
record Employee(int id, String name, String dept, int salary,
int age, String city) { }
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee(1, "Navika", "Engineering", 95000, 29, "Bengaluru"),
new Employee(2, "Srihitha", "Engineering", 120000, 35, "Hyderabad"),
new Employee(3, "Advitha", "HR", 60000, 41, "Chennai"),
new Employee(4, "Anamaneni", "Sales", 70000, 26, "Mumbai"),
new Employee(5, "Navika A", "Engineering", 110000, 31, "Bengaluru"),
new Employee(6, "Srihitha A", "Sales", 82000, 38, "Delhi"),
new Employee(7, "Advitha A", "Finance", 90000, 45, "Mumbai"));
System.out.println("1. " + employees.stream()
.filter(e -> e.dept().equals("Engineering"))
.map(Employee::name).sorted().toList());
System.out.println("2. " + employees.stream()
.collect(groupingBy(Employee::dept, TreeMap::new, counting())));
System.out.println("3. " + employees.stream()
.collect(groupingBy(Employee::dept, TreeMap::new,
averagingInt(Employee::salary))));
System.out.println("4. " + employees.stream()
.sorted(Comparator.comparingInt(Employee::salary).reversed())
.limit(3).map(Employee::name).toList());
System.out.println("5. " + employees.stream()
.collect(groupingBy(Employee::dept, TreeMap::new,
collectingAndThen(
maxBy(Comparator.comparingInt(Employee::salary)),
o -> o.get().name()))));
System.out.println("6. " + employees.stream()
.collect(partitioningBy(e -> e.age() >= 35,
mapping(Employee::name, toList()))));
System.out.println("7. " + employees.stream()
.mapToInt(Employee::salary).sum());
System.out.println("8. " + employees.stream()
.map(Employee::city).distinct().sorted().toList());
System.out.println("9. " + employees.stream()
.filter(e -> e.city().equals("Mumbai") || e.city().equals("Delhi"))
.map(Employee::name).collect(joining(", ")));
System.out.println("10. " + employees.stream()
.map(Employee::salary).distinct()
.sorted(Comparator.reverseOrder()).skip(1).findFirst());
}
}1. [Navika, Navika A, Srihitha]
2. {Engineering=3, Finance=1, HR=1, Sales=2}
3. {Engineering=108333.33333333333, Finance=90000.0, HR=60000.0, Sales=76000.0}
4. [Srihitha, Navika A, Navika]
5. {Engineering=Srihitha, Finance=Advitha A, HR=Advitha, Sales=Srihitha A}
6. {false=[Navika, Anamaneni, Navika A], true=[Srihitha, Advitha, Srihitha A, Advitha A]}
7. 627000
8. [Bengaluru, Chennai, Delhi, Hyderabad, Mumbai]
9. Anamaneni, Srihitha A, Advitha A
10. Optional[110000]Explanation: Each answer is one pipeline. Questions 2, 3 and 5 use groupingBy with a downstream collector and TreeMap::new for sorted keys. Question 6 uses partitioningBy. Question 10 removes duplicate salaries, sorts them from highest to lowest, skips the highest and takes the next one, wrapped in an Optional because there might not be a second salary.
Interview Corner
What are common mistakes when using Java streams?
Using forEach to add to an external collection instead of collecting; modifying shared state from lambdas, which breaks parallel streams; reusing a stream after a terminal operation; calling Optional.get() without checking; using filter(...).count() > 0 instead of anyMatch; sorting just to find a minimum; forgetting limit on infinite streams; and using parallel streams on small or I/O-bound workloads. Streams should be side-effect-free pipelines with the most specific terminal operation.
Practice
- Easy: Rewrite this with a stream: a loop that collects the lengths of all names longer than 5 letters into a list.
- Harder: Add three questions of your own to the employee checkpoint, for example the youngest employee in each city, and answer each with one pipeline.
Key Takeaways
Was this tutorial helpful?