Java 8+ Features and Streams Interview Questions for Java
25 Java 8+ Features and Streams questions with answers for java roles — 9 easy, 11 medium, 5 hard.
About Java 8+ Features and Streams
Java 8 changed how Java code is written, and later releases kept adding power. This topic covers lambdas, functional interfaces, method references, the Stream API, collectors, Optional, the date and time API, and newer features such as var, records, text blocks, sealed types and pattern matching for switch up to Java 21.
Sheet progressStart practicing
3 QUESTIONS FREESign up to unlock the full question bank and track your progress.Unlock every question and answer, then continue practising company-wise and package-wise.
A functional interface has exactly one abstract method, so it can be implemented with a lambda or method reference. It may also have default and static methods. Examples are Runnable, Comparator, and those in java.util.function: Function, Predicate, Consumer, Supplier and BiFunction. The @FunctionalInterface annotation makes the compiler enforce the single abstract method rule.
02
What is the difference between intermediate and terminal operations in streams?
Intermediate operations like filter, map, sorted and distinct return a new stream and are lazy; nothing runs until a terminal operation is called. Terminal operations like collect, forEach, reduce, count and findFirst trigger processing and produce a result. Laziness lets streams fuse steps into a single pass and short-circuit, so findFirst on filtered data stops early.
03
What is the difference between map() and flatMap()?
map() transforms each element into exactly one new element, so a Stream of orders mapped to totals stays one-to-one. flatMap() transforms each element into a stream and flattens all of them into one stream, which is useful for one-to-many cases, such as turning a list of orders into a single stream of all items with orders.stream().flatMap(o -> o.items().stream()).
Method references are short forms of lambdas that just call an existing method. Types: static method (Integer::parseInt), instance method of a specific object (System.out::println), instance method of an arbitrary object of a type (String::toUpperCase) and constructor (ArrayList::new). They make stream code clearer when the lambda only forwards its arguments.
05
A team member chains Optional.get() everywhere and gets NoSuchElementException. What do you suggest?
get() on an empty Optional throws, so it brings back the same problem Optional was meant to solve. Suggest orElseThrow with a clear domain exception, orElse or orElseGet for defaults, and map or flatMap to chain safe transformations. Static analysis can flag get() without isPresent. Since Java 10 orElseThrow() without arguments is the clear name for the unchecked version.
06
Find the second highest salary from a list of employees using streams.
Sort distinct salaries in descending order and skip one.
Optional<Integer> second = employees.stream()
.map(Employee::salary)
.distinct()
.sorted(Comparator.reverseOrder())
.skip(1)
.findFirst();
This returns empty if fewer than two distinct salaries exist. For huge data a single pass tracking top two is O(n).
07
Write a stream to partition numbers into even and odd lists.
partitioningBy splits into a map with true and false keys.
Map<Boolean, List<Integer>> parts = nums.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
List<Integer> evens = parts.get(true);
List<Integer> odds = parts.get(false);
Both keys are always present, even if one list is empty, unlike groupingBy.
08
Join a list of names into a comma separated string with brackets using streams.
Situation: a reviewer said my one-line stream with nested collectors was hard to follow. Task: balance brevity with clarity. Action: I accepted the point, split the pipeline into named steps with small helper methods and added a comment on the business rule. Result: the next person who changed it did so without asking me, and I now aim for code a new joiner can read in one go.
Optional is a container that may or may not hold a value, used mainly as a method return type to show that a result can be absent, for example findById. Use map, filter, orElse, orElseGet and orElseThrow instead of calling get() blindly. Do not use Optional for fields, parameters or collections, and prefer orElseGet when the default is costly to create.
A lambda can use a local variable from the enclosing method only if that variable is final or effectively final, meaning it is never reassigned after initialisation. Lambdas capture the value, not the variable, so this avoids confusing behaviour and races. To accumulate results, use a stream collector or an AtomicInteger instead of changing a captured local.
12
What is the difference between Collectors.toMap() and groupingBy()?
toMap() creates one entry per element using key and value functions and throws IllegalStateException on duplicate keys unless you give a merge function. groupingBy() collects all elements with the same key into a List by default, or applies a downstream collector like counting or summingInt. Use toMap for unique keys like id and groupingBy for categories like city.
13
A stream pipeline that updates a shared ArrayList in forEach gives random results with parallel(). Why?
ArrayList is not thread safe, and parallel streams run forEach on many threads, so additions race, get lost or throw exceptions. Side effects in streams are the problem. Replace forEach with collect(Collectors.toList()) or toList(), which handles merging per thread safely. Keep lambdas stateless and avoid modifying external state inside stream operations.
14
toMap() throws Duplicate key exception in production. How do you fix it?
The data has repeated keys that the developer assumed were unique. Decide the business rule, then pass a merge function, for example toMap(User::email, u -> u, (a, b) -> a) to keep the first, or use groupingBy if all values are needed. Also check upstream data quality, since duplicates may signal a deeper bug such as missing unique constraints.
15
Legacy code uses anonymous classes and loops everywhere. How would you modernise it safely?
First add tests around the behaviour. Then refactor in small steps: anonymous Comparators and Runnables become lambdas, simple loops that filter and transform become streams, and null checks around returns become Optional where useful. Keep complex loops with early exits as loops if a stream is less readable. Use IDE refactorings and review each change to avoid altering behaviour.
16
Using streams, find the employee with the highest salary in each department.
Group by department with a maxBy downstream collector.
Map<String, Optional<Employee>> top = employees.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.maxBy(Comparator.comparingInt(Employee::salary))));
To get values without Optional, wrap with collectingAndThen(maxBy(...), Optional::get), which is safe because each group has at least one element.
17
Write a sealed interface with records and a switch that computes area.
The switch needs no default because the type set is closed.
sealed interface Shape permits Circle, Rect {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}
static double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.r() * c.r();
case Rect(double w, double h) -> w * h;
};
}
18
Flatten a list of lists of integers and compute the sum of squares of the even numbers.
flatMap then use a primitive stream to avoid boxing.
int result = lists.stream()
.flatMap(List::stream)
.filter(n -> n % 2 == 0)
.mapToInt(n -> n * n)
.sum();
mapToInt returns an IntStream, whose sum() is simpler and faster than reduce on boxed Integers.
19
Tell me about a time you made a codebase more readable.
Situation: our reporting module had nested loops 200 lines long that new joiners feared. Task: make it maintainable without changing output. Action: I added snapshot tests on real report files, then refactored into small methods using streams, groupingBy and records for intermediate data, and reviewed each step with a teammate. Result: the module shrank by 40 percent and later changes took a day instead of a week.
20
Describe a time you had to learn a new technology quickly.
Situation: I joined a project using reactive Spring WebFlux with no prior experience. Task: contribute within two weeks. Action: I studied Project Reactor basics, built a small practice service, paired with a senior on my first ticket and kept notes on common operators. Result: I delivered my first feature in the second week and later wrote the team guide on avoiding blocking calls in reactive code.
Parallel streams use the common ForkJoinPool and help only for large, CPU-bound, stateless work on sources that split well, like arrays and ArrayList. Avoid them for small data, I/O or blocking calls, LinkedList or iterate() sources, ordered operations like limit and findFirst, and shared mutable state. In a web server they compete for the same common pool, so measure before using them.
22
Explain pattern matching for switch and record patterns in Java 21.
Pattern matching for switch lets a case match on type and bind a variable, such as case Circle c -> Math.PI * c.r() * c.r(). Record patterns deconstruct a record directly: case Point(int x, int y). Guards add conditions with when. With sealed types the compiler checks the switch is exhaustive, so adding a new subtype causes a compile error where it is not handled.
23
What is the difference between reduce() and collect()?
reduce() combines elements into a single immutable value using an identity and an associative function, such as summing numbers. collect() is a mutable reduction that accumulates into a container like a List, Map or StringBuilder, which is far more efficient for building collections. Using reduce to build a list creates many intermediate copies, so use collect for containers.
24
You need to process a 5 GB file using streams without running out of memory. How?
Use Files.lines(path), which reads lazily line by line, inside try-with-resources. Apply filter and map, then aggregate with a collector that keeps small results such as counts or sums instead of collecting all lines into a list. If the output is large, write it incrementally to a BufferedWriter. Avoid sorted() and distinct() on huge data because they buffer everything.
25
A stream over a list of orders is slow in a hot path. How do you analyse it?
Profile first with JFR or async-profiler to confirm the stream is the hotspot. Check for repeated work like calling sorted() inside a loop, boxing overhead with Stream of Integer instead of IntStream, or re-streaming the same list many times. Cache computed maps, use primitive streams and, if still slow, a plain loop can be clearer and faster for very hot code.
Frequently asked questions
How important is Java 8+ Features and Streams in Java interviews?
Java 8 changed how Java code is written, and later releases kept adding power. This topic covers lambdas, functional interfaces, method references, the Stream API, collectors, Optional, the date and time API, and newer features such as var, records, text blocks, sealed types and pattern matching for switch up to Java 21.
How should I use the Java 8+ Features and Streams sheet?
Go top to bottom — easy to hard. Answer before opening each answer, tick what you solved and save misses to Last-Minute Revision.