Stream Best Practices and Common Mistakes

Naveen AnamaneniUpdated · 6 min read

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 filter steps first, expensive map steps later.
  • Use the most specific terminal operation: count, sum, anyMatch, max say more than reduce or collect.
  • Prefer toList() (Java 16) for read-only results.
  • Return streams rarely. Return a List from 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

MistakeFix
stream.forEach(x -> list.add(x))List<T> list = stream.toList();
filter(...).count() > 0anyMatch(...)
filter(...).findFirst().isPresent()anyMatch(...)
map(...).filter(x -> x != null)map(...).filter(Objects::nonNull)
sorted().findFirst()min(comparator)
collect(Collectors.toList()) for read-only liststoList()
reduce(0, Integer::sum)mapToInt(...).sum()
Optional.get() without a checkorElse, orElseThrow, ifPresent
Reusing a stream variableCreate a new stream each time
parallel() on small dataRemove 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.

Cleanup.java▶ Try it
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);
    }
}
Output
[NAVIKA, ADVITHA] true
[NAVIKA, ADVITHA] true

Explanation: 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.

  1. Names of everyone in Engineering, sorted.
  2. Number of employees in each department, sorted by department.
  3. Average salary in each department.
  4. Names of the 3 highest-paid employees.
  5. Name of the highest-paid employee in each department.
  6. Names split into age 35 and over, and under 35.
  7. Total salary bill.
  8. All distinct cities, sorted.
  9. Names of employees in Mumbai or Delhi, as one comma-separated string.
  10. The second-highest salary.
EmployeeQuestions.java▶ Try it
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());
    }
}
Output
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

  1. Easy: Rewrite this with a stream: a loop that collects the lengths of all names longer than 5 letters into a list.
  2. 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.

Quick check: Stream best practices

4 questions
1Which is the better way to check if any element matches?
2Why avoid adding to an outside list inside forEach?
3Which is the best way to find the smallest element?
4When is a plain loop better than a stream?

Key Takeaways

Was this tutorial helpful?

Lesson 324 of 482