Java 8 Interview Preparation
A. Introduction to Java 8 Java 8 is one of the biggest releases in Java history because it introduced Functional Programming concepts into Java. Major Features Lambda ExpressionsFunctional…
A. Introduction to Java 8
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
Java 8 is one of the biggest releases in Java history because it introduced Functional Programming concepts into Java.
Major Features
- Lambda Expressions
- Functional Interfaces
- Stream API
- Method References
- Default Methods
- Static Methods in Interfaces
- Optional Class
- New Date & Time API (java.time)
- CompletableFuture
- Nashorn JavaScript Engine (deprecated later)
Interview One-Liner: Java 8 introduced functional programming features like Lambda Expressions and Stream API, making Java code shorter, more readable, and easier to process collections.
Real Project Usage
In my projects, I mainly use:
- Lambda Expressions
- Stream API
- Optional
- Method References
- Date & Time API
- CompletableFuture for asynchronous processing
Before Java 8, Java was mainly Object-Oriented.
Java 8 introduced Functional Programming concepts, allowing developers to write cleaner, concise, and more expressive code.
Improvements
Before Java 8
- Large anonymous classes
- More boilerplate code
- Manual iteration
- Difficult parallel processing
After Java 8
- Lambda expressions
- Streams
- Functional interfaces
- Parallel Streams
- Better Date API
Interview One-Liner: Java 8 revolutionized Java by introducing functional programming concepts that significantly reduced boilerplate code and improved collection processing.
| Java 7 | Java 8 |
|---|---|
| More boilerplate code | Less code using Lambdas |
| Manual loops | Stream API |
| Anonymous classes | Lambda Expressions |
| Old Date API | java.time API |
| Null checks everywhere | Optional |
| Sequential processing | Parallel Streams |
| Limited asynchronous support | CompletableFuture |
Java 8 improves:
- Readability
- Performance
- Productivity
- Parallel Processing
- Functional Programming
- Better Date Handling
In my Spring Boot microservices projects, I frequently use:
- Lambda Expressions
- Stream API
- Optional
- Method References
- Comparator using Lambda
- Collectors
- Date & Time API
- CompletableFuture for asynchronous tasks
Example
List<String> names = users.stream
.map(User::getName)
.collect(Collectors.toList);
Interview Tip: Mention a real-world scenario:
We use Streams for filtering and transforming collections, Optional to avoid NullPointerException, Lambdas to reduce boilerplate code, and CompletableFuture for asynchronous API calls.
B. Lambda Expressions
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
A Lambda Expression is an anonymous function introduced in Java 8.
It allows us to write implementations of functional interfaces without creating anonymous classes.
Syntax
(parameters) -> expression
or
(parameters) -> {
statements;
}
Example
Before Java 8
Runnable r = new Runnable {
@Override
public void run {
System.out.println("Running");
}
};
After Java 8
Runnable r = -> System.out.println("Running");
Advantages
- Less code
- Better readability
- Functional programming
- Easier collection processing
Interview One-Liner: Lambda Expressions are anonymous functions used to provide implementations for functional interfaces with much less code.
Before Java 8, implementing interfaces required anonymous classes, which added unnecessary boilerplate.
Lambda Expressions reduce this complexity and make the code more concise.
Example
Without Lambda
Collections.sort(list, new Comparator<String> {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
With Lambda
Collections.sort(list, (a, b) -> a.compareTo(b));
Benefits
- Cleaner code
- Less boilerplate
- Improved readability
- Easier maintenance
- Better support for Stream API
Lambda Expressions simplify the implementation of functional interfaces, reduce boilerplate code, and improve code readability.
General Syntax
(parameters) -> expression
No Parameters
-> System.out.println("Hello");
One Parameter
x -> x * x
Multiple Parameters
(a, b) -> a + b
Multiple Statements
(a, b) -> {
int sum = a + b;
return sum;
};
Interview Tip: The arrow (->) separates the parameter list from the Lambda body.
A Lambda Expression can only be used with a Functional Interface.
A Functional Interface is an interface that contains exactly one abstract method.
Example
@FunctionalInterface
interface Calculator {
int add(int a, int b);
}
Lambda Implementation
Calculator c = (a, b) -> a + b;
Common Functional Interfaces
- Predicate
- Function
- Consumer
- Supplier
- Comparator
- Runnable
- Callable
Interview One-Liner: Lambda Expressions provide implementations of Functional Interfaces.
| Lambda | Anonymous Class |
|---|---|
| Less code | More code |
| No class creation | Creates anonymous class |
| Better readability | Verbose |
| Uses Functional Interface | Can implement any interface or abstract class |
| Cannot define instance variables | Can define instance variables |
Example
Anonymous Class
Runnable r = new Runnable {
@Override
public void run {
System.out.println("Hello");
}
};
Lambda
Runnable r = -> System.out.println("Hello");
Interview Question
Which performs better?
Generally, Lambdas are more efficient because they don’t create separate anonymous class instances in the same way anonymous classes do, though the JVM optimizes both.
Yes.
Lambda can access:
- Local variables
- Instance variables
- Static variables
However, local variables must be effectively final.
Example
int num = 10;
Runnable r = -> System.out.println(num);
Valid because num is not modified.
An effectively final variable is a local variable whose value is assigned only once and is never modified afterward. It does not need to be explicitly declared with the final keyword.
Example
int x = 10;
Runnable r = -> System.out.println(x);
Valid.
Invalid Example
int x = 10;
Runnable r = -> System.out.println(x);
x++;
This causes a compile-time error because x is no longer effectively final.
Why?
Local variables are stored on the stack, while Lambdas may execute later. Restricting captured local variables to effectively final values prevents inconsistencies and ensures thread safety.
Interview One-Liner: Local variables used inside a Lambda must be final or effectively final because Lambdas capture their value, not the variable itself.
Method References provide a shorter and cleaner way to refer to an existing method when a Lambda simply calls that method.
Syntax
ClassName::methodName
Example
Lambda
list.forEach(name -> System.out.println(name));
Method Reference
list.forEach(System.out::println);
Types of Method References
1. Static Method
Math::abs
2. Instance Method of a Particular Object
printer::print
3. Instance Method of an Arbitrary Object
String::toUpperCase
Interview One-Liner: Method References make the code more readable by replacing simple Lambdas that only invoke an existing method.
Constructor References allow us to refer to a constructor using the ::new syntax instead of writing a Lambda.
Example
Lambda
Supplier<List<String>> list = -> new ArrayList<>;
Constructor Reference
Supplier<List<String>> list = ArrayList::new;
Common Syntax
ClassName::new
Interview One-Liner: Constructor References are a shorthand for creating new objects through a constructor using ClassName::new.
Lambda Expressions are widely used in Java applications, especially with collections and asynchronous programming.
Common Use Cases
- Sorting collections
employees.sort((e1, e2) -> e1.getName.compareTo(e2.getName));
- Filtering data
employees.stream
.filter(e -> e.getSalary > 50000)
.toList;
- Mapping objects
employees.stream
.map(Employee::getName)
.toList;
- Iterating collections
employees.forEach(System.out::println);
- Event handling (GUI applications)
- Implementing Runnable or Callable
new Thread( -> System.out.println("Task Running")).start;
- Using CompletableFuture
CompletableFuture.runAsync( -> processOrder);
Real Project Examples
In Spring Boot microservices, I commonly use Lambdas for:
- Processing collections with the Stream API.
- Sorting and filtering API responses.
- Converting Entities to DTOs using map.
- Iterating over collections with forEach.
- Writing asynchronous tasks with CompletableFuture.
Interview One-Liner: In real projects, Lambdas are mainly used with the Stream API for filtering, mapping, sorting, iteration, and asynchronous programming, resulting in cleaner and more maintainable code.
⭐ Quick Interview Revision
| Question | One-Line Answer |
|---|---|
| What is Java 8? | Java 8 introduced functional programming features like Lambdas and Streams. |
| Biggest feature? | Lambda Expressions and Stream API. |
| What is Lambda? | An anonymous function used to implement a functional interface. |
| Why Lambda? | To reduce boilerplate code and improve readability. |
| Functional Interface? | An interface with exactly one abstract method. |
| Lambda vs Anonymous Class? | Lambda is concise and intended for functional interfaces, while anonymous classes are more verbose and can extend abstract classes or implement multiple methods. |
| Effectively Final? | A local variable whose value is assigned only once and never modified. |
| Method Reference? | A shorthand for a Lambda that simply calls an existing method. |
| Constructor Reference? | A shorthand for object creation using ClassName::new. |
| Project Usage? | Streams, Lambdas, Optional, Method References, Date & Time API, and CompletableFuture. |
C. Functional Interfaces
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
A Functional Interface is an interface that contains exactly one abstract method. It can have multiple default and static methods, but only one abstract method.
Functional Interfaces are mainly used to support Lambda Expressions and Method References.
Example
@FunctionalInterface
interface Calculator {
int add(int a, int b);
}
Using Lambda:
Calculator cal = (a, b) -> a + b;
Interview One-Liner: A Functional Interface is an interface with exactly one abstract method and is used as the target type for Lambda Expressions.
@FunctionalInterface is an annotation introduced in Java 8 that tells the compiler that the interface must have only one abstract method.
If another abstract method is added, the compiler throws an error.
Example
@FunctionalInterface
interface Demo {
void display;
default void show {
System.out.println("Default");
}
}
Invalid Example
@FunctionalInterface
interface Demo {
void show;
void display; // Compile-time Error
}
Advantages
- Compile-time validation
- Improves readability
- Prevents accidental addition of abstract methods
Interview One-Liner: @FunctionalInterface ensures that an interface contains only one abstract method.
Java 8 provides several built-in Functional Interfaces in the java.util.function package.
| Interface | Purpose |
|---|---|
| Predicate | Test a condition |
| Function | Transform one object into another |
| Consumer | Consume data without returning anything |
| Supplier | Supply data without taking input |
| UnaryOperator | Same input and output type |
| BinaryOperator | Operate on two values of the same type |
| BiFunction | Takes two inputs and returns one output |
Interview Tip: These are heavily used in the Stream API.
A Predicate represents a condition or boolean test.
It accepts one input and returns either true or false.
Method
boolean test(T t)
Example
Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(10));
Output
true
Common Uses
- Filtering collections
- Validation
- Conditions
Interview One-Liner: Predicate is used to test a condition and always returns a boolean value.
A Function accepts one input and returns one output.
It is mainly used for data transformation.
Method
R apply(T t)
Example
Function<String, Integer> length = str -> str.length;
System.out.println(length.apply("Java"));
Output
4
Common Uses
- DTO conversion
- Data transformation
- Mapping in Streams
Interview One-Liner: Function transforms one object into another.
A Consumer accepts an input but does not return any value.
Method
void accept(T t)
Example
Consumer<String> print = name -> System.out.println(name);
print.accept("Prakash");
Common Uses
- Logging
- Printing
- Saving data
- forEach
Interview One-Liner: Consumer takes input, performs an operation, and returns nothing.
A Supplier does not take any input but returns a value.
Method
T get
Example
Supplier<String> supplier = -> "Hello";
System.out.println(supplier.get);
Common Uses
- Lazy object creation
- Configuration values
- Random data generation
Interview One-Liner: Supplier provides data without taking any input.
A UnaryOperator is a special type of Function where the input and output types are the same.
Method
T apply(T t)
Example
UnaryOperator<String> upper = str -> str.toUpperCase;
System.out.println(upper.apply("java"));
Output
JAVA
Interview One-Liner: UnaryOperator accepts one value and returns the same type.
A BinaryOperator accepts two inputs of the same type and returns the same type.
Method
T apply(T t1, T t2)
Example
BinaryOperator<Integer> add = (a, b) -> a + b;
System.out.println(add.apply(10, 20));
Output
30
Common Uses
- Sum
- Maximum
- Minimum
- Reduction operations
Interview One-Liner: BinaryOperator works on two values of the same type and returns the same type.
A BiFunction accepts two inputs (which may be of different types) and returns one output.
Method
R apply(T t, U u)
Example
BiFunction<String, Integer, String> result =
(name, age) -> name + " - " + age;
System.out.println(result.apply("John", 25));
Output
John - 25
Interview One-Liner: BiFunction accepts two inputs and returns one result.
You can create your own Functional Interface using the @FunctionalInterface annotation.
Example
@FunctionalInterface
interface Greeting {
void sayHello(String name);
}
Using Lambda
Greeting greet = name -> System.out.println("Hello " + name);
greet.sayHello("Prakash");
When to Use
Create a custom Functional Interface when the built-in interfaces do not match your business requirement.
Interview Tip: In most projects, built-in interfaces (Predicate, Function, Consumer, etc.) are sufficient. Custom Functional Interfaces are used for domain-specific operations.
| Predicate | Function |
|---|---|
| Tests a condition | Transforms data |
| Returns boolean | Returns any type |
| Method is test | Method is apply |
| Used in filter | Used in map |
| Mainly for validation | Mainly for conversion |
Example
Predicate
Predicate<Integer> even = n -> n % 2 == 0;
Function
Function<String, Integer> length = String::length;
Interview One-Liner: Predicate checks a condition and returns a boolean, whereas Function converts one object into another.
D. Stream API
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
The Stream API is a feature introduced in Java 8 to process collections in a functional and declarative way.
It allows operations like filtering, sorting, mapping, grouping, and reducing without modifying the original collection.
Example
List<String> names = List.of("Ram", "Shyam", "Amit");
names.stream
.filter(name -> name.startsWith("A"))
.forEach(System.out::println);
Advantages
- Less code
- Better readability
- Easy filtering and transformation
- Supports parallel processing
Interview One-Liner: Stream API is used to process collections efficiently using functional programming.
Before Java 8, processing collections required manual loops and extra code.
Streams simplify these operations and improve readability.
Without Stream
for (Employee e : employees) {
if (e.getSalary > 50000) {
System.out.println(e.getName);
}
}
With Stream
employees.stream
.filter(e -> e.getSalary > 50000)
.forEach(e -> System.out.println(e.getName));
Benefits
- Less boilerplate code
- Easier filtering
- Better readability
- Easy parallel processing
| Collection | Stream |
|---|---|
| Stores data | Processes data |
| Can be reused | Cannot be reused after a terminal operation |
| Supports adding/removing elements | Read-only processing |
| Available in Java before 8 | Introduced in Java 8 |
| Eagerly stores elements | Processes elements on demand (lazy for intermediate operations) |
Interview One-Liner: A Collection stores data, whereas a Stream processes data.
A Stream generally follows three steps.
Step 1: Create Stream
employees.stream;
Step 2: Intermediate Operations
.filter
.map
.sorted
.distinct
Step 3: Terminal Operation
.collect
.forEach
.count
.reduce
Example
employees.stream
.filter(e -> e.getSalary > 50000)
.map(Employee::getName)
.collect(Collectors.toList);
Interview Tip: Without a terminal operation, intermediate operations are not executed.
| Intermediate | Terminal |
|---|---|
| Returns another Stream | Returns final result |
| Lazy | Executes the pipeline |
| Can be chained | Ends the Stream |
| Examples: filter, map, sorted | Examples: collect, count, forEach, reduce |
Interview One-Liner: Intermediate operations build the pipeline, while terminal operations execute it and produce the final result.
Intermediate operations are not executed immediately.
They execute only when a terminal operation is called.
Example
employees.stream
.filter(e -> {
System.out.println(e.getName);
return true;
});
Nothing is printed because there is no terminal operation.
After adding:
.count;
The pipeline executes.
Advantages
- Better performance
- Avoids unnecessary computation
- Optimizes processing
Interview One-Liner: Streams use lazy evaluation, meaning intermediate operations run only when a terminal operation is invoked.
| External Iteration | Internal Iteration |
|---|---|
| Controlled by developer | Controlled by Stream API |
| Uses loops | Uses Streams |
| More code | Less code |
| Sequential by default | Can easily be parallel |
External
for (String name : names) {
System.out.println(name);
}
Internal
names.forEach(System.out::println);
Interview One-Liner: External iteration is managed by the programmer, whereas internal iteration is managed by the Stream API.
Stateless Operations
These do not depend on previously processed elements.
Examples:
- map
- filter
- peek
Example
numbers.stream
.filter(n -> n > 10);
Stateful Operations
These depend on previously processed elements or require knowledge of the entire stream.
Examples:
- sorted
- distinct
- limit
- skip
Example
numbers.stream
.sorted;
Interview One-Liner: Stateless operations process each element independently, whereas stateful operations need information about other elements.
A Parallel Stream divides the data into multiple parts and processes them concurrently using the Fork/Join Framework.
Example
employees.parallelStream
.forEach(System.out::println);
Advantages
- Faster for large datasets
- Better CPU utilization
- Uses multiple processor cores
Interview One-Liner: Parallel Streams improve performance by processing data concurrently on multiple CPU cores.
Parallel Streams are not always faster. They should be avoided in the following situations:
- Small collections (parallel overhead outweighs benefits)
- Operations that depend on element order
- Shared mutable data (can cause race conditions)
- I/O-bound operations like database calls or REST API calls
- Performance-critical applications where thread management needs fine control
Example (Avoid)
employees.parallelStream
.forEach(employee -> saveToDatabase(employee));
Each database operation is I/O-bound, so a parallel stream usually won’t improve performance and may increase contention.
Interview One-Liner: Avoid Parallel Streams for small datasets, ordered processing, shared mutable state, and I/O operations such as database or API calls.
⭐ Quick Interview Revision
| Question | One-Line Answer |
|---|---|
| Functional Interface | Interface with exactly one abstract method. |
| @FunctionalInterface | Ensures only one abstract method at compile time. |
| Predicate | Tests a condition and returns a boolean. |
| Function | Converts one object into another. |
| Consumer | Takes input and returns nothing. |
| Supplier | Returns data without taking input. |
| UnaryOperator | One input and same-type output. |
| BinaryOperator | Two same-type inputs and same-type output. |
| BiFunction | Two inputs and one output. |
| Stream API | Functional way to process collections. |
| Stream vs Collection | Collection stores data; Stream processes data. |
| Intermediate Operation | Returns another Stream and is lazily evaluated. |
| Terminal Operation | Executes the pipeline and produces the final result. |
| Lazy Evaluation | Intermediate operations run only when a terminal operation is called. |
| Internal Iteration | Iteration handled by the Stream API. |
| Parallel Stream | Processes elements concurrently using the Fork/Join Framework. |
| When to Avoid Parallel Streams | Small datasets, ordered processing, shared mutable state, and I/O-bound operations. |
E. Stream Operations
Interview Tip: Interviewers often ask the purpose of each Stream operation and expect you to know whether it is an Intermediate or Terminal operation.
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
filter is an intermediate operation used to select elements that satisfy a given condition.
It accepts a Predicate and returns a new Stream.
Example
List<String> names = List.of("Ram", "Amit", "Shyam");
names.stream
.filter(name -> name.startsWith("A"))
.forEach(System.out::println);
Output
Amit
Project Use
- Filter active users
- Filter orders by status
- Filter employees by salary
Interview One-Liner: filter selects elements based on a condition.
map is an intermediate operation used to transform one object into another.
It accepts a Function.
Example
employees.stream
.map(Employee::getName)
.forEach(System.out::println);
Project Use
- Entity → DTO conversion
- Extract names, IDs, emails
- Data transformation
Interview One-Liner: map transforms each element into another form.
flatMap converts multiple collections into a single Stream.
It is commonly used to flatten nested collections.
Example
List<List<String>> list = List.of(
List.of("A", "B"),
List.of("C", "D"));
list.stream
.flatMap(Collection::stream)
.forEach(System.out::println);
Output
A
B
C
D
Project Use
- Flatten nested lists
- Process nested objects
- Flatten orders and order items
Interview One-Liner: flatMap converts a Stream of collections into a single flattened Stream.
Removes duplicate elements.
Example
List.of(1,2,2,3,3,4)
.stream
.distinct
.forEach(System.out::println);
Output
1
2
3
4
Interview One-Liner: distinct removes duplicate elements.
Sorts Stream elements.
Example
numbers.stream
.sorted
.forEach(System.out::println);
Custom Sorting
employees.stream
.sorted(Comparator.comparing(Employee::getSalary))
.forEach(System.out::println);
Interview One-Liner: sorted sorts elements in natural or custom order.
peek is mainly used for debugging.
It allows inspecting elements without modifying them.
Example
employees.stream
.peek(System.out::println)
.count;
Interview Tip: Avoid using peek for business logic or modifying data.
Interview One-Liner: peek is mainly used for debugging Stream pipelines.
Returns only the first N elements.
Example
numbers.stream
.limit(5)
.forEach(System.out::println);
Project Use
Pagination
Top N records
Interview One-Liner: limit restricts the Stream to the first specified number of elements.
Skips the first N elements.
Example
numbers.stream
.skip(5)
.forEach(System.out::println);
Project Use
Pagination
Interview One-Liner: skip ignores the first specified number of elements.
forEach is a terminal operation used to perform an action on every element.
Example
names.stream
.forEach(System.out::println);
Interview One-Liner: forEach performs an action for every Stream element.
collect converts the Stream into a collection or another desired result.
Example
List<String> names =
employees.stream
.map(Employee::getName)
.collect(Collectors.toList);
Common Collectors
- toList
- toSet
- toMap
- groupingBy
Interview One-Liner: collect gathers Stream elements into a collection or another final result.
reduce combines all elements into a single result.
Example
int sum = List.of(1,2,3,4)
.stream
.reduce(0, Integer::sum);
Output
10
Project Use
- Sum
- Total Amount
- Maximum
- Minimum
Interview One-Liner: reduce combines Stream elements into one final value.
Returns the number of elements.
Example
long total =
employees.stream
.count;
Interview One-Liner: count returns the total number of elements in the Stream.
Returns the minimum element.
Example
Optional<Integer> min =
numbers.stream
.min(Integer::compareTo);
Interview One-Liner: min returns the smallest element.
Returns the maximum element.
Example
Optional<Integer> max =
numbers.stream
.max(Integer::compareTo);
Interview One-Liner: max returns the largest element.
Returns true if at least one element satisfies the condition.
Example
boolean found =
employees.stream
.anyMatch(e -> e.getSalary > 100000);
Interview One-Liner: anyMatch returns true if any element matches the condition.
Returns true only if all elements satisfy the condition.
Example
employees.stream
.allMatch(e -> e.getSalary > 10000);
Interview One-Liner: allMatch returns true only when every element matches the condition.
Returns true if no element satisfies the condition.
Example
employees.stream
.noneMatch(e -> e.getSalary < 0);
Interview One-Liner: noneMatch returns true when no element matches the condition.
Returns the first element of the Stream.
Example
employees.stream
.findFirst;
Interview One-Liner: findFirst returns the first element and preserves encounter order.
Returns any element from the Stream.
It is mainly useful with Parallel Streams.
Example
employees.parallelStream
.findAny;
Interview Tip: – Sequential Stream → Usually returns the first element.
- Parallel Stream → May return any matching element for better performance.
Interview One-Liner: findAny returns any element and is optimized for Parallel Streams.
F. Collectors
Interview Tip: Collectors are heavily used with collect to transform Stream results into collections, maps, grouped data, and summary statistics.
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
Collects Stream elements into a List.
Example
List<String> names =
employees.stream
.map(Employee::getName)
.collect(Collectors.toList);
Interview One-Liner: toList collects Stream elements into a List.
Collects elements into a Set, automatically removing duplicates.
Example
Set<String> names =
employees.stream
.map(Employee::getDepartment)
.collect(Collectors.toSet);
Interview One-Liner: toSet collects elements into a Set and removes duplicates.
Converts Stream elements into a Map.
Example
Map<Integer, String> map =
employees.stream
.collect(Collectors.toMap(
Employee::getId,
Employee::getName));
Interview Tip: Duplicate keys cause an exception unless a merge function is provided.
Interview One-Liner: toMap converts Stream elements into key-value pairs.
Groups elements based on a field.
Example
Map<String, List<Employee>> result =
employees.stream
.collect(Collectors.groupingBy(Employee::getDepartment));
Project Use
- Group employees by department
- Orders by status
- Students by class
Interview One-Liner: groupingBy groups elements based on a key.
Splits elements into two groups based on a boolean condition.
Example
Map<Boolean, List<Employee>> result =
employees.stream
.collect(Collectors.partitioningBy(
e -> e.getSalary > 50000));
Interview One-Liner: partitioningBy divides elements into true and false groups.
Joins String elements into one String.
Example
String result =
names.stream
.collect(Collectors.joining(", "));
Output
Ram, Amit, John
Interview One-Liner: joining combines multiple Strings into one String.
Counts elements in a Stream.
Example
Long count =
employees.stream
.collect(Collectors.counting);
Interview One-Liner: counting returns the number of elements using a Collector.
Applies a transformation before collecting.
Example
Map<String, List<String>> result =
employees.stream
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.mapping(
Employee::getName,
Collectors.toList)));
Interview One-Liner: mapping transforms elements before the final collection.
Performs an additional operation after collecting.
Example
List<String> list =
employees.stream
.collect(Collectors.collectingAndThen(
Collectors.toList,
Collections::unmodifiableList));
Interview One-Liner: collectingAndThen applies a finishing operation after collection.
Returns summary statistics like:
- Count
- Sum
- Average
- Minimum
- Maximum
Example
IntSummaryStatistics stats =
employees.stream
.collect(Collectors.summarizingInt(
Employee::getSalary));
Interview One-Liner: summarizingInt provides count, sum, average, min, and max in one operation.
Calculates the average of integer values.
Example
double avg =
employees.stream
.collect(Collectors.averagingInt(
Employee::getSalary));
Interview One-Liner: averagingInt calculates the average of integer values.
Finds the maximum element using a Comparator.
Example
Optional<Employee> emp =
employees.stream
.collect(Collectors.maxBy(
Comparator.comparing(Employee::getSalary)));
Interview One-Liner: maxBy returns the maximum element using a Comparator.
Finds the minimum element using a Comparator.
Example
Optional<Employee> emp =
employees.stream
.collect(Collectors.minBy(
Comparator.comparing(Employee::getSalary)));
Interview One-Liner: minBy returns the minimum element using a Comparator.
G. Optional
Interview Tip: Optional is one of the most frequently asked Java 8 topics in Spring Boot interviews. Interviewers usually ask why it was introduced, when to use it, and its best practices.
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
Optional is a container object introduced in Java 8 to represent the presence or absence of a value.
Its main purpose is to avoid NullPointerException (NPE) and make null handling more explicit.
Before Java 8
User user = repository.findById(id);
if (user != null) {
System.out.println(user.getName);
}
Using Optional
Optional<User> user = repository.findById(id);
user.ifPresent(u -> System.out.println(u.getName));
Advantages
- Reduces NullPointerException
- Cleaner code
- Encourages explicit null handling
- Improves readability
Interview One-Liner: Optional is a container object that represents the presence or absence of a value and helps avoid NullPointerException.
Instead of returning null, a method returns an Optional.
The caller is forced to handle the case where a value may be absent.
Example
Without Optional
User user = repository.findById(id);
System.out.println(user.getName);
If user is null, this throws:
NullPointerException
With Optional
String name =
repository.findById(id)
.map(User::getName)
.orElse("Unknown");
Interview One-Liner: Optional avoids NullPointerException by making the absence of a value explicit and providing safe methods to handle it.
Optional.of creates an Optional that must contain a non-null value.
If the value is null, it throws a NullPointerException.
Example
Optional<String> name =
Optional.of("Java");
Invalid
Optional.of(null);
Interview One-Liner: Use Optional.of only when you are certain the value is never null.
Optional.ofNullable creates an Optional that can contain either a value or null.
Example
Optional<String> name =
Optional.ofNullable(getName);
If the value is null, it returns an empty Optional instead of throwing an exception.
Interview One-Liner: ofNullable safely handles both null and non-null values.
Creates an empty Optional.
Example
Optional<String> name =
Optional.empty;
Interview One-Liner: Optional.empty represents the absence of a value.
Checks whether an Optional contains a value.
Example
Optional<String> name =
Optional.of("Java");
if (name.isPresent) {
System.out.println(name.get);
}
Interview Tip: Prefer ifPresent or orElse over isPresent followed by get.
Interview One-Liner: isPresent checks whether a value exists inside the Optional.
Executes the given action only if a value is present.
Example
Optional.of("Java")
.ifPresent(System.out::println);
Interview One-Liner: ifPresent executes code only when a value exists.
Returns the Optional value if present; otherwise returns a default value.
Example
String name =
Optional.ofNullable(null)
.orElse("Guest");
Output
Guest
Interview One-Liner: orElse returns the contained value or a default value if absent.
Returns the value if present; otherwise executes a Supplier to generate the default value.
Example
String name =
Optional.ofNullable(null)
.orElseGet( -> "Guest");
Difference
orElse(value)
Always evaluates the default value.
orElseGet( -> value)
Evaluates the Supplier only if needed.
Interview One-Liner: orElseGet lazily computes the default value and is preferred when the fallback is expensive to create.
Throws an exception if the Optional is empty.
Example
User user =
repository.findById(id)
.orElseThrow(
-> new UserNotFoundException);
Project Use
- Entity not found
- Resource not found
- Validation failures
Interview One-Liner: orElseThrow throws a custom exception when no value is present.
Transforms the value inside an Optional if it is present.
Example
String name =
Optional.of(user)
.map(User::getName)
.orElse("Unknown");
Interview One-Liner: map transforms the contained value while preserving the Optional wrapper.
Used when the mapping function itself returns an Optional.
It avoids nested Optionals.
Example
Optional<Address> address =
userOptional.flatMap(User::getAddress);
Without flatMap, the result would be:
Optional<Optional<Address>>
Interview One-Liner: flatMap prevents nested Optionals by flattening the result.
Returns the Optional only if the value satisfies the given condition.
Example
Optional<String> name =
Optional.of("Java")
.filter(s -> s.startsWith("J"));
Interview One-Liner: filter keeps the value only if it matches the specified condition.
❌ Mistakes
Using Optional as an Entity field
class User {
Optional<String> name;
}
Using Optional as a method parameter
void save(Optional<User> user)
Calling get without checking
optional.get;
Using Optional.of(null)
Optional.of(null);
Best Practices
- Use Optional as a return type.
- Prefer orElseThrow or orElseGet.
- Avoid isPresent + get.
- Never use Optional for fields or parameters.
Interview One-Liner: Use Optional only as a return type and avoid using get without checking for a value.
H. Default & Static Methods
Interview Tip: Default methods are commonly asked because they were introduced to support interface evolution without breaking existing implementations.
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
Before Java 8, adding a new method to an interface forced every implementing class to implement that method.
This broke existing code.
Default methods allow interfaces to add new methods with a default implementation without affecting existing implementations.
Example
interface Vehicle {
default void start {
System.out.println("Vehicle Started");
}
}
Advantages
- Backward compatibility
- Interface evolution
- Reduce duplicate code
Real Example
Java 8 added methods like forEach and stream to the Collection interface using default methods without breaking existing implementations.
Interview One-Liner: Default methods allow new functionality to be added to interfaces without breaking existing implementations.
Java 8 allows interfaces to contain static methods.
They belong to the interface itself and are called using the interface name.
Example
interface MathUtil {
static int square(int x) {
return x * x;
}
}
Calling
MathUtil.square(5);
Important Points
- Cannot be overridden.
- Cannot be called through an object.
- Called using the interface name.
Interview One-Liner: Static methods in interfaces provide utility methods related to the interface and are invoked using the interface name.
This creates a default method conflict, also called the Diamond Problem.
Example
interface A {
default void show {
System.out.println("A");
}
}
interface B {
default void show {
System.out.println("B");
}
}
class Demo implements A, B {
}
Compilation fails because Java doesn’t know which implementation to use.
Interview One-Liner: If two interfaces provide the same default method, the implementing class must resolve the conflict by overriding the method.
The Diamond Problem occurs when a class inherits the same default method from multiple interfaces.
Example
interface A {
default void show {
System.out.println("A");
}
}
interface B {
default void show {
System.out.println("B");
}
}
class Demo implements A, B {
@Override
public void show {
A.super.show;
}
}
Output
A
Interview One-Liner: The Diamond Problem occurs when multiple interfaces provide the same default method, requiring the implementing class to resolve the conflict.
Rule 1: Class wins over Interface
If a superclass provides the method, it takes priority over interface default methods.
Example
class Parent {
public void show {
System.out.println("Parent");
}
}
Even if an interface has a default show, the class method is used.
Rule 2: More specific Interface wins
If one interface extends another and both define the same default method, the more specific interface is chosen.
Rule 3: Override explicitly
If two unrelated interfaces define the same default method, the implementing class must override it.
@Override
public void show {
A.super.show;
}
or
@Override
public void show {
B.super.show;
}
Interview One-Liner: Default method conflicts are resolved by giving priority to class methods, then more specific interfaces, and finally by explicitly overriding the method when necessary.
I. Method Reference
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
A Method Reference is a shorthand notation for a Lambda Expression when the Lambda only calls an existing method.
Instead of writing a Lambda, we can directly refer to the method using the :: operator.
Syntax
ClassName::methodName
Example
Using Lambda
List<String> names = List.of("Ram", "Amit");
names.forEach(name -> System.out.println(name));
Using Method Reference
names.forEach(System.out::println);
Advantages
- Less code
- Better readability
- Easier to maintain
- Improves code clarity
Interview One-Liner: Method Reference is a shorthand for a Lambda Expression that simply invokes an existing method.
Java provides four types of Method References.
| Type | Example |
|---|---|
| Static Method Reference | Math::abs |
| Instance Method of a Particular Object | printer::print |
| Instance Method of an Arbitrary Object | String::toUpperCase |
| Constructor Reference | ArrayList::new |
Interview Tip: Remember these four types, as interviewers often ask them directly.
Used to refer to a static method of a class.
Example
Lambda
Function<Integer, Integer> abs = n -> Math.abs(n);
Method Reference
Function<Integer, Integer> abs = Math::abs;
Common Uses
- Utility methods
- Validation methods
- Conversion methods
Interview One-Liner: A Static Method Reference refers to a static method using ClassName::methodName.
There are two kinds of instance method references.
J. Date & Time API
Interview Tip: Almost every Java 8 interview includes questions on the new Date & Time API because it replaced the older Date and Calendar APIs.
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
Before Java 8, developers mainly used java.util.Date and Calendar, which had several limitations.
Problems
- Mutable (not thread-safe)
- Difficult to use
- Confusing API
- Month starts from 0 in Calendar
- Poor timezone support
- Difficult date calculations
Example
Calendar cal = Calendar.getInstance;
cal.set(2025, 0, 1); // January is 0
Java 8 Solution
Java introduced the java.time package, which provides immutable, thread-safe, and easy-to-use classes.
Interview One-Liner: The old Date and Calendar APIs were mutable, confusing, and not thread-safe. Java 8 introduced the java.time API to solve these issues.
LocalDate represents only a date, without time or timezone.
Example
LocalDate today = LocalDate.now;
System.out.println(today);
Output
2026-07-27
Common Methods
today.plusDays(5);
today.minusMonths(1);
today.getYear;
Project Use
- Birth dates
- Invoice dates
- Joining dates
Interview One-Liner: LocalDate represents only the date (year, month, day).
LocalTime represents only the time.
Example
LocalTime time = LocalTime.now;
Output
14:30:25
Project Use
- Office timings
- Store opening hours
- Meeting schedules
Interview One-Liner: LocalTime represents only the time without date or timezone.
LocalDateTime combines both date and time.
Example
LocalDateTime now = LocalDateTime.now;
Output
2026-07-27T14:30:20
Project Use
- Audit logs
- Order timestamps
- Login time
Interview One-Liner: LocalDateTime represents both date and time without timezone information.
ZonedDateTime represents date, time, and timezone.
Example
ZonedDateTime india =
ZonedDateTime.now(ZoneId.of("Asia/Kolkata"));
Project Use
- International applications
- Flight booking
- Global meetings
Interview One-Liner: ZonedDateTime stores date, time, and timezone together.
Instant represents a specific moment on the UTC timeline.
Example
Instant now = Instant.now;
Project Use
- Logging
- Distributed systems
- Event timestamps
- Database storage
Interview One-Liner: Instant represents an exact point in time in UTC.
Duration represents a time difference in hours, minutes, seconds, or milliseconds.
Example
LocalTime start = LocalTime.of(10, 0);
LocalTime end = LocalTime.of(12, 30);
Duration duration = Duration.between(start, end);
System.out.println(duration.toHours);
Output
2
Interview One-Liner: Duration calculates the difference between two time-based values.
Period represents the difference between two dates in years, months, and days.
Example
LocalDate start = LocalDate.of(2025,1,1);
LocalDate end = LocalDate.now;
Period period = Period.between(start, end);
Project Use
- Employee experience
- Customer subscription
- Age calculation
Interview One-Liner: Period calculates the difference between two dates.
DateTimeFormatter is used to format and parse dates.
Example
LocalDate today = LocalDate.now;
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("dd-MM-yyyy");
System.out.println(today.format(formatter));
Output
27-07-2026
Interview One-Liner: DateTimeFormatter formats and parses date and time values.
Example
Date date = new Date;
LocalDate localDate =
date.toInstant
.atZone(ZoneId.systemDefault)
.toLocalDate;
Reverse Conversion
Date date =
Date.from(
localDate.atStartOfDay(ZoneId.systemDefault)
.toInstant);
Interview One-Liner: Convert Date to LocalDate using toInstant, atZone, and toLocalDate.
Java 8 provides the ZoneId class to work with different time zones.
Example
ZonedDateTime india =
ZonedDateTime.now(ZoneId.of("Asia/Kolkata"));
ZonedDateTime london =
ZonedDateTime.now(ZoneId.of("Europe/London"));
Best Practices
- Store timestamps as Instant (UTC) in databases.
- Convert to the user’s local timezone only when displaying data.
- Avoid using the system default timezone in distributed applications unless required.
Project Example
In a global e-commerce application, order timestamps can be stored as Instant in UTC. When a user views the order, the timestamp is converted to the user’s timezone using ZoneId.
Interview One-Liner: Use ZoneId and ZonedDateTime for timezone handling, and prefer storing timestamps in UTC using Instant.
K. CompletableFuture
Interview Tip: CompletableFuture is one of the most frequently asked Java 8 topics for developers with 3+ years of experience. Interviewers often ask how you use it in Spring Boot microservices.
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
Before Java 8, asynchronous programming was done using Thread, ExecutorService, and Future, which had many limitations.
CompletableFuture provides a powerful way to execute asynchronous tasks, combine multiple tasks, handle exceptions, and chain operations.
Advantages
- Non-blocking programming
- Better performance
- Supports task chaining
- Easy exception handling
- Combines multiple asynchronous tasks
Project Example
In a Spring Boot e-commerce application, when a user places an order, we can fetch:
- Customer details
- Product details
- Inventory status
- Payment information
simultaneously using CompletableFuture, reducing overall response time.
Interview One-Liner: CompletableFuture enables asynchronous, non-blocking programming with support for task chaining, combining results, and exception handling.
| Future | CompletableFuture |
|---|---|
| Introduced in Java 5 | Introduced in Java 8 |
| Only retrieve result | Retrieve and process result |
| Cannot chain tasks | Supports task chaining |
| Limited exception handling | Built-in exception handling |
| Cannot combine multiple tasks | Can combine multiple tasks |
| Usually blocking (get) | Supports non-blocking operations |
Interview One-Liner: CompletableFuture is an enhanced version of Future with support for asynchronous pipelines, chaining, combining tasks, and exception handling.
Used when an asynchronous task returns a result.
Example
CompletableFuture<String> future =
CompletableFuture.supplyAsync( -> "Hello");
System.out.println(future.get);
Project Use
- Fetch customer details
- Call REST APIs
- Read database records
Interview One-Liner: supplyAsync executes a task asynchronously and returns a result.
Used when the asynchronous task does not return a result.
Example
CompletableFuture.runAsync( ->
System.out.println("Task Completed"));
Project Use
- Logging
- Sending emails
- Sending notifications
Interview One-Liner: runAsync executes a task asynchronously without returning any result.
Used to transform the result of a previous asynchronous task.
Example
CompletableFuture<String> future =
CompletableFuture.supplyAsync( -> "java")
.thenApply(String::toUpperCase);
Output
JAVA
Interview One-Liner: thenApply transforms the result of the previous CompletableFuture.
Consumes the result without returning another value.
Example
CompletableFuture.supplyAsync( -> "Java")
.thenAccept(System.out::println);
Interview One-Liner: thenAccept consumes the result and returns CompletableFuture<Void>.
Used to chain two dependent asynchronous tasks.
Example
CompletableFuture<String> future =
CompletableFuture.supplyAsync( -> "Order")
.thenCompose(order ->
CompletableFuture.supplyAsync( -> order + " Processed"));
Project Use
- Fetch customer → Fetch orders
- Fetch user → Fetch permissions
Interview One-Liner: thenCompose chains dependent asynchronous operations.
Combines the results of two independent asynchronous tasks.
Example
CompletableFuture<String> user =
CompletableFuture.supplyAsync( -> "John");
CompletableFuture<Integer> age =
CompletableFuture.supplyAsync( -> 30);
user.thenCombine(age,
(name, a) -> name + " " + a);
Project Use
- Customer + Orders
- Product + Inventory
Interview One-Liner: thenCombine combines the results of two independent CompletableFutures.
Handles exceptions and provides a fallback value.
Example
CompletableFuture<String> future =
CompletableFuture.supplyAsync( -> {
throw new RuntimeException;
})
.exceptionally(ex -> "Default Value");
Interview One-Liner: exceptionally handles exceptions and returns a fallback result.
Processes both success and failure.
Example
CompletableFuture.supplyAsync( -> "Java")
.handle((result, ex) -> {
if (ex != null)
return "Error";
return result.toUpperCase;
});
Difference
- exceptionally → Handles only exceptions.
- handle → Handles both success and exception.
Interview One-Liner: handle processes both successful results and exceptions.
Waits until all CompletableFutures complete.
Example
CompletableFuture<Void> future =
CompletableFuture.allOf(f1, f2, f3);
Project Use
- Wait for multiple API calls
- Multiple database queries
Interview One-Liner: allOf waits for all asynchronous tasks to complete.
Returns the result of the first completed CompletableFuture.
Example
CompletableFuture<Object> future =
CompletableFuture.anyOf(f1, f2, f3);
Project Use
- Fastest service response
- Multiple cache servers
Interview One-Liner: anyOf returns the result of whichever task finishes first.
Many CompletableFuture methods have both Async and Non-Async versions.
Example
thenApply
thenApplyAsync
Difference
Non-Async
- Executes in the thread that completed the previous stage.
Async
- Executes in another thread (usually the ForkJoinPool or a custom Executor).
Interview Tip: In Spring Boot production applications, prefer using a custom Executor with Async methods instead of the common ForkJoinPool.
Interview One-Liner: Async methods execute on another thread, while non-Async methods execute on the thread that completed the previous stage.
⭐ Real Project Interview Answer
Q. How have you used CompletableFuture in your project?
In our Spring Boot microservices, we use CompletableFuture to call multiple independent services, such as Customer Service, Inventory Service, and Pricing Service, in parallel. We combine the results using allOf or thenCombine, reducing the overall API response time. We also use exceptionally for fallback handling and configure a custom thread pool for better performance.
L. Nashorn & Miscellaneous Features
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
Java 8 introduced the Base64 utility class for encoding and decoding binary data into Base64 format.
Example
String encoded =
Base64.getEncoder
.encodeToString("Java".getBytes);
String decoded =
new String(Base64.getDecoder.decode(encoded));
Project Use
- JWT tokens
- File encoding
- Image encoding
- API communication
Interview One-Liner: Java 8 provides the Base64 class for encoding and decoding data.
StringJoiner joins multiple strings with a delimiter and optional prefix/suffix.
Example
StringJoiner joiner =
new StringJoiner(", ");
joiner.add("Java")
.add("Spring")
.add("Kafka");
System.out.println(joiner);
Output
Java, Spring, Kafka
Interview One-Liner: StringJoiner joins multiple strings using a delimiter.
Arrays.parallelSort sorts arrays using multiple threads.
It uses the Fork/Join Framework, making it faster for large arrays.
Example
int[] arr = {5,2,8,1};
Arrays.parallelSort(arr);
Difference
- Arrays.sort → Single-threaded.
- Arrays.parallelSort → Multi-threaded.
Interview One-Liner: Arrays.parallelSort performs parallel sorting for better performance on large arrays.
Java 8 allows applying the same annotation multiple times to a class or method.
Example
@Role("Admin")
@Role("User")
class Employee {
}
Interview One-Liner: Repeatable Annotations allow the same annotation to be used multiple times on the same element.
Java 8 allows annotations to be applied to type declarations and type usages.
Example
List<@NotNull String> names;
Benefits
- Better validation
- Static code analysis
- Improved type checking
Interview One-Liner: Type Annotations allow annotations on any type usage, improving validation and code analysis.
Nashorn is the JavaScript engine introduced in Java 8.
It allows Java applications to execute JavaScript code.
Example
ScriptEngine engine =
new ScriptEngineManager
.getEngineByName("nashorn");
engine.eval("print('Hello Java')");
Important Interview Point
- Introduced in Java 8.
- Deprecated in Java 11.
- Removed in Java 15.
Interview One-Liner: Nashorn enabled Java applications to run JavaScript code but has been deprecated and removed in later Java versions.
Spliterator is an advanced iterator introduced in Java 8.
It supports:
- Traversing elements
- Splitting data for parallel processing
Example
List<String> list = List.of("A", "B", "C");
Spliterator<String> sp = list.spliterator;
sp.forEachRemaining(System.out::println);
Advantages
- Better parallel processing
- Used internally by Streams
- Supports splitting large datasets
Interview One-Liner: Spliterator is an enhanced iterator designed for efficient traversal and parallel processing of collections.
M. Java 8 Coding Interview Questions
Interview Tip: These are among the most frequently asked Java 8 coding questions in interviews for 3–8 years of experience. Focus on understanding the Stream operations used rather than memorizing the code.
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
Solution
List<Integer> evenNumbers = numbers.stream
.filter(n -> n % 2 == 0)
.collect(Collectors.toList);
Interview Explanation
- filter selects elements matching a condition.
- Here, only even numbers are collected.
Solution
Set<Integer> unique = new HashSet<>;
Set<Integer> duplicates = numbers.stream
.filter(n -> !unique.add(n))
.collect(Collectors.toSet);
Interview Explanation
- HashSet.add returns false for duplicate elements.
- Duplicate values are collected into a Set.
Solution
List<Integer> unique = numbers.stream
.distinct
.collect(Collectors.toList);
Interview Explanation
- distinct removes duplicate values.
Solution
List<Integer> result = numbers.stream
.distinct
.collect(Collectors.toList);
Interview Explanation
- Same solution as finding unique elements.
Solution
employees.stream
.sorted(Comparator.comparing(Employee::getSalary))
.forEach(System.out::println);
Descending Order
employees.stream
.sorted(Comparator.comparing(Employee::getSalary).reversed)
.forEach(System.out::println);
Interview Explanation
- Comparator.comparing sorts by salary.
Solution
employees.stream
.sorted(
Comparator.comparing(Employee::getDepartment)
.thenComparing(Employee::getSalary))
.forEach(System.out::println);
Interview Explanation
- First sort by department.
- If departments are equal, sort by salary.
Solution
Optional<Integer> secondHighest =
employees.stream
.map(Employee::getSalary)
.distinct
.sorted(Comparator.reverseOrder)
.skip(1)
.findFirst;
Interview Explanation
- Remove duplicates.
- Sort in descending order.
- Skip the highest salary.
- Return the next one.
Solution
int n = 3;
Optional<Integer> salary =
employees.stream
.map(Employee::getSalary)
.distinct
.sorted(Comparator.reverseOrder)
.skip(n - 1)
.findFirst;
Interview Explanation
Replace n with the required rank.
Solution
Map<String, Optional<Employee>> result =
employees.stream
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.maxBy(
Comparator.comparing(Employee::getSalary))));
Interview Explanation
- Group by department.
- Find maximum salary within each department.
Solution
Map<String, List<Employee>> result =
employees.stream
.collect(Collectors.groupingBy(Employee::getDepartment));
Interview Explanation
Groups all employees by department.
Solution
Map<String, Long> result =
employees.stream
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.counting));
Interview Explanation
Uses groupingBy with counting.
Solution
Map<Integer, Employee> result =
employees.stream
.collect(Collectors.toMap(
Employee::getId,
e -> e));
Interview Explanation
Converts a List into a Map using ID as the key.
Solution
Map<Character, Long> result =
str.chars
.mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(
c -> c,
Collectors.counting));
Interview Explanation
Groups characters and counts their occurrences.
Solution
Character ch =
str.chars
.mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(
c -> c,
LinkedHashMap::new,
Collectors.counting))
.entrySet
.stream
.filter(e -> e.getValue == 1)
.map(Map.Entry::getKey)
.findFirst
.orElse(null);
Interview Explanation
- LinkedHashMap preserves insertion order.
- Return the first character with a count of 1.
Solution
String reversed =
IntStream.range(0, str.length)
.mapToObj(i -> str.charAt(str.length - 1 - i))
.map(String::valueOf)
.collect(Collectors.joining);
Interview Note
Although possible, interviewers usually prefer:
new StringBuilder(str).reverse.toString;
Solution
long count =
str.toLowerCase
.chars
.filter(c -> "aeiou".indexOf(c) != -1)
.count;
Interview Explanation
Counts all vowels using filter.
Solution
List<Integer> merged =
Stream.concat(list1.stream,
list2.stream)
.collect(Collectors.toList);
Interview Explanation
Stream.concat combines two Streams.
Solution
List<Integer> common =
list1.stream
.filter(list2::contains)
.collect(Collectors.toList);
Interview Explanation
Filters elements present in both lists.
Solution
Map<Boolean, List<Integer>> result =
numbers.stream
.collect(Collectors.partitioningBy(
n -> n % 2 == 0));
Interview Explanation
- true → Even
- false → Odd
Solution
List<String> result =
nestedList.stream
.flatMap(Collection::stream)
.collect(Collectors.toList);
Interview Explanation
Converts multiple lists into one list.
Solution
int sum =
numbers.stream
.reduce(0, Integer::sum);
Interview Explanation
reduce combines all elements into one result.
Solution
Optional<String> longest =
strings.stream
.max(Comparator.comparing(String::length));
Interview Explanation
Uses Comparator.comparing with String.length.
Solution
Set<String> unique = new HashSet<>;
Set<String> duplicates =
words.stream
.filter(word -> !unique.add(word))
.collect(Collectors.toSet);
Interview Explanation
Same approach as finding duplicate numbers.
Solution
int n = 5;
List<Integer> salaries =
employees.stream
.map(Employee::getSalary)
.distinct
.sorted(Comparator.reverseOrder)
.limit(n)
.collect(Collectors.toList);
Interview Explanation
- Remove duplicates.
- Sort descending.
- Take top N salaries.
Maximum
Optional<Integer> max =
numbers.stream
.max(Integer::compareTo);
Minimum
Optional<Integer> min =
numbers.stream
.min(Integer::compareTo);
Interview Explanation
Use max and min with a Comparator.
Solution
List<String> upper =
names.stream
.map(String::toUpperCase)
.collect(Collectors.toList);
Interview Explanation
Uses map to transform each string.
Solution
Map<String, Integer> sorted =
map.entrySet
.stream
.sorted(Map.Entry.comparingByValue)
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(a, b) -> a,
LinkedHashMap::new));
Interview Explanation
- Convert the Map into a Stream.
- Sort by value.
- Collect into a LinkedHashMap to preserve the sorted order.
N. Performance & Best Practices
Interview Tip: For experienced developers (4+ years), interviewers expect not only knowledge of Java 8 APIs but also when not to use them.
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
Both Streams and for loops are used to process collections, but they serve different purposes.
| Stream | for-loop |
|---|---|
| Declarative (what to do) | Imperative (how to do it) |
| Less code | More code |
| Functional style | Traditional style |
| Easy parallel processing | Manual thread handling |
| Slight overhead | Usually faster for simple iterations |
Example
For Loop
for (Employee emp : employees) {
if (emp.getSalary > 50000) {
System.out.println(emp.getName);
}
}
Stream
employees.stream
.filter(e -> e.getSalary > 50000)
.map(Employee::getName)
.forEach(System.out::println);
Which is Faster?
- For simple loops → for-loop is generally faster.
- For complex collection transformations → Streams improve readability and maintainability.
Interview One-Liner: Use Streams for readability and complex data processing, and use for-loops for simple, performance-critical iterations.
Avoid Streams when:
- Very simple loops
- Performance-critical code
- Complex business logic that’s difficult to read
- Code requiring frequent break or continue
- Heavy modifications to the collection while iterating
Example
Instead of:
employees.stream
.filter(...)
.map(...)
.flatMap(...)
.sorted(...)
.peek(...)
.collect(...);
A simple loop may be easier to understand if the logic becomes too complicated.
Interview One-Liner: Don’t use Streams when they reduce readability or when maximum performance is required.
Parallel Streams are not always faster.
Common Pitfalls
- Small datasets
- Shared mutable objects
- Database operations
- REST API calls
- File I/O
- Order-dependent processing
- Thread contention
Bad Example
employees.parallelStream
.forEach(employee -> saveToDatabase(employee));
Interview One-Liner: Parallel Streams work well for CPU-intensive tasks but should be avoided for I/O-bound operations.
A side effect means modifying external state while processing a Stream.
Bad Example
List<String> result = new ArrayList<>;
employees.stream
.map(Employee::getName)
.forEach(result::add);
Better
List<String> result =
employees.stream
.map(Employee::getName)
.collect(Collectors.toList);
Why?
Side effects:
- Make code harder to understand
- Cause race conditions with parallel streams
- Reduce maintainability
Interview One-Liner: Streams should ideally be side-effect free and rely on transformations rather than modifying external state.
Best Practices
✅ Return Optional from methods when a value may be absent.
✅ Use orElse, orElseGet, or orElseThrow.
✅ Use map and flatMap with Optional.
❌ Don’t use Optional as an entity field.
❌ Don’t use Optional in method parameters.
Example
User user =
repository.findById(id)
.orElseThrow(
-> new UserNotFoundException);
Interview One-Liner: Use Optional as a return type, not as a field or method parameter.
Immutable collections cannot be modified after creation.
Example
List<String> list =
Collections.unmodifiableList(names);
Or (Java 9+)
List<String> list =
List.of("A", "B", "C");
Advantages
- Thread-safe
- Prevent accidental modification
- Better reliability
Interview One-Liner: Immutable collections improve safety and help prevent unintended changes.
Java provides primitive streams to avoid boxing and unboxing overhead.
Example
int sum =
IntStream.range(1, 10)
.sum;
Advantages
- Better performance
- Lower memory usage
- Specialized operations
Interview One-Liner: Primitive Streams improve performance by avoiding wrapper object creation.
Boxing
Primitive → Wrapper
int x = 10;
Integer i = x;
Unboxing
Wrapper → Primitive
Integer i = 10;
int x = i;
Why Avoid?
Frequent boxing creates unnecessary objects and increases memory usage and GC pressure.
Better
IntStream.range(1,1000)
instead of
Stream<Integer>
Interview One-Liner: Use primitive streams when working with numeric data to avoid boxing and unboxing overhead.
A Stream can be consumed only once.
Example
Stream<String> stream = names.stream;
stream.count;
stream.forEach(System.out::println);
This throws:
IllegalStateException
Solution
Create a new Stream whenever needed.
Interview One-Liner: A Stream cannot be reused after a terminal operation.
Sequential Stream
Safe because processing happens in one thread.
Parallel Stream
Thread safety depends on the operations performed.
Avoid modifying shared mutable objects.
Bad Example
List<String> result = new ArrayList<>;
employees.parallelStream
.forEach(e -> result.add(e.getName));
Better
employees.parallelStream
.map(Employee::getName)
.collect(Collectors.toList);
Interview One-Liner: Streams themselves are not thread-safe if shared mutable state is modified. Prefer stateless operations and collectors.
O. Production Scenario Questions
Interview Tip: These questions test whether you’ve used Java 8 in real Spring Boot microservices rather than just knowing the APIs.
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
Parallel Streams use the common ForkJoinPool, which is shared by the JVM.
In web applications:
- Request threads are already managed by the web server.
- Using Parallel Streams can create thread contention.
- It may reduce overall application throughput.
- Blocking operations (DB/API calls) perform poorly.
Better Approach
Use:
- CompletableFuture
- Spring @Async
- Custom ExecutorService
Interview One-Liner: Avoid Parallel Streams in web applications because they use the shared ForkJoinPool and are not suitable for I/O-bound request processing.
Best Practices
- Filter early.
- Map only required fields.
- Avoid unnecessary sorted and distinct.
- Prefer primitive streams.
- Reduce object creation.
- Break long pipelines into meaningful methods if needed.
Example
Good
employees.stream
.filter(Employee::isActive)
.map(Employee::getName)
.collect(Collectors.toList);
Interview One-Liner: Apply filtering early, avoid unnecessary operations, and use primitive streams where appropriate.
Never load millions of records into memory at once.
Best Practices
- Database pagination
- Batch processing
- Streaming database results
- Chunk processing
- Parallel processing only for CPU-bound work
- Process records incrementally
Spring Boot Example
- Read 5,000 records.
- Process them.
- Save results.
- Read the next batch.
Interview One-Liner: Process large datasets in batches or streams instead of loading everything into memory.
Instead of
if(user != null){
return user.getName;
}
Use
String name =
Optional.ofNullable(user)
.map(User::getName)
.orElse("Unknown");
Interview One-Liner: Optional makes null handling explicit and reduces the chances of NullPointerException.
Imperative
List<String> names = new ArrayList<>;
for(Employee e : employees){
if(e.isActive){
names.add(e.getName);
}
}
Functional
List<String> names =
employees.stream
.filter(Employee::isActive)
.map(Employee::getName)
.collect(Collectors.toList);
Interview One-Liner: Replace loops with Stream operations like filter, map, and collect for clearer, declarative code.
Call the APIs in parallel using CompletableFuture.supplyAsync and combine their results.
Example
CompletableFuture<Customer> customer =
CompletableFuture.supplyAsync( -> customerService.getCustomer);
CompletableFuture<Order> order =
CompletableFuture.supplyAsync( -> orderService.getOrders);
CompletableFuture.allOf(customer, order).join;
Project Example
Order Summary API:
- Customer Service
- Product Service
- Inventory Service
- Payment Service
All are called simultaneously to reduce response time.
Interview One-Liner: Use CompletableFuture to execute independent REST calls in parallel and combine the results.
map
Transforms one object into another.
employees.stream
.map(Employee::getName)
flatMap
Flattens nested collections.
departments.stream
.flatMap(d -> d.getEmployees.stream)
Interview One-Liner: Use map for one-to-one transformations and flatMap to flatten nested collections or nested Optionals/Streams.
Use peek for debugging intermediate results.
Example
employees.stream
.peek(System.out::println)
.filter(Employee::isActive)
.collect(Collectors.toList);
You can also:
- Debug step by step in your IDE.
- Break a long pipeline into smaller methods or variables.
Interview One-Liner: Use peek for debugging and avoid using it for business logic.
Lambda expressions cannot throw checked exceptions directly.
Option 1
Handle the exception inside the Lambda.
files.stream
.forEach(file -> {
try {
process(file);
} catch (IOException e) {
log.error("Error", e);
}
});
Option 2
Create a wrapper utility that converts checked exceptions into unchecked ones.
Interview One-Liner: Handle checked exceptions inside the Lambda or wrap them in a custom utility method.
Best Practices
- Keep each pipeline focused on one task.
- Break complex pipelines into helper methods.
- Use method references where appropriate.
- Give intermediate results meaningful variable names.
- Avoid chaining too many operations in a single statement.
Bad Example
employees.stream
.filter(...)
.map(...)
.sorted(...)
.distinct
.flatMap(...)
.collect(...);
Better
Stream<Employee> activeEmployees =
employees.stream
.filter(Employee::isActive);
List<String> names =
activeEmployees
.map(Employee::getName)
.sorted
.collect(Collectors.toList);
Interview One-Liner: Prefer small, readable Stream pipelines over long chains, and extract complex logic into well-named methods.
⭐ Real Project Interview Answer
Q. Which Java 8 features do you use regularly in your Spring Boot microservices?
In my projects, I frequently use Streams for filtering, mapping, grouping, and converting entities to DTOs. I use Optional to reduce null checks, CompletableFuture for parallel service calls, Collectors for aggregation, Method References to improve readability, and the java.time API for date handling. For performance, I avoid Parallel Streams for database or REST calls and instead use CompletableFuture with a custom thread pool.
⭐ Final Java 8 Interview Cheat Sheet (Must Prepare)
If you’re preparing for Java Backend / Spring Boot interviews (3–8 years), these are the highest-priority topics:
⭐⭐⭐⭐⭐ Must Know
- Lambda Expressions
- Functional Interfaces
- Stream API
- Stream Operations
- Collectors
- Optional
- CompletableFuture
- Java Time API
- Java 8 Coding Questions (especially Employee-based problems)
⭐⭐⭐⭐ Important
- Method References
- Performance Best Practices
- Parallel Streams
- Primitive Streams
- Production Scenarios
⭐⭐⭐ Nice to Know
- Base64
- StringJoiner
- Spliterator
- Repeatable Annotations
- Nashorn
Mastering these topics with code examples and real project scenarios will prepare you for the majority of Java 8 interview questions in service-based and product-based companies.”,