Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

Built for engineers who want to go deep.


← Java Interview Prep: 8+ Years (Senior & Lead)

Expert Core Java

  • Tricky Java Output, Operators & OOP Edge Cases — Interview Questions
  • Tricky Exceptions, Memory & Keyword Questions — Interview Questions
  • Classic Java Language Questions, Senior-Grade Answers — Interview Questions
  • Classic Collections, Threads & JDK APIs, Senior-Grade Answers — Interview Questions
  • Reflection, Dynamic Proxies, final & Modern OOP Design — Interview Questions

JVM Internals & Performance

  • Class Loading, Bytecode & Object Layout — Interview Questions
  • JIT Compilation & Runtime Optimisations — Interview Questions
  • Garbage Collectors Deep Dive — Interview Questions
  • JVM Tuning, GC Logs & Memory Footprint — Interview Questions
  • Memory Leaks, OutOfMemoryErrors & Profiling Tools — Interview Questions
  • Modules, Agents & Advanced JVM APIs — Interview Questions

Collections & Concurrency at Scale

  • Collections Internals & Complexity — Interview Questions
  • Iterators, Comparators & Ordering Contracts — Interview Questions
  • Concurrent Collections, Queues & Lock-Free Structures — Interview Questions
  • Threads, Executors & ForkJoin Internals — Interview Questions
  • Locks, Atomics, CAS & Synchronizers — Interview Questions
  • Java Memory Model, volatile, Fences & ThreadLocal — Interview Questions
  • Deadlock, Livelock, Starvation & Concurrent Design — Interview Questions
  • CompletableFuture, Parallel Streams & Non-Blocking I/O — Interview Questions

Modern Java (8 to 21+)

  • Lambdas & Functional Interfaces Internals — Interview Questions
  • Streams & Collectors Deep Dive — Interview Questions
  • Optional & Interface Default/Static Methods — Interview Questions
  • Java 9–25 Features & Virtual Threads — Interview Questions

Design Patterns, SOLID & Clean Code

  • Design Pattern Trade-offs & Combinations — Interview Questions
  • SOLID, Clean Code & Anti-Patterns — Interview Questions

Spring & Spring Boot Internals

  • IoC, Dependency Injection & Bean Lifecycle Internals — Interview Questions
  • Spring AOP, Proxies & @Async Internals — Interview Questions
  • Spring Configuration, Auto-Configuration & Custom Starters — Interview Questions
  • Spring MVC & REST Internals, Exception Frameworks — Interview Questions
  • Spring Security Advanced Internals — Interview Questions
  • Spring WebFlux, Reactor & R2DBC — Interview Questions
  • Spring Cloud, Observability & Distributed Tracing — Interview Questions
  • Spring Boot 3, Native Images & Production Scenarios — Interview Questions

JPA, Hibernate & Databases at Scale

  • Spring Data JPA — Queries, Projections, Custom Repositories & Locking — Interview Questions
  • JPA Entity Mapping, Associations & Cascades — Interview Questions
  • JPQL vs Native Queries in Depth — Interview Questions
  • Hibernate Caching — First-Level, Second-Level & Query Cache — Interview Questions
  • Lazy vs Eager Loading, LazyInitializationException & N+1 — Interview Questions
  • JPA Transactions, Propagation, Isolation & Dirty Checking — Interview Questions
  • SQL vs NoSQL, Indexing & Query Tuning — Interview Questions
  • Database Scaling, Replication, Pooling & Consistency Models — Interview Questions
  • Redis, Search, Time-Series, CDC & Transactional Data Modelling — Interview Questions

Testing Strategy & API Design

  • Spring Boot Test Slices, Context & Test Strategy — Interview Questions
  • Testing Web, Persistence, Security, Async & Messaging in Spring Boot — Interview Questions
  • JUnit 5 & Mockito, Advanced — Interview Questions
Chaturmind
← Java Interview Prep: 8+ Years (Senior & Lead)

Expert Core Java

  • Tricky Java Output, Operators & OOP Edge Cases — Interview Questions
  • Tricky Exceptions, Memory & Keyword Questions — Interview Questions
  • Classic Java Language Questions, Senior-Grade Answers — Interview Questions
  • Classic Collections, Threads & JDK APIs, Senior-Grade Answers — Interview Questions
  • Reflection, Dynamic Proxies, final & Modern OOP Design — Interview Questions

JVM Internals & Performance

  • Class Loading, Bytecode & Object Layout — Interview Questions
  • JIT Compilation & Runtime Optimisations — Interview Questions
  • Garbage Collectors Deep Dive — Interview Questions
  • JVM Tuning, GC Logs & Memory Footprint — Interview Questions
  • Memory Leaks, OutOfMemoryErrors & Profiling Tools — Interview Questions
  • Modules, Agents & Advanced JVM APIs — Interview Questions

Collections & Concurrency at Scale

  • Collections Internals & Complexity — Interview Questions
  • Iterators, Comparators & Ordering Contracts — Interview Questions
  • Concurrent Collections, Queues & Lock-Free Structures — Interview Questions
  • Threads, Executors & ForkJoin Internals — Interview Questions
  • Locks, Atomics, CAS & Synchronizers — Interview Questions
  • Java Memory Model, volatile, Fences & ThreadLocal — Interview Questions
  • Deadlock, Livelock, Starvation & Concurrent Design — Interview Questions
  • CompletableFuture, Parallel Streams & Non-Blocking I/O — Interview Questions

Modern Java (8 to 21+)

  • Lambdas & Functional Interfaces Internals — Interview Questions
  • Streams & Collectors Deep Dive — Interview Questions
  • Optional & Interface Default/Static Methods — Interview Questions
  • Java 9–25 Features & Virtual Threads — Interview Questions

Design Patterns, SOLID & Clean Code

  • Design Pattern Trade-offs & Combinations — Interview Questions
  • SOLID, Clean Code & Anti-Patterns — Interview Questions

Spring & Spring Boot Internals

  • IoC, Dependency Injection & Bean Lifecycle Internals — Interview Questions
  • Spring AOP, Proxies & @Async Internals — Interview Questions
  • Spring Configuration, Auto-Configuration & Custom Starters — Interview Questions
  • Spring MVC & REST Internals, Exception Frameworks — Interview Questions
  • Spring Security Advanced Internals — Interview Questions
  • Spring WebFlux, Reactor & R2DBC — Interview Questions
  • Spring Cloud, Observability & Distributed Tracing — Interview Questions
  • Spring Boot 3, Native Images & Production Scenarios — Interview Questions

JPA, Hibernate & Databases at Scale

  • Spring Data JPA — Queries, Projections, Custom Repositories & Locking — Interview Questions
  • JPA Entity Mapping, Associations & Cascades — Interview Questions
  • JPQL vs Native Queries in Depth — Interview Questions
  • Hibernate Caching — First-Level, Second-Level & Query Cache — Interview Questions
  • Lazy vs Eager Loading, LazyInitializationException & N+1 — Interview Questions
  • JPA Transactions, Propagation, Isolation & Dirty Checking — Interview Questions
  • SQL vs NoSQL, Indexing & Query Tuning — Interview Questions
  • Database Scaling, Replication, Pooling & Consistency Models — Interview Questions
  • Redis, Search, Time-Series, CDC & Transactional Data Modelling — Interview Questions

Testing Strategy & API Design

  • Spring Boot Test Slices, Context & Test Strategy — Interview Questions
  • Testing Web, Persistence, Security, Async & Messaging in Spring Boot — Interview Questions
  • JUnit 5 & Mockito, Advanced — Interview Questions
HomeLearnJava Interview PrepJava Interview Prep: 8+ Years (Senior & Lead)Modern Java (8 to 21+)
✓ FreeAdvanced· 11 min read

Streams & Collectors Deep Dive — Interview Questions

Stream lazy evaluation and loop fusion, terminal operations and why streams are single-use, short-circuiting (limit, anyMatch), reduce with identity/accumulator/combiner, sequential vs parallel performance, parallel streams with shared state and with I/O, multi-field sorting, toMap vs groupingBy (and duplicate keys), custom collectors, peek, flattening nested lists, the second-highest number, and whether intermediate operations change stream size.

Published September 25, 2026


How to use this lesson

Senior stream questions test two things:

  • Correctness: associativity, duplicate keys, shared state.
  • Performance intuition: laziness, boxing, parallelism overhead.

Give the idiomatic code, and the trap.

Q1. How does lazy evaluation work in streams?

Short answer: Intermediate operations (filter, map, sorted, …) only build a pipeline description. Nothing runs until a terminal operation is invoked. Then the elements are pulled one at a time through the whole chain (loop fusion), rather than each stage processing the whole collection:

Stream.of("a", "bb", "ccc", "dddd")
      .filter(s -> { System.out.println("filter " + s); return s.length() > 1; })
      .map(s -> { System.out.println("map " + s); return s.toUpperCase(); })
      .findFirst();
// prints: filter a, filter bb, map bb → stops. "ccc" and "dddd" are never touched

The benefits:

  • Short-circuiting (see Q4).
  • No intermediate collections.
  • Infinite streams are possible (Stream.iterate(...).limit(n)).

The exceptions: stateful operations like sorted() and distinct() must see (or buffer) all the upstream elements before emitting.

Learn it in depth → Streams API

Q2. Why is collect(Collectors.toList()) a terminal operation?

Short answer: Because it consumes the stream and produces a non-stream result: a List. Terminal operations trigger the pipeline's execution, and close the stream. collect performs a mutable reduction:

  1. a supplier creates the container;
  2. the accumulator adds each element;
  3. the combiner merges the partial containers in parallel runs.

Key points to cover:

  • Since Java 16, stream.toList() returns an unmodifiable list, and is shorter.
  • Collectors.toList() makes no guarantee about the list type or mutability (it's currently an ArrayList).
  • Use toCollection(ArrayList::new) when you need a mutable list.

Q3. Why can't a stream be reused after a terminal operation?

Short answer: A stream is a one-shot pipeline over a data source, not a data structure. Once a terminal operation has run, the source (an iterator, an I/O channel, a generator) may be consumed, and the pipeline's internal state is finished. Reusing it throws IllegalStateException: stream has already been operated upon or closed. This design lets streams wrap non-repeatable sources, like files, sockets or generators. To reuse the logic, keep a Supplier<Stream<T>>, or re-create the stream from the collection.

Q4. How does short-circuiting work with limit() and anyMatch()?

Short answer: Short-circuiting operations can finish without processing all the elements:

  • Terminal: anyMatch, allMatch, noneMatch, findFirst, findAny. anyMatch stops at the first true, and allMatch at the first false.
  • Intermediate: limit(n), takeWhile(pred) (Java 9). Once satisfied, they signal upstream to stop pulling.

Because evaluation is lazy and element-at-a-time, upstream operations run only for the elements actually needed. That's what makes Stream.iterate(1, n -> n + 1).filter(isPrime).limit(10) terminate. In parallel ordered streams, limit and findFirst are expensive: they must respect encounter order. unordered() or findAny are cheaper.

Q5. Explain how reduce() works, with identity, accumulator and combiner.

Short answer: reduce(identity, accumulator, combiner) folds the elements into one result:

  • identity: the starting value, and the neutral element (0 for a sum, "" for concatenation). For correctness in parallel, combiner(identity, x) == x.
  • accumulator (partialResult, element) → newPartial: folds one element in.
  • combiner (partial1, partial2) → merged: merges the partial results from parallel sub-streams. It's needed when the result type differs from the element type, and must be compatible with the accumulator.
  • Both functions must be associative and stateless, or parallel results will be wrong.
int totalQty = orders.stream().reduce(0, (sum, o) -> sum + o.qty(), Integer::sum);   // T=Order, U=Integer
BigDecimal total = orders.stream().map(Order::total).reduce(BigDecimal.ZERO, BigDecimal::add);
Optional<Order> biggest = orders.stream().reduce((a, b) -> a.total().compareTo(b.total()) >= 0 ? a : b);

Key points to cover:

  • For mutable containers (lists, maps, StringBuilder), use collect, not reduce. reduce with mutation breaks in parallel, and copying is O(n²).
  • Prefer specialised forms (mapToInt(...).sum(), max(comparator)) for clarity and to avoid boxing.

Q6. What's the performance difference between sequential and parallel streams?

Short answer: Parallel streams can speed up large, CPU-bound, stateless work (roughly up to the core count), but they add overhead: splitting, task scheduling, combining, and thread coordination. So they're:

  • faster for big datasets, with expensive per-element computation and good splitting sources (arrays, ArrayList, IntStream.range);
  • slower for small collections, cheap operations, LinkedList or I/O sources, ordered operations (sorted, limit, forEachOrdered), boxing-heavy pipelines, or when the common pool is already busy (in servers).

Always benchmark with JMH on production-like hardware. The "NQ model" (N elements × Q cost per element > ~10,000) is a useful rule of thumb.

Q7. How would you sort a list of objects by several fields using streams?

Short answer: Use sorted(Comparator), with composed comparators. The stream itself stays unchanged (it isn't sorted in place):

List<Employee> sorted = employees.stream()
        .sorted(Comparator.comparing(Employee::department)
                .thenComparing(Employee::salary, Comparator.reverseOrder())
                .thenComparing(Employee::name))
        .toList();

Key points to cover:

  • sorted() is a stateful, full barrier: it buffers everything.
  • For big data, sort in the database (ORDER BY, with an index) instead.

Q8. Can you safely parallelise a stream that modifies a shared variable?

Short answer: No. Mutating shared state from a parallel stream (list.add, counter++, map.put on non-concurrent maps) causes data races: lost updates, corrupt collections, or ArrayIndexOutOfBoundsException inside ArrayList. Even a thread-safe container adds contention, and loses ordering. Use reductions and collectors, which are designed for parallel execution: each thread accumulates into its own container, and the containers are merged.

List<String> bad = new ArrayList<>();
names.parallelStream().map(String::trim).forEach(bad::add);     // RACE: wrong size, or an exception

List<String> good = names.parallelStream().map(String::trim).toList();
Map<String, Long> counts = words.parallelStream().collect(Collectors.groupingByConcurrent(w -> w, Collectors.counting()));

Q9. What's the difference between Collectors.toMap() and groupingBy()?

Short answer:

  • toMap(keyFn, valueFn) produces one value per key. It throws IllegalStateException on duplicate keys, unless you pass a merge function ((a, b) -> a). It also throws NullPointerException for null values (it uses HashMap.merge internally). Its overloads accept a map supplier (LinkedHashMap::new, TreeMap::new).
  • groupingBy(classifier) produces Map<K, List<T>>: all the elements sharing a key are collected together. It accepts a downstream collector (counting(), summingInt, mapping, toSet, maxBy, teeing), and a map factory.
Map<String, Customer> byEmail = customers.stream()
        .collect(Collectors.toMap(Customer::email, c -> c, (first, dup) -> first));   // explicit duplicate policy

Map<String, Long> ordersPerCity = orders.stream()
        .collect(Collectors.groupingBy(Order::city, TreeMap::new, Collectors.counting()));

Learn it in depth → Collectors and groupingBy

Q10. How do you implement a custom collector?

Short answer: Use Collector.of(supplier, accumulator, combiner, finisher, characteristics...), or implement the Collector interface. You define:

  • supplier: creates a new mutable container;
  • accumulator: adds an element to the container;
  • combiner: merges two containers (for parallel runs);
  • finisher: converts the container into the final result (optional, with IDENTITY_FINISH);
  • characteristics: UNORDERED, CONCURRENT, IDENTITY_FINISH.
// Collect into an immutable top-N list without sorting the whole stream
static <T> Collector<T, ?, List<T>> topN(int n, Comparator<? super T> cmp) {
    return Collector.of(
        () -> new PriorityQueue<T>(cmp),                                  // a min-heap by cmp
        (pq, t) -> { pq.offer(t); if (pq.size() > n) pq.poll(); },
        (a, b) -> { b.forEach(t -> { a.offer(t); if (a.size() > n) a.poll(); }); return a; },
        pq -> pq.stream().sorted(cmp.reversed()).toList());
}
List<Order> top5 = orders.stream().collect(topN(5, Comparator.comparing(Order::total)));

Key points to cover:

  • Java 12's Collectors.teeing combines two collectors, without writing a custom one.
  • Java 22+ gatherers (Stream.gather) handle custom intermediate operations (windows, scans, deduplication by key).

Q11. What is peek() for, and when should you avoid it?

Short answer: peek(action) runs a side effect for each element as it flows past, returning the same stream. It's intended for debugging (logging the elements between stages). Avoid it for business logic:

  • it may not run at all, or not for every element: short-circuiting, or the implementation skipping elements when the result is computable without them (for example count() on a sized source since Java 9 doesn't execute peek);
  • it's non-deterministic in parallel streams;
  • mutating state in it breaks the stateless contract.

For side effects, use forEach at the end, or a proper map that returns new values.

Q12. How would you flatten a nested list with streams?

Short answer: Use flatMap, which maps each element to a stream, and concatenates those streams:

List<List<OrderLine>> nested = orders.stream().map(Order::lines).toList();
List<OrderLine> flat = nested.stream().flatMap(List::stream).toList();

// Directly from the parent objects, and for deeper nesting chain more flatMaps:
List<String> skus = orders.stream().flatMap(o -> o.lines().stream()).map(OrderLine::sku).distinct().toList();

// Java 16+: mapMulti avoids creating a Stream per element (useful for small or optional expansions)
List<String> skus2 = orders.stream().<String>mapMulti((o, sink) -> o.lines().forEach(l -> sink.accept(l.sku()))).toList();

Q13. How can you efficiently find the second-highest number with streams?

Short answer: The simplest readable version sorts, which is O(n log n):

Optional<Integer> secondHighest = numbers.stream()
        .distinct()                                     // "second highest distinct value"; drop this if duplicates count
        .sorted(Comparator.reverseOrder())
        .skip(1)
        .findFirst();

For large inputs, a single O(n) pass, without sorting, is better:

int[] top = numbers.stream().mapToInt(Integer::intValue).collect(
        () -> new int[]{Integer.MIN_VALUE, Integer.MIN_VALUE},
        (t, x) -> { if (x > t[0]) { t[1] = t[0]; t[0] = x; } else if (x < t[0] && x > t[1]) t[1] = x; },
        (a, b) -> { for (int x : b) { if (x > a[0]) { a[1] = a[0]; a[0] = x; } else if (x < a[0] && x > a[1]) a[1] = x; } });
// top[1] is the answer (check it for MIN_VALUE, meaning "not found")

Clarify the edge cases: duplicates (is [5, 5, 3] answered with 5 or 3?), fewer than two elements (return Optional.empty()), and negative numbers.

Q14. What are the risks of parallelStream() with I/O-bound tasks?

Short answer:

  • It runs on the common ForkJoinPool, with only cores − 1 threads. Blocking I/O (HTTP, JDBC, files) occupies those few threads while they wait, so throughput is poor, and every other user of the common pool (other parallel streams, CompletableFuture defaults) starves.
  • ForkJoin compensates for blocking only through ManagedBlocker, which ordinary I/O libraries don't use.
  • There's no control over concurrency, timeouts or error handling per call. One slow call stalls the whole terminal operation.
  • It can overload downstream services, with uncontrolled parallel calls.

Instead: use a virtual-thread executor, or CompletableFuture with a dedicated bounded executor, or structured concurrency, and limit concurrency with a semaphore.

Q15. Can intermediate operations change the size of a stream?

Short answer: Yes.

  • filter, distinct, limit, skip, takeWhile and dropWhile reduce the count;
  • flatMap/mapMulti can expand or shrink it (each element becomes 0..n elements);
  • map, peek and sorted preserve it.

That's why a stream's SIZED characteristic can be lost after certain operations. It matters for optimisations: count() can skip evaluating the pipeline only while the size is known, and toArray pre-sizes the result.

Follow-up questions this topic invites — and their answers

Q: What's the difference between findFirst() and findAny()? A: findFirst returns the first element in encounter order. It's deterministic, but costly in parallel. findAny may return any element (in practice, often the first one sequentially). It's cheaper in parallel, and fine when any match will do.

Q: How do you avoid boxing overhead in streams? A: Use the primitive streams (IntStream, LongStream, DoubleStream) through mapToInt and friends, and their terminal operations (sum, average, summaryStatistics). Use boxed() only when an object stream is needed.

Q: What is Collectors.teeing (Java 12)? A: It passes every element to two collectors, then merges their results with a function. For example: count and sum together, giving an average object in one pass.

Q: When should you not use streams? A: With complex control flow (early returns, checked exceptions, several accumulators), in performance-critical tight loops over primitives (sometimes a plain loop is clearer and faster), and when debugging readability suffers. Streams are a tool, not a style mandate.

Previous

Lambdas & Functional Interfaces Internals — Interview Questions

Next

Optional & Interface Default/Static Methods — Interview Questions

AI Tutor

Lesson: Streams & Collectors Deep Dive — Interview Questions

Quick actions

AI responses can be inaccurate. Verify critical information.