skip to content

When and how should you use peek() and sorted() (including a custom comparator)? What are the pitfalls of each?

level: middleimportance: should knowfreq 58%

answer

  1. peek = observe/debug, pass-through, don't depend on it
  2. peek may be skipped (e.g. count optimization) or never run
  3. sorted() needs Comparable -> else ClassCastException
  4. sorted(Comparator): comparing + thenComparing + reversed
  5. sorted is a barrier: O(n) memory, full traversal, no infinite streams

basics

~20 s

peek lets you look at each element as it flows by, mainly for debugging or logging, without changing it. sorted orders the stream, either by natural order or by a Comparator you pass in. Pitfalls: peek may not run for every element (and shouldn't change state), and sorted must buffer the whole stream and needs Comparable elements if you give no comparator.

solid answer

~50 s

peek(Consumer) is an intermediate op that performs a side effect on each element and passes it through unchanged — its intended use is debugging/logging, not transformation. Because it's lazy and laziness allows short-circuiting, peek may run on fewer elements than you expect (or zero if no terminal op), so never rely on it for required logic or state mutation. sorted() orders elements by natural ordering, which requires the elements to implement Comparable, else you get a ClassCastException at runtime; sorted(Comparator) lets you supply ordering explicitly and compose with thenComparing, reversed, nullsFirst, etc. sorted is a stateful barrier: it buffers the whole stream, costs O(n) memory and at least O(n log n) time, defeats upstream short-circuiting, and never terminates on infinite streams. Best practice: filter/limit before sorting, prefer Comparator.comparing(...) over hand-written comparators, and don't use peek for anything you can't afford to skip.

code

java · 14 lines
java
record Person(String last, String first, int age) {}

List<Person> ordered = people.stream()
    .filter(p -> p.age() >= 18)                 // shrink BEFORE sorting
    .peek(p -> log.debug("kept {}", p))         // observe only; not required logic
    .sorted(Comparator.comparing(Person::last)  // primary key
                      .thenComparing(Person::first)) // tie-breaker
    .collect(Collectors.toList());

// Pitfall: no-arg sorted() on non-Comparable elements -> ClassCastException at runtime
// people.stream().sorted();  // Person is not Comparable -> CCE

// Pitfall: peek skipped by an optimizing terminal op
long n = Stream.of(1,2,3).peek(System.out::println).count(); // may print nothing

go deeper

for a junior

Knows peek is for debugging/logging and sorted orders the stream, and can pass a simple Comparator like Comparator.comparing(...).

for a middle

Explains peek's pass-through nature and that it may be skipped, and uses sorted with composed comparators (thenComparing/reversed/nullsFirst) while knowing the Comparable requirement.

for a senior

Articulates sorted as a stateful barrier with O(n) memory and short-circuit implications, the ClassCastException runtime risk, and why peek must never carry required logic, especially in parallel.

for a principal

Guides API/style conventions (comparator composition over hand-rolled, no peek for state), and reasons about top-N selection or external sorting alternatives when sorted's full-buffer cost is prohibitive at scale.

## peek ### Signature and intent `Stream<T> peek(Consumer<? super T> action)` A **Consumer** takes one element and returns nothing (`void`) — it performs a *side effect*. `peek` runs the consumer on each element **as it passes** and then forwards the same element unchanged. Its documented purpose is **debugging**: seeing what flows through a stage of the pipeline. ```java list.stream() .peek(x -> System.out.println("before filter: " + x)) .filter(x -> x > 2) .peek(x -> System.out.println("after filter: " + x)) .collect(toList()); ``` ### Pitfalls of peek 1. **It may not run for every element.** Because the pipeline is lazy and may short-circuit, the JVM can skip `peek` for elements that don't need to be produced. A famous case: in some JDK versions `stream.peek(...).count()` skips `peek` entirely because `count` can be computed without traversing elements. So **never** rely on `peek` for logic that must happen. 2. **It must not mutate state you depend on.** Using `peek` to add to an external list or change elements is an anti-pattern — it's a side effect in a place designed only for observation, and parallel streams make such mutations unsafe. 3. **No terminal op = peek never runs.** Like all intermediate ops, it's lazy. Rule of thumb: `peek` is for *looking*, never for *doing required work*. ## sorted ### Two forms - `sorted()` — orders by **natural ordering**. This requires every element to implement `Comparable<T>` (e.g. `Integer`, `String`). If they don't, you get a **`ClassCastException` at runtime** (not a compile error). - `sorted(Comparator<? super T> comparator)` — orders by the comparator you supply, for custom or multi-key ordering. ```java // Natural order Stream.of(3, 1, 2).sorted(); // 1, 2, 3 // Custom comparator, composed people.stream() .sorted(Comparator.comparing(Person::lastName) .thenComparing(Person::firstName) .reversed()); // Null-safe ordering names.stream().sorted(Comparator.nullsFirst(Comparator.naturalOrder())); ``` ### Building comparators idiomatically Prefer the `Comparator` factory/combinator methods over hand-written lambdas: - `Comparator.comparing(keyExtractor)` — sort by an extracted key. - `comparingInt/comparingLong/comparingDouble` — primitive-key versions avoid boxing. - `thenComparing(...)` — tie-breakers. - `reversed()` — invert. - `nullsFirst / nullsLast` — handle nulls. These are readable, correct (they avoid `a - b` integer-overflow bugs), and composable. ### Pitfalls of sorted 1. **Stateful barrier.** `sorted` must buffer the **entire** stream before emitting anything, so it uses O(n) memory and forces a full traversal — and it **never terminates on an infinite stream**. 2. **Defeats upstream short-circuiting.** `sorted().limit(3)` still consumes and sorts everything. 3. **Requires Comparable for the no-arg form** — otherwise a runtime `ClassCastException`. 4. **Stable but order-sensitive in parallel.** `sorted` is a stable sort; in parallel pipelines maintaining encounter order adds coordination cost. ### Best practice - Apply `filter`/`limit`/`distinct` *before* `sorted` so it sorts fewer elements. - Use `Comparator.comparing(...)` chains, not raw subtraction. - If you only need the top N, a sorted+limit works but consider whether a partial selection is cheaper for very large inputs. ## Putting it together `peek` and `sorted` sit at opposite ends: `peek` is a lightweight, side-effecting *observation* you must not depend on, while `sorted` is a heavyweight *stateful barrier* whose ordering you must get right (Comparable or Comparator) and whose cost you must respect.

  • Why is using peek() to populate an external list considered an anti-pattern?
    peek is meant for non-interfering observation. The JDK may skip it for elements it can optimize away (so your list could be incomplete), and in parallel streams concurrent writes to a shared mutable list are unsafe (data races). If you want to collect elements, use a terminal collector (collect/toList); if you want the result, use map. peek's contract does not promise it runs for every element.
  • What happens if you call the no-arg sorted() on a stream of a type that doesn't implement Comparable?
    It compiles, but at runtime when sorted tries to compare elements it throws a ClassCastException (the elements can't be cast to Comparable). The fix is to pass an explicit Comparator via sorted(Comparator), e.g. Comparator.comparing(...).

peek is a CCTV camera over a conveyor belt — it watches items go by but never touches them, and if the line is shut early it films nothing. sorted is a sorting depot that must receive every parcel before it can ship the first one out in order.

saying these in an interview costs you the question

  • Using peek for transformation or required side effects instead of map/collect
  • Assuming peek always runs once per element
  • Forgetting that no-arg sorted() needs Comparable (runtime ClassCastException)
  • Writing comparators as (a,b) -> a - b (integer overflow) instead of Comparator.comparingInt
  • Sorting before filtering/limiting, making sorted do extra work

context