4.
Java 8 and Beyond
How do you decide when to use streams vs imperative loops in performance-
sensitive applications?
Use streams for readability and declarative code, but they may introduce overhead in
tight loops.
Prefer primitive streams (IntStream, LongStream) to avoid boxing in numeric pipelines.
Use imperative loops when fine-tuned control, early exit, or loop fusion is required.
Parallel streams can be dangerous without understanding underlying spliterators and
thread pools.
Profile performance impact using JMH or async-profiler before refactoring to streams in
hot paths.
What are the trade-offs of using Optional as a method return type in public
APIs?
Optional improves null safety and encourages better handling of absent values.
It adds slight memory and performance overhead compared to null in high-throughput
paths.
Should not be used for fields, parameters, or in collections to avoid nesting and boxing.
Clearly signals 'may be empty' semantics to the caller, improving API clarity.
Avoid returning Optional from getters or in latency-critical methods unless justified.
How does the default method feature in interfaces affect interface design and
binary compatibility?
Allows interfaces to evolve without breaking existing implementations.
Can lead to ambiguity in diamond inheritance scenarios, requiring careful conflict
resolution.
Encourages misuse when used to inject business logic instead of utility behavior.
Supports interface-based composition but complicates reasoning about method
resolution.
Must be tested across multiple versions to ensure compatibility in published libraries.
How do you ensure safe and efficient use of lambda expressions in concurrent
applications?
Avoid capturing mutable state inside lambdas used across threads to prevent race
conditions.
Use method references or stateless lambdas wherever possible to reduce allocation.
Be cautious with lazy evaluation in streams which may defer execution to unexpected
thread contexts.
Understand that lambdas are compiled to synthetic classes and may introduce GC
overhead in tight loops.
Use functional interfaces with care when threading context may be preserved (e.g., in
async callbacks).
What are the practical use cases for CompletableFuture and what are its
common pitfalls?
Useful for composing async workflows without blocking threads (e.g., async HTTP, DB
calls).
Supports non-blocking chaining, timeouts, and fallback logic via fluent APIs.
Avoid blocking methods like get()/join() unless necessary — they defeat the async
model.
Exception handling is non-trivial — must use exceptionally(), handle(), or
whenComplete() properly.
Threading model depends on default or custom Executor — unbounded fork-join pool
can cause saturation.
How does method reference resolution differ between overloaded and generic
methods in Java 8?
Compiler uses contextual type inference (target typing) to resolve ambiguous method
references.
Overloaded methods require explicit casting or lambda syntax when the compiler can't
disambiguate.
Generic methods may cause inference errors if type bounds or wildcards are not
satisfied.
IDE warnings may differ from actual compile-time behavior — testing with javac is
essential.
Be cautious when refactoring — changing a method signature can silently break method
references.
What optimizations does the JVM apply to lambda expressions at runtime?
Lambda expressions are implemented using invokedynamic and translated to
lightweight bytecode.
JVM reuses lambda classes across invocations unless state capture prevents it.
Method references are more efficient than capturing lambdas due to reduced object
creation.
HotSpot inlines lambdas aggressively when they are monomorphic and non-capturing.
Captured variables are stored in synthetic fields — affects GC and memory pressure in
tight loops.
How does Stream.collect() work internally and how would you implement a
custom collector?
collect() uses the Collector interface with supplier, accumulator, combiner, and finisher
components.
Built-in collectors handle mutable reduction (e.g., toList, toMap) efficiently for parallel
streams.
Custom collectors must ensure associativity and thread safety for parallel execution.
Design collectors to minimize contention and avoid unnecessary synchronization.
Implement finisher to convert intermediate result to final form (e.g., mutable →
immutable).
When should you use method references vs lambda expressions?
Method references improve readability and reduce boilerplate when existing methods
match target signature.
Lambdas offer more flexibility for inline expressions, especially when additional logic is
needed.
Method references can hide side effects — review carefully in stateful contexts.
Use lambdas when exception handling or control structures are required inside the
function body.
Inconsistent formatting or mixing both styles can reduce readability — enforce a team
convention.
What are the limitations of parallel streams and when are they inappropriate?
Parallel streams use the common fork-join pool which can interfere with other async
tasks.
Not suitable for IO-bound operations or pipelines with expensive setup per element.
Spliterators must support efficient splitting — otherwise parallelism degrades
performance.
Debugging is harder due to concurrency and lazy evaluation behaviors.
Prefer explicit thread pools or async frameworks for controlled concurrency and better
error handling.