1. What will be printed by the following stream pipeline?
List<String> list = List.of("apple", "banana", "cherry");
list.stream()
.filter(s -> s.startsWith("a"))
.peek(System.out::print);
Output: nothing is printed.
The whole point of this question is that Java streams are lazy. Intermediate operations — filter, peek, map, distinct — do not actually do any work when you chain them. They just build a description of what should happen. The pipeline only starts running when a terminal operation is reached.
Here, the pipeline has filter and peek, both intermediate operations, and then… nothing. There is no collect(), no forEach(), no count(), no terminal operation at all. So the JVM never executes anything. peek, despite its name, never runs, and System.out::print is never called.
This is by design. Laziness lets Java build efficient pipelines — it can skip work, short-circuit, and process only the elements that are actually needed, rather than eagerly running every stage on every element.
The fix would be to add a terminal operation, for example:
list.stream()
.filter(s -> s.startsWith("a"))
.peek(System.out::print)
.count();
The interview lesson is simple: intermediate operations alone produce no output. A stream pipeline does nothing until a terminal operation kicks it into gear.
Answer:
nothing is printed.
The whole point of this question is that Java streams are lazy. Intermediate operations — filter, peek, map, distinct — do not actually do any work when you chain them. They just build a description of what should happen. The pipeline only starts running when a terminal operation is reached.
Here, the pipeline has filter and peek, both intermediate operations, and then… nothing. There is no collect(), no forEach(), no count(), no terminal operation at all. So the JVM never executes anything. peek, despite its name, never runs, and System.out::print is never called.
This is by design. Laziness lets Java build efficient pipelines — it can skip work, short-circuit, and process only the elements that are actually needed, rather than eagerly running every stage on every element.
The fix would be to add a terminal operation, for example:
list.stream()
.filter(s -> s.startsWith("a"))
.peek(System.out::print)
.count();
The interview lesson is simple: intermediate operations alone produce no output. A stream pipeline does nothing until a terminal operation kicks it into gear.
2. What is the purpose of the Producer-Extends, Consumer-Super (PECS) rule in Java generics?
Answer: PECS tells you which wildcard to use: if you only read items from a collection, use ? extends T (producer); if you only write items into a collection, use ? super T (consumer).
The rule exists because wildcards trade away capability in one direction to gain flexibility in another, and using the wrong one causes compile errors that confuse everyone.
? extends T means “some unknown subtype of T.” You can safely read from such a collection — anything you get out is at least a T. But you cannot add to it, because the compiler does not know the exact element type. It could be an Integer list or a Double list, so putting a Number in is not guaranteed safe. That is the “Producer extends” half: if the collection produces values for you to read, use ? extends.
? super T means “some unknown supertype of T.” Here the logic flips. You can safely write a T into it, because whatever the list actually holds is guaranteed to accept a T — a List<Object> certainly accepts an Integer. But you cannot confidently read items as T, because the element could be any supertype. That is the “Consumer super” half: if the collection consumes values you write, use ? super.
A memorable framing: PECS = Producer Extends, Consumer Super. Producers give you things (so they extend), consumers take your things (so they super). Choose the wildcard based on whether you are reading or writing, and the compiler will stop fighting you.
Answer:
PECS tells you which wildcard to use: if you only read items from a collection, use ? extends T (producer); if you only write items into a collection, use ? super T (consumer).
The rule exists because wildcards trade away capability in one direction to gain flexibility in another, and using the wrong one causes compile errors that confuse everyone.
? extends T means “some unknown subtype of T.” You can safely read from such a collection — anything you get out is at least a T. But you cannot add to it, because the compiler does not know the exact element type. It could be an Integer list or a Double list, so putting a Number in is not guaranteed safe. That is the “Producer extends” half: if the collection produces values for you to read, use ? extends.
? super T means “some unknown supertype of T.” Here the logic flips. You can safely write a T into it, because whatever the list actually holds is guaranteed to accept a T — a List<Object> certainly accepts an Integer. But you cannot confidently read items as T, because the element could be any supertype. That is the “Consumer super” half: if the collection consumes values you write, use ? super.
A memorable framing: PECS = Producer Extends, Consumer Super. Producers give you things (so they extend), consumers take your things (so they super). Choose the wildcard based on whether you are reading or writing, and the compiler will stop fighting you.
3. What is the key functional difference between map and flatMap in Streams?
Answer: map transforms each element into exactly one result — a 1:1 mapping. flatMap transforms each element into a stream and then flattens all those streams into a single stream.
Both are intermediate operations that take a function. The difference is in what that function returns.
map takes a function that returns one value per input. Stream.of("a", "bb").map(s -> s.length()) gives [1, 2] — one output per input, input size preserved. The output is a Stream of those results.
flatMap takes a function that returns a Stream for each element, and then concatenates all those streams. Consider List.of("ab", "cd").stream().flatMap(s -> s.chars().mapToObj(c -> (char) c)). Each input string expands into a stream of its characters, and flatMap flattens the result into one stream of four characters.
Where does this matter in practice? The classic case is a list of lists — say, a List<List<String>>. A map would leave you with a Stream<List<String>> (same nesting). A flatMap with list -> list.stream() collapses it into a Stream<String>, giving you one flat sequence.
The memory hook: map maps 1→1; flatMap flattens 1→N into a single stream.
Answer:
map transforms each element into exactly one result — a 1:1 mapping. flatMap transforms each element into a stream and then flattens all those streams into a single stream.
Both are intermediate operations that take a function. The difference is in what that function returns.
map takes a function that returns one value per input. Stream.of("a", "bb").map(s -> s.length()) gives [1, 2] — one output per input, input size preserved. The output is a Stream of those results.
flatMap takes a function that returns a Stream for each element, and then concatenates all those streams. Consider List.of("ab", "cd").stream().flatMap(s -> s.chars().mapToObj(c -> (char) c)). Each input string expands into a stream of its characters, and flatMap flattens the result into one stream of four characters.
Where does this matter in practice? The classic case is a list of lists — say, a List<List<String>>. A map would leave you with a Stream<List<String>> (same nesting). A flatMap with list -> list.stream() collapses it into a Stream<String>, giving you one flat sequence.
The memory hook: map maps 1→1; flatMap flattens 1→N into a single stream.
4. What is the value printed by this reduction stream operation?
int total = Stream.of(1, 2, 3, 4)
.reduce(0, (a, b) -> a + b);
System.out.println(total);
Output: 10
reduce combines all elements of a stream into a single value. The two-argument form takes an identity and a combiner.
The identity is the starting value and the neutral element for the operation. For addition that’s 0. The combiner is a binary function; here (a, b) -> a + b adds two values.
Reduction proceeds by folding: start with 0, combine with 1 → 1; combine with 2 → 3; combine with 3 → 6; combine with 4 → 10. The result is 10.
Two details matter for interviews. First, because an identity is provided, the result is a plain int — no Optional wrapper, no “no elements” case. (The Optional-returning reduce form only exists for the no-identity overload, where an empty stream has no answer.) Second, the identity is a genuine initial value, not an offset — with an identity of 0 and a + combiner, the result is simply the sum.
Answer:
10
reduce combines all elements of a stream into a single value. The two-argument form takes an identity and a combiner.
The identity is the starting value and the neutral element for the operation. For addition that’s 0. The combiner is a binary function; here (a, b) -> a + b adds two values.
Reduction proceeds by folding: start with 0, combine with 1 → 1; combine with 2 → 3; combine with 3 → 6; combine with 4 → 10. The result is 10.
Two details matter for interviews. First, because an identity is provided, the result is a plain int — no Optional wrapper, no “no elements” case. (The Optional-returning reduce form only exists for the no-identity overload, where an empty stream has no answer.) Second, the identity is a genuine initial value, not an offset — with an identity of 0 and a + combiner, the result is simply the sum.
5. What is the output of the following stream pipeline?
List<Integer> list = List.of(1, 2, 3);
list.stream()
.map(x -> x * 2)
.forEach(System.out::print);
Output: 246
Two operations: an intermediate map and a terminal forEach.
map(x -> x * 2) transforms each element: 1 → 2, 2 → 4, 3 → 6. The stream now holds [2, 4, 6].
forEach(System.out::print) applies print to each element. Note the method reference is print — which writes without spaces or newlines. So the output is the digits run together: 246.
If it had been println, each value would be on its own line. The answer is the unspaced concatenation 246.
Answer:
246
Two operations: an intermediate map and a terminal forEach.
map(x -> x * 2) transforms each element: 1 → 2, 2 → 4, 3 → 6. The stream now holds [2, 4, 6].
forEach(System.out::print) applies print to each element. Note the method reference is print — which writes without spaces or newlines. So the output is the digits run together: 246.
If it had been println, each value would be on its own line. The answer is the unspaced concatenation 246.
6. Which functional interface signature matches the Java 8 lambda s -> s.length()?
Answer: Function<String, Integer>.
A lambda matches a functional interface based on its shape — the parameter types and return type.
s -> s.length() takes one argument (s) and produces an integer result (s.length() returns int). So it’s a function that maps a String to an int. Among the standard functional interfaces:
Function<T, R>— takes oneT, returns oneR.Function<String, Integer>takes aStringand returns anInteger(theintis boxed). This matches.Consumer<T>— takes oneT, returnsvoid. The lambda returns a value, so no match.Supplier<T>— takes nothing, returns aT. The lambda takes an argument, so no match.Predicate<T>— takes oneT, returnsboolean. The lambda returns anint, so no match.
The answer is Function<String, Integer> — the mapping shape: one input, one output.
Answer:
Function<String, Integer>.
A lambda matches a functional interface based on its shape — the parameter types and return type.
s -> s.length() takes one argument (s) and produces an integer result (s.length() returns int). So it’s a function that maps a String to an int. Among the standard functional interfaces:
Function<T, R>— takes oneT, returns oneR.Function<String, Integer>takes aStringand returns anInteger(theintis boxed). This matches.Consumer<T>— takes oneT, returnsvoid. The lambda returns a value, so no match.Supplier<T>— takes nothing, returns aT. The lambda takes an argument, so no match.Predicate<T>— takes oneT, returnsboolean. The lambda returns anint, so no match.
The answer is Function<String, Integer> — the mapping shape: one input, one output.
Premium Content
Unlock Streams, Lambdas & Generics and all premium lessons with a subscription.
From ₹199.99/year — See plans