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
java.util.function and their methods.Function<T, R> with R apply(T), Predicate<T> with boolean test(T), Consumer<T> with void accept(T) and Supplier<T> with T get(). They cover takes-and-returns, takes-and-tests, takes-only and returns-only.
Predicate<String> and Function<String, Boolean> can hold the same lambda. Which would you choose and why?The Predicate, because its name says it is a yes/no rule, its test method returns a plain boolean, and it comes with helpers for combining rules. The Function version boxes every answer into a Boolean object.
Function<int, int> not allowed?Generic type arguments must be reference types, and int is a primitive. I write Function<Integer, Integer>, and Java boxes and unboxes the values automatically. A later lesson in this module covers primitive versions such as IntUnaryOperator, which avoid that boxing.
When the logic needs several statements, such as an if chain for grades or a loop that checks each character of a phone number. A block lambda has braces and must return a value on every path when the interface returns something.
Consumer<T> and Supplier<T>?They are opposites. A Consumer takes a value and returns nothing, through accept, so it is used for side effects like printing or logging. A Supplier takes nothing and returns a value, through get, so it is used to make values on demand.
Supplier<Integer> and calls get() when sending and again when verifying. What goes wrong?Each get() runs the lambda again, so the verify step generates a new OTP that does not match the one sent. The fix is to call get() once, keep the value in a variable or in the session, and use that stored value in both places.
BiFunction and BinaryOperator?BiFunction<T, U, R> takes two inputs of any types and returns any type. BinaryOperator<T> is the special case where both inputs and the result share one type; it extends BiFunction<T, T, T>. I use the operator when it fits because the name says "combines two of the same kind".
BiSupplier?The "Bi" prefix means two inputs, and a supplier takes no input at all, so there is nothing to double. If I need to return two values, I return one object that holds both, such as a record.
Lesson: BiFunction, UnaryOperator and BinaryOperatorIntPredicate when Predicate<Integer> already exists?Generics work only with objects, so Predicate<Integer> has to box every int into an Integer. IntPredicate takes a plain int, so there is no object creation, less memory use and no risk of a null value. It matters in large loops and number-heavy code.
IntUnaryOperator called applyAsInt and not apply?The As... names mark a primitive result and keep the methods apart. A class could implement both IntUnaryOperator and IntToDoubleFunction, and two methods named apply(int) with different return types could not live in one class.
CharPredicate or FloatUnaryOperator interfaces?Java provides primitive versions only for int, long and double (plus BooleanSupplier), to keep the package small. Smaller types widen automatically: a char works with IntPredicate and a float with DoubleUnaryOperator.
UnaryOperator<String> c = trim.andThen(upper); fail to compile?andThen is inherited from Function and its return type is Function<T, V>, not UnaryOperator<T>. I store the result in a Function<String, String> variable, which behaves the same.
Consumer.andThen do if the first consumer throws an exception?The exception goes up to the caller and the second consumer does not run. Both consumers get the same input, in a fixed order, so I put the step that must happen first, like saving a payment, before the optional one, like sending an SMS.
Lesson: Combine functions and predicatesYes. A field lives in an object on the heap, and the lambda reaches it through the captured this reference, which itself never changes. So the lambda is changing the object, not a copied local value.
for loop inside a lambda?Declare a new variable inside the loop body, like int n = i;, and use n in the lambda. n is a fresh, effectively final variable on every pass, so each lambda gets its own value, while i keeps changing.
Comparator has compare, equals, reversed, thenComparing and many static methods. Is it a functional interface?Yes. reversed and thenComparing are default methods and comparing or naturalOrder are static, so they do not count. equals(Object) is abstract but matches a public method of Object, so it does not count either. Only compare is left, so a lambda supplies just that.
Predicate.and?I add a default method that returns a new instance of the same interface as a lambda, calling this and the other instance, for example default DiscountRule then(DiscountRule next) { return a -> next.apply(this.apply(a)); }. Static factory methods like flat(50) make the chains read well.
I keep each rule as a named Predicate<Form> in a LinkedHashMap from message to rule. A validator runs every rule and collects the messages of those that fail. Rules stay small, testable and reusable, and adding a rule is one line.
They all captured one shared mutable thing, usually an int[] counter or a field, and read it later when it held its final value. I copy the current value into a new local variable inside the loop and capture that, so each lambda gets its own.
Static references like Integer::parseInt, bound instance references on a known object like System.out::println, unbound instance references where the object comes in as the first argument like String::length, and constructor references like ArrayList::new.
String::length the same object type as s -> s.length()?Both become an object of whatever functional interface the target asks for, such as Function<String, Integer> or ToIntFunction<String>. Neither has a type until the target decides it, and both behave the same when called.
Function<Integer, String> f = Integer::toString; fail to compile?Integer has a static toString(int) and an instance toString(). With an Integer input Java could call either Integer.toString(x) or x.toString(), so the reference is ambiguous. A lambda, String::valueOf or Object::toString fixes it.
It is object::method, where the object on the left is fixed when the reference is created, like System.out::println or validator::isValid. Every input of the functional interface is passed to the method as an argument.
A NullPointerException is thrown immediately, on the line that creates the reference, because Java inserts a null check there. A lambda like () -> name.length() would be created without error and fail only when it runs.
It is ClassName::instanceMethod, like String::length or Student::marks. No object is fixed: the first input of the functional interface becomes the object, and any further inputs become the arguments. So String::length is s -> s.length().
Comparator.comparing(Student::getCgpa).reversed() compile when the lambda version does not?With a lambda, .reversed() removes the target type, so Java cannot tell what s is and uses Object. The method reference names the class Student itself, which gives Java the type it needs, so the chain compiles.
It is ClassName::new, a short form of a lambda that only calls a constructor, like ArrayList::new for () -> new ArrayList<>(). It is useful wherever code must create objects without deciding the class itself.
Supplier built from a constructor reference the same as a bound reference to one object?No. A bound reference keeps one existing object and reuses it. A constructor reference creates nothing up front; each get() builds a brand new object.
I use a reference when the lambda only passes its parameters into one method and the method name says what happens, like String::isBlank or Student::marks. If there is a condition, a fixed argument, two steps or swapped order, I keep the lambda, or move a longer rule into a named method and reference that.
Swapped argument order with two-input unbound references like String::contains, constructor references that silently take a key, like computeIfAbsent(id, ArrayList::new), and references to vaguely named methods that hide the logic. The compile errors, like brackets after ::, the compiler already catches.
First I check the body is one call that passes the parameters straight through in order; otherwise it stays a lambda. Then I look at what is in front of the method: a class with a static method gives Class::method, an existing object gives obj::method, the first parameter gives Class::instanceMethod, and new gives Class::new.
Swapped inputs with two-input unbound references like String::contains or String::startsWith, computeIfAbsent(intKey, ArrayList::new) that uses the key as a capacity, and using a method of the right shape but wrong meaning, like Integer::sum as a comparator. Tests and careful reading catch these, the compiler does not.
A collection stores values in memory, lets me add and remove them, and can be read many times. A stream stores nothing, has no index, never changes its source and can be walked through only once.
Lesson: What is a stream?java.util.stream.Stream related to InputStream and OutputStream?No, only the name is shared. InputStream and OutputStream in java.io move bytes to and from files or the network. java.util.stream.Stream is a pipeline of values for processing data in memory.
Map have no stream() method, and how do you stream a map?Map does not extend Collection, so it does not get stream(). I stream one of its views: entrySet().stream() for key-value pairs, keySet().stream() for keys or values().stream() for values.
Yes, it follows the source. A List gives index order, a TreeSet or TreeMap gives sorted order and a LinkedHashMap gives insertion order. A HashSet or HashMap promises no order, so I never print a result that depends on it.
An intermediate operation returns a new Stream, so more steps can follow, and it does no work on its own. A terminal operation returns a result or void, starts the processing, and closes the stream.
filter return a new stream instead of changing the one it was called on?Each step adds a stage to the pipeline, and the stream never changes its source data. Returning a new stream lets the steps be chained with dots and keeps every step free of side effects on the original collection.
Lesson: Source, intermediate and terminal operationsfilter and map?One element at a time, through the whole pipeline: the first element is filtered, mapped and consumed before the second is read. It is like one loop with every step in its body, so no middle lists are built.
Lesson: Lazy evaluation and element flowNo. Stateful operations like sorted must collect every element before they pass any on, because the first sorted element is not known until all have arrived. Around such a step the trace shows all earlier steps finishing first.
It is an IllegalStateException thrown when a stream object is used after a terminal operation or after another step was already chained on it. I fix it by building a new stream for each use, for example calling list.stream() again or using a Supplier<Stream<T>>.
ConcurrentModificationException in stream code? Does it need threads?It happens when the source collection is changed while the stream is reading it, for example list.add inside a forEach lambda on list.stream(). No threads are needed; one lambda changing its own source is enough.
When I need the index or the previous element, when a running value like a balance decides when to stop, when a method throws a checked exception, or when the stream lambdas would become long blocks of logic.
Lesson: Loops versus streamsI name the source, turn each skipping if into filter, each computed value into map, and the thing the loop builds into the terminal operation: toList(), count() or mapToInt(...).sum(). A counter with break becomes limit, and a break at the first match becomes findFirst or anyMatch.
In list.stream().filter(test).findFirst(), filter does nothing when called. When findFirst runs, elements are pulled one at a time, and as soon as one passes the filter the stream stops, so the rest of the list is never read.
A stream never changes its source; it builds a new result. If a lambda adds to or removes from the source list while the stream reads it, Java throws ConcurrentModificationException, so I build the change separately and apply it after the stream.
&&?They give the same result, and the speed is almost the same because the data is still walked once. I chain filters when each one is a separate business rule, and I combine them when the condition is short and belongs together, like a range check.
Lesson: Keep elements with filterNullPointerException on production data. What do you check?Either the list holds null elements, or a field used in the predicate is null. I add .filter(Objects::nonNull) as the first step for null elements, and for null fields I compare with the constant first, like "FAILED".equals(t.status()), or check != null on the left of &&.
map change the type of the stream?Yes. map takes a Function<T, R>, so a Stream<T> becomes a Stream<R>. For example students.stream().map(Student::name) is a Stream<String>, and later steps see only Strings.
map and nothing appeared. Why?Streams are lazy, so map runs only when a terminal operation pulls elements. And since Java 9, count() may skip map completely when the source size is known, because mapping cannot change the count. Printing belongs in forEach, not in map.
flatMap?Whenever each element holds several values I want to treat as one sequence: a list of lists, orders with items, students with subjects, or lines that I split into words. The sign is that my function would return a list, an array or a stream for each element.
Lesson: Flatten nested data with flatMapOne flatMap removes one level of nesting, so I chain two: for example departments to courses with flatMap(d -> d.courses().stream()), then courses to students with flatMap(c -> c.students().stream()).
distinct not remove duplicate objects of my class?Most likely the class does not override equals and hashCode, so objects are compared by identity and every object is unique. Overriding both, based on the fields that define sameness, or making the class a record, fixes it.
sorted(Comparator.comparing(Student::marks).reversed().thenComparing(Student::name)). The reversed() must come right after the marks key; if it came after thenComparing, it would flip the names as well.
limit and skip do?limit(n) passes on at most the first n elements and then stops the stream; skip(n) drops the first n elements and passes on the rest. Both work on positions, keep the order, and do not fail when the stream is shorter than n.
limit called short-circuiting?Because it can end the stream before the source is used up. Once it has passed n elements, earlier steps are not asked for more, so a filter over a huge log stops as soon as enough matches are found.
dropWhile return when the very first element fails the test?The whole stream. dropWhile only drops a front part where the test is true; if the first element fails, that front part is empty, so nothing is dropped.
log.stream().dropWhile(line -> !line.startsWith("ERROR")). It drops the lines before the first error and keeps the error line and everything after it. The same predicate with takeWhile gives the lines before the error.
peek and forEach?peek is intermediate and returns a stream, so more steps can follow, and it runs only if and when a terminal operation pulls elements. forEach is terminal: it ends the pipeline and is guaranteed to visit every element that reaches it.
peek change the elements of a stream?It cannot replace them, because its Consumer returns nothing. It could call a setter on a mutable object, but that is a hidden side effect that may not run for every element. To produce changed values I use map.
Each lesson has quizzes, practice and more interview questions on the same topic.
Open the first lesson