Infinite streams, Function and composition, sorting with streams and custom comparators, how sorted() works internally, reduce and its three forms, filter, Collectors.toList vs toList(), Stream.of, limit vs skip, list-to-map conversion with duplicates, iterate vs generate, and count vs sum vs reduce.
Published September 25, 2026
These questions look like API trivia, but each hides a trap: toMap throwing on duplicate keys, reduce with the wrong identity, sorted() buffering the whole stream, Stream.of(array) behaving differently for primitive arrays. Mention the trap, and you stand out.
Short answer: With Stream.iterate(seed, next) (each element is derived from the previous one), or Stream.generate(supplier) (independent values). Always bound them, with limit, takeWhile or a short-circuiting terminal operation.
Stream.iterate(1, n -> n * 2).limit(10).toList(); // 1, 2, 4 … 512
Stream.iterate(LocalDate.of(2026, 1, 1), d -> d.isBefore(end), d -> d.plusDays(1)) // Java 9: bounded iterate
.forEach(this::generateDailyReport);
Stream.generate(UUID::randomUUID).limit(5).toList();
IntStream.iterate(0, i -> i + 3).takeWhile(i -> i < 20).sum();
Function interface, and how is it used?Short answer: Function<T, R> represents a transformation, R apply(T t). It's what map takes. It composes: f.andThen(g) means apply f, then g, and f.compose(g) means apply g, then f. Function.identity() returns its input unchanged.
Function<String, String> trim = String::strip;
Function<String, String> normalise = trim.andThen(s -> s.toLowerCase(Locale.ROOT));
Map<String, User> byEmail = users.stream().collect(Collectors.toMap(u -> normalise.apply(u.email()), Function.identity()));
Key points to cover:
BiFunction<T,U,R>, UnaryOperator<T>, and primitive specialisations (ToIntFunction, IntFunction), which avoid boxing.Short answer: stream().sorted() for natural order, or sorted(comparator), then collect. The source isn't modified.
List<Product> cheapestFirst = products.stream()
.sorted(Comparator.comparing(Product::price).thenComparing(Product::name))
.toList();
List<String> desc = names.stream().sorted(Comparator.reverseOrder()).toList();
Short answer: Build the comparator with the Comparator factory methods, and pass it to sorted, max, min or collectors such as maxBy, or to a TreeMap supplier in toMap/groupingBy.
Comparator<Employee> bySeniority = Comparator
.comparing(Employee::level, Comparator.reverseOrder())
.thenComparing(Employee::joinDate)
.thenComparing(Employee::id); // tie-breaker for a deterministic order
Optional<Employee> mostSenior = employees.stream().min(bySeniority);
Map<String, Employee> topPerDept = employees.stream().collect(Collectors.toMap(
Employee::department, e -> e, BinaryOperator.minBy(bySeniority)));
Key points to cover:
Comparator.nullsFirst and nullsLast handle null keys. comparingInt and comparingLong avoid boxing.sorted() work internally, with natural ordering versus a comparator?Short answer: In both cases, sorted() is a stateful barrier. It buffers every upstream element (into an array or list), sorts the buffer once the upstream is exhausted, and only then pushes elements downstream. With natural ordering it uses Comparator.naturalOrder(), which requires the elements to be Comparable; otherwise you get a ClassCastException at runtime. With a custom comparator, it uses yours. Either way, the sort is TimSort, which is stable for ordered streams.
Key points to cover:
SORTED by natural order (for example, from a TreeSet spliterator), sorted() with no comparator is a no-op.sorted() followed by limit(k) still sorts everything. For the top-k of a huge stream, a bounded heap is cheaper.sorted() on an infinite stream never completes.reduce() used for?Short answer: reduce folds all the elements into a single value with an associative function. It has three forms:
reduce(identity, accumulator): returns T.reduce(accumulator): returns Optional<T> (the stream may be empty).reduce(identity, accumulator, combiner): for reducing to a different type, and for combining partial results in parallel.BigDecimal total = lines.stream().map(Line::amount).reduce(BigDecimal.ZERO, BigDecimal::add);
Optional<Order> largest = orders.stream().reduce((a, b) -> a.total().compareTo(b.total()) >= 0 ? a : b);
int totalChars = words.stream().reduce(0, (sum, w) -> sum + w.length(), Integer::sum); // String → int
Common trap: a non-neutral identity, such as reduce(10, Integer::sum). In a parallel stream, the identity is applied once per chunk, so the result changes with the number of chunks. For building collections, use collect: reduce is meant for immutable values.
filter() work?Short answer: filter(Predicate) is a lazy, stateless intermediate operation. As each element reaches it during the terminal operation's single pass, the predicate is evaluated. Elements that return true are passed downstream, and the rest are dropped immediately. It never buffers, and it preserves encounter order.
Collectors.toList()? How does it compare with Stream.toList()?Short answer: collect(Collectors.toList()) gathers the elements into a new List. In practice that's an ArrayList, but no guarantee is made about its type, mutability or thread-safety. Java 16's stream.toList() is shorter, and returns an unmodifiable list.
Collectors.toList() | Stream.toList() | Collectors.toUnmodifiableList() | |
|---|---|---|---|
| Mutable | In practice yes (not guaranteed) | No | No |
| Nulls allowed | Yes | Yes | No (NPE) |
| Available since | Java 8 | Java 16 | Java 10 |
Key points to cover:
Collectors.toCollection(ArrayList::new).Stream.of() work?Short answer: Stream.of(T... values) creates a sequential, ordered stream from its arguments. Stream.of(single) creates a one-element stream. Stream.ofNullable(x) (Java 9) creates an empty stream for null.
Common trap: Stream.of(intArray) gives a Stream<int[]> with one element, because a primitive array isn't a T[]. Use Arrays.stream(intArray) or IntStream.of(...). For an Integer[], Stream.of(arr) does stream the elements.
limit() and skip()?Short answer: limit(n) keeps at most the first n elements, and short-circuits: upstream stops producing after n. skip(n) discards the first n elements, and passes the rest. Together they paginate: skip(page * size).limit(size).
Key points to cover:
unordered() if the order doesn't matter.LIMIT/OFFSET, or keyset pagination), not in a stream over a full result set.Short answer: collect(Collectors.toMap(keyMapper, valueMapper)). Know its three traps:
IllegalStateException. Pass a merge function.null values throw an NPE (a HashMap.merge limitation).LinkedHashMap::new, TreeMap::new).Map<String, Employee> byEmail = employees.stream()
.collect(Collectors.toMap(Employee::email, Function.identity())); // throws on a duplicate email
Map<String, BigDecimal> revenueByCity = orders.stream()
.collect(Collectors.toMap(Order::city, Order::total, BigDecimal::add, TreeMap::new)); // merge + sorted keys
Map<String, List<Employee>> byDept = employees.stream().collect(Collectors.groupingBy(Employee::department));
Stream.iterate() and Stream.generate()?Short answer: iterate(seed, f) produces a sequence where each element depends on the previous one (seed, f(seed), f(f(seed))…), and it's ordered. generate(supplier) calls the supplier independently for each element, and the stream is unordered. Use it for random values, UUIDs or constants.
Key points to cover:
iterate parallelises poorly, because each element depends on the one before. Java 9's three-argument iterate(seed, hasNext, next) adds a built-in stop condition, like a for loop.count(), sum() and reduce()?Short answer:
count() returns the number of elements, as a long. It's on every stream, and can skip executing the pipeline when the size is known.sum() adds up the elements. It's only on primitive streams (IntStream, LongStream, DoubleStream), with no boxing, and it returns 0 when empty.reduce() is the general fold, for any associative operation (sum, product, max, or merging objects).long n = orders.stream().filter(Order::isPaid).count();
int units = lines.stream().mapToInt(Line::quantity).sum();
BigDecimal revenue = orders.stream().map(Order::total).reduce(BigDecimal.ZERO, BigDecimal::add); // BigDecimal: reduce
Key points to cover:
IntStream.sum() can overflow silently. Use mapToLong, or Math.addExact in a reduce, for large totals.Q: Why is collect preferred over reduce for building a list?
A: reduce expects immutable values. Using it to add to an ArrayList either copies the list at every step (O(n²)), or mutates shared state (which breaks in parallel). collect is designed for mutable reduction, with a supplier, an accumulator and a combiner.
Q: How do you get a Map<Boolean, List<T>> splitting elements by a condition?
A: Collectors.partitioningBy(predicate), optionally with a downstream collector such as counting().
Q: What does takeWhile do differently from filter?
A: takeWhile stops at the first element that fails the predicate, which makes it short-circuiting and suited to sorted or infinite streams. filter checks every element.
Q: How do you create a stream from a Map?
A: Stream a view: map.entrySet().stream(), map.keySet().stream() or map.values().stream(). Entries give you both the key and the value, for example to sort by value with Map.Entry.comparingByValue().