Loading...
Loading...
Java interview guide
Lambdas, streams, Optional, records and the newer language features. Every answer is short enough to say aloud, and every question links to the lesson that teaches it.
60 questionsLast updated
mapMulti and when would you use it over flatMap?mapMulti, added in Java 16, gives each element plus a sink, and you push zero or more results with sink.accept. I use it when each element yields only a few results, often decided by an if, so I avoid building small streams. When I already have a collection per element, flatMap(x -> x.list().stream()) is simpler.
I check the step order: whether filters run before parsing, mapping and sorting, and whether limit comes right after sorted. Then I look for costly work, like formatting or object creation, done for rows that are later dropped. Only after that would I consider other tools such as parallel streams or doing the work in the database.
I would check whether limit(5) runs before sorted. In that case the pipeline takes the first five in list order and sorts only them, which looks right but is not the top five. Sorting first and then limiting fixes it.
map versus flatMap with a real example.For a list of orders, map(Order::total) gives one total per order, so the count stays the same. flatMap(o -> o.items().stream()) gives every item of every order as one stream, so the count can grow. If my function returns a list or a stream per element, I need flatMap.
count() return a long and not an int?A stream is not limited to a collection's size; it can come from a generator or a huge file and hold more than 2.1 billion elements. A long can hold any realistic count. If I need an int, I use Math.toIntExact, which throws instead of silently overflowing.
ArrayList inside forEach?It is a side effect: the stream changes something outside itself. On a parallel stream several threads call add at once on a list that is not thread-safe, so elements are lost or an exception is thrown. It also hides intent; filter(...).map(...).toList() says what the result is and is always safe.
min and max return an Optional?Because the stream can be empty, and then there is no answer. Returning null would invite a NullPointerException later. Optional makes the "maybe missing" case part of the type, so the caller handles it with orElse or ifPresent.
Two steps: find the top marks with max(comparingInt(Student::marks)), then filter(s -> s.marks() == top). A single max can only return one element.
They stop reading the stream as soon as the answer is known. anyMatch stops at the first match, allMatch at the first failure. Because streams are lazy, the earlier map or filter steps also stop, which saves real work on large data.
!orders.stream().anyMatch(o -> o.cancelled()) or noneMatch?orders.stream().noneMatch(o -> o.cancelled()). It does the same work but reads like the question, "no order is cancelled", and the reader does not need to spot the !.
Optional instead of null?There may be no matching element. An Optional makes that case visible in the type, so the caller handles it with orElse or ifPresentOrElse instead of forgetting a null check.
findFirst return the same element every time on a HashSet?A HashSet has no defined order, so "first" just means whatever the set's iteration gives. It may look stable, but it can change with different contents or Java versions. If I need a specific element, I sort first or use min with a comparator.
There is no error, just a wrong answer: the total comes out one too high. On a parallel stream each chunk starts from the identity, so the error grows with the number of chunks and can differ between machines.
Lesson: reduce with an identityreduce with an identity return on an empty stream?The identity itself, because the accumulator never runs. That is why this form returns a plain value and not an Optional: there is always an answer.
reduce return an Optional?It has no identity to fall back on, so an empty stream has no possible answer. The Optional makes the caller handle that case, the same reason min and max return one.
reduce and collect?reduce is for immutable results: numbers, Strings, records, or picking one element. collect is a mutable reduction: it fills containers like lists, maps or builders, giving each parallel chunk its own fresh container through a supplier.
toList() by default: it is shorter and the result cannot be changed by mistake. If the code must edit the list afterwards, I say so with Collectors.toCollection(ArrayList::new), rather than relying on the unpromised behaviour of Collectors.toList().
String[] from a stream?stream.toArray(String[]::new). The generator creates an array of the right type and size. Plain toArray() returns Object[], which cannot be cast to String[].
count gives 0 and toList an empty list. min, max, findFirst, findAny and the one-argument reduce give Optional.empty. reduce with an identity returns the identity. allMatch and noneMatch give true, anyMatch gives false.
First reject an empty batch, because allMatch is true on empty data. Then anyMatch or filter for marks outside 0 to 100, distinct().count() against the size to catch duplicate roll numbers, and count and max for the summary. Each check is one short pipeline over the same list.
A supplier that creates an empty container, an accumulator that adds one element, a combiner that joins two partial containers in a parallel stream, and a finisher that turns the container into the final result. The three-argument collect(ArrayList::new, ArrayList::add, ArrayList::addAll) shows the first three directly.
TreeSet whose comparator compares only string length?Strings of the same length are treated as duplicates, so only the first one of each length is kept. A TreeSet uses the comparator, not equals, to decide uniqueness, so the comparator must match what "same" means for the data.
toMap?The two-argument form throws IllegalStateException with a "Duplicate key" message. To allow repeats I pass a merge function as the third argument, which decides whether to keep the first value, the last value or a combination such as the sum.
toMap store null values?No. If the value function returns null, toMap throws NullPointerException, even though HashMap itself allows null values. I give a default inside the value function, filter those elements out, or use a plain loop with put if null really must be stored.
For one combined level I use a record key, record DeptCity(String dept, String city), and the classifier e -> new DeptCity(e.dept(), e.city()). A record works because its equals and hashCode use both fields. For two separate levels I can nest a second groupingBy inside the first, which the Sorted and nested grouping lesson covers.
The default result is a HashMap, which has no defined order. For display I copy it into a TreeMap, or pass TreeMap::new as the map factory to groupingBy so the map is sorted from the start.
counting() return Long and not Integer?A stream can have more elements than an int can count, so the API uses long for counts, the same as Stream.count(). That is why the map must be Map<K, Long>; if I need an Integer, I can use summingInt(x -> 1).
mapping do as a downstream collector?It applies a function to each element of the group and passes the result to another collector. For example groupingBy(Student::branch, mapping(Student::name, toList())) gives the names per branch instead of whole student objects.
summarizingInt give you, and why use it?It returns an IntSummaryStatistics with count, sum, min, max and average, all computed in a single pass. Without it, getting all five means several passes over the data or a hand-written loop with five variables.
collect call?teeing(minBy(comparingInt(Product::price)), maxBy(comparingInt(Product::price)), (lo, hi) -> new Range(lo.get(), hi.get())). If I only need the prices, not the products, summarizingInt(Product::price) gives min and max directly.
partitioningBy return when no element passes the test?A map that still has both keys. The true side is an empty list, or 0 with counting(), so code that reads get(true) never sees null.
Store the rule as a Predicate<Txn>, such as amount above a limit or a new device, then partitioningBy(rule, summingInt(Txn::amount)). Both totals are always present, so the report never has to check for a missing side.
flatMapping?When each element holds several values and I want all of them per group, such as all skills of the members of each team. mapping would give a list of lists; flatMapping(m -> m.skills().stream(), toSet()) gives one flat set per team.
groupingBy(Student::branch, mapping(Student::name, toCollection(TreeSet::new))) gives sorted, unique names. If duplicates must stay, I use collectingAndThen(mapping(Student::name, toList()), list -> list.stream().sorted().toList()), or sort the students before grouping, since each group keeps stream order.
Map.Entry.comparingByValue().reversed() not compile inside sorted?Java infers generic types from the target, but .reversed() means comparingByValue() is typed on its own first, so its key type becomes Object. I either pass Comparator.reverseOrder() into comparingByValue, or add a type witness: Map.Entry.<String, Integer>comparingByValue().reversed().
Nested grouping when the report is read level by level, like a city heading with categories under it. A record key such as CityCategory when I mostly look up single combinations or need more than two levels, because a flat map with a clear key is easier to read and test.
toMap works in tests but fails in production with a duplicate key error. What do you do?First I check whether duplicates are a data error or normal. If they are an error, I fix the data or report it clearly. If they are normal, I add a merge function that states the rule, such as keep the latest or add the amounts, or I switch to groupingBy if I need all values per key.
null data?groupingBy rejects null keys and toMap rejects null values, both with NullPointerException. Before collecting I either filter those elements out or replace the blank with a meaningful default such as "UNKNOWN", depending on whether the report should show them.
map and mapToInt?map returns a Stream<R> of objects, so map(Student::marks) gives a Stream<Integer>. mapToInt returns an IntStream of plain int values, which has sum() and average() and avoids boxing.
LongStream or mapToLong over IntStream?When the total can pass about 2.1 billion, for example money in paise, file sizes or view counts. IntStream.sum() would overflow silently and give a wrong, often negative, number.
A list stream does not give positions, so I stream the indexes instead with IntStream.range(0, list.size()) and read list.get(i) inside the lambda. For arrays I use array[i] the same way.
I stream IntStream.range(1, arr.length) and compare arr[i] with arr[i - 1] inside filter or map. Starting at 1 means i - 1 is never negative.
filter not stop an infinite stream, while takeWhile does?filter checks every value and cannot know that no later value will pass, so it keeps going. takeWhile ends the stream at the first value that fails, which is right when the values only grow or only shrink.
It is any change a lambda makes outside itself, like adding to an outside list, updating a counter or putting into a map. Streams expect lambdas that only turn an input into an output, so side effects in the middle of a pipeline are a design smell.
Lesson: Side effects and why they break streamsCollections.synchronizedList fix that code?It stops the lost items and the crash, but every thread now waits for a lock, so the parallel work becomes slower, and the order of the items is still random. Using collect or toList() is both correct and keeps the original order.
The source's Spliterator splits the data into chunks, and the chunks run on threads of the common ForkJoinPool, with the calling thread helping. Each chunk produces a partial result, and the partial results are joined in pairs until one is left. This is the fork/join model.
forEach and forEachOrdered on a parallel stream?forEach runs the action as soon as any thread reaches an element, so the order is random. forEachOrdered runs the actions in encounter order, which is correct for printing a list in order but gives up part of the parallel speed.
When the data is large, each element needs real CPU work, the merge step is cheap like a sum or a max, and the source splits well, like an array, an ArrayList or an IntStream.range. Even then I confirm it with a measurement.
The JIT compiler speeds code up after the first runs, garbage collection can pause the program, and other programs and the CPU's changing speed add noise. One run might show parallel winning or losing by chance.
Lesson: When parallel streams help and hurtFor small, simple work a loop can be slightly faster, because a stream builds a pipeline and calls lambdas, and boxed elements add cost. The gap is usually tiny compared with any I/O. I choose by readability and measure only when there is a real performance problem.
Lesson: Stream pitfalls, readability and performanceOne step per line, method references or named methods instead of long lambdas, and named predicates for business rules. If the pipeline does two jobs, I split it and keep the middle result in a well-named list, never in a stream variable.
Lesson: Stream pitfalls, readability and performanceI would call list.stream().mapToInt(Integer::intValue).summaryStatistics(). The IntSummaryStatistics object gives count, sum, min, max and average together, so the list is read only once.
A running wallet balance that stops at the first overdraft is a loop. Names of students above 90 is a sequential stream. Counting primes up to several million over an IntStream.range is a good parallel candidate, after a measurement confirms the gain.
A null check works only if the caller remembers it, and a String return type gives no hint that null can come back. With Optional<String>, the possibility of "no value" is in the type, so the compiler stops the caller from using the result as a plain String.
No. It only helps where it is used, mainly on return values of lookup methods. Fields, parameters, and values from old libraries can still be null, and Optional.of(null) itself throws an NPE. It makes the missing case visible; it does not change how null works.
get() on an empty Optional?It throws NoSuchElementException with the message "No value present". So get() without a check simply replaces a NullPointerException with a different crash.
return inside the lambda of ifPresent to return a value from the outer method?No. A return inside the lambda only ends the lambda, and a Consumer cannot return a value anyway. If you need to return something based on the Optional, use the methods from the next lessons, such as map and orElse, instead of ifPresent.
orElse(loadFromDb()) call the database even when the value is present?Because Java evaluates all method arguments before calling the method. loadFromDb() runs first to produce the argument, and only then does orElse look inside the Optional and decide to ignore that argument.
I would write userRepository.findById(id).orElseThrow(() -> new UserNotFoundException(id)). The service layer throws a specific exception with a clear message, and the web layer maps that exception to a 404 response.
map returns null?map returns an empty Optional. Internally it wraps the result with Optional.ofNullable, so a null result simply means "no value" for the rest of the chain.
Each lesson has quizzes, practice and more interview questions on the same topic.
Open the first lesson