Java 8 interview questions
A. Java 8 Core Concepts (Fundamentals) Some newly added Features of Java 8 are Lambda Expression, Functional Interface, Stream API, Default Method, Method References, Date Time API, Optional Class, Nashorm,…
A. Java 8 Core Concepts (Fundamentals)
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
Some newly added Features of Java 8 are Lambda Expression, Functional Interface, Stream API, Default Method, Method References, Date Time API, Optional Class, Nashorm, and JavaScript Engine.
Short Explanations
- Lambda Expression: lambda expression is a block of code that takes parameters and returns a value. We can refer to Lambda Expression as an object.
- Functional Interfaces:Â It is an interface that contains only one abstract method.
- Method References: Method References allow us to call the Class method by its name. “::” denotes method reference.
- Default method:Â It is a method in the interface that implementation is not required in the class implementing that interface.
- Stream API:Â Stream APIÂ is a sequence of objects used to process Collections.
- Date Time API:Â It is introduced to overcome old Date drawbacks like new Date Time API is thread safe.
- Optional:Â It is a Wrapper class to check the null value.
- Nashorn, JavaScript Engine:Â Nashorn is an improved Javascript engine to replace the existing Rhino.
Lambda expression is a function without a name. It is also known as an anonymous function as it does not have type information by itself. We can also say it is a short and concise way to represent a method without a name.
Lambda expressions are mainly used to implement the single abstract method of a functional interface, making your code cleaner, more readable, and more functional in style.
Basic Syntax of Lambda Expression
(parameters) -> { body }
Breakdown:
- parameters → input values
- -> → lambda operator (arrow)
- body → logic to execute
Example 1: Lambda with No Parameter
() -> System.out.println("Hello Lambda");
Example 2: Lambda with One Parameter
(a) -> System.out.println(a);
Parentheses are optional for a single parameter:
a -> System.out.println(a);
A Functional Interface in Java is an interface that contains exactly one abstract method, , but it can have any number of default and static methods. It is mainly used to support lambda expressions and method references introduced in Java 8.
Syntax Example (Custom Functional Interface)
@FunctionalInterface
interface Calculator {
int add(int a, int b);
}
The @FunctionalInterface annotation is optional, but it ensures compile-time checking.
Using Functional Interface with Lambda Expression
public class Test {
public static void main(String[] args) {
Calculator calc = (a, b) -> a + b;
System.out.println(calc.add(10, 20));
}
}
Functional Interface with Default Method
@FunctionalInterface
interface Greeting {
void sayHello();
default void sayBye() {
System.out.println("Bye");
}
}
Still a functional interface because it has only one abstract method.
The Stream API in Java 8 is a feature that allows us to process collections of data in a functional and declarative way.
It provides a stream of elements from a data source (such as a List, Set, or Array) and supports operations like filtering, mapping, sorting, and aggregation.
Why is Stream API Used?
The Stream API is mainly used to:
- Reduce boilerplate code
- Improve readability and maintainability
- Support functional programming
- Enable parallel processing
- Perform bulk operations on collections
Example Without Stream API
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> even = new ArrayList<>();
for (Integer i : list) {
if (i % 2 == 0) {
even.add(i);
}
}
System.out.println(even);
Same Example Using Stream API
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> even = list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(even);
Cleaner, shorter, and more expressive
Optional is a class introduced in Java 8 that is used to avoid NullPointerException. It acts as a wrapper which may or may not contain a value.
Instead of returning null, a method returns an Optional, so the developer is forced to check whether a value is present before using it.
Optional Example
Optional<String> name = Optional.ofNullable(getName());
System.out.println(name.orElse("Guest"));
Here, if getName() returns null, Optional safely returns "Guest" instead of throwing a NullPointerException.
What Optional Is NOT Meant For?
Not for:
- Class fields
- Method parameters
- Serialization
Best for:
- Method return types
Default methods are methods in an interface that have a method body. They were introduced in Java 8 using the default keyword. Default methods allow interfaces to have method implementations without forcing implementing classes to override them.
Syntax Example
interface Vehicle {
default void start() {
System.out.println("Vehicle is starting");
}
}
Using Default Method
class Car implements Vehicle {
// no need to override start()
}
public class Test {
public static void main(String[] args) {
Car car = new Car();
car.start();
}
}
Why Were Default Methods Introduced? (Most Important)
Before Java 8:
- Interfaces could have only abstract methods
- Adding a new method to an interface would break all existing implementations
Java 8 introduced default methods to:
- Maintain backward compatibility
- Add new methods to existing interfaces without breaking code
- Support Stream API and lambda expressions
Real Example (Interview Point)
Java added default methods like forEach() to List and Iterable in Java 8 without breaking old code.
Static methods in interfaces are methods that belong to the interface itself, not to the implementing class. They were introduced in Java 8 and are defined using the static keyword.
Static methods in interfaces are utility methods that can be called using the interface name and cannot be overridden. It is basically used for helper functionality.
Syntax Example
interface Calculator {
static int add(int a, int b) {
return a + b;
}
}
Calling Static Method
public class Test {
public static void main(String[] args) {
int result = Calculator.add(10, 20);
System.out.println(result);
}
}
Must be called using interface name, not object.
Why Static Methods Were Introduced?
- To provide utility/helper methods
- To keep related logic inside the interface
- To avoid creating separate utility classes
- To support Stream API and functional programming
Important Rules (Interview Points)
- Static methods must have a body
- They cannot be overridden by implementing classes
- Called using InterfaceName.method()
- Cannot use
thisorsuper
Method reference is a short and clean way to call an existing method using the :: operator. It is mainly used when a lambda expression only calls one method, so we can replace the lambda with a method reference.
Simple Example
// Lambda
list.forEach(n -> System.out.println(n));
// Method Reference
list.forEach(System.out::println);
Types of Method References
1. Static method reference
ClassName::staticMethod
2. Instance method of a particular object
object::instanceMethod
3. Instance method of an arbitrary object of a class
ClassName::instanceMethod
4. Constructor reference
ClassName::new
| Collection | Stream |
|---|---|
| Collection is used to store data | Stream is used to process data |
| It holds elements in memory | It does not store elements |
| Can be iterated multiple times | Can be used only once |
| Operations are eager | Operations are lazy |
| Supports add, remove, update | Supports filter, map, reduce |
| Exists since early Java versions | Introduced in Java 8 |
| Follows imperative style | Follows functional style |
Type inference means that the Java compiler automatically detects the data type instead of the programmer explicitly specifying it. In Java 8, type inference is mainly used in lambda expressions and method references to reduce boilerplate code.
In Simple we can explain it as Type inference means the compiler figures out the data type automatically, so we don’t need to write it explicitly.
Example Without Type Inference
Comparator<String> c = (String a, String b) -> a.compareTo(b);
Example With Type Inference
Comparator<String> c = (a, b) -> a.compareTo(b);
Compiler automatically understands that a and b are String.
Type Inference in Lambda Expressions
list.forEach((Integer n) -> System.out.println(n));
// Using type inference
list.forEach(n -> System.out.println(n));
In the Java Stream API, stream operations are divided into intermediate and terminal operations. They are used together to form a stream pipeline.
Simple Explanation
Intermediate operations prepare the stream, and terminal operations execute it. Intermediate operations are lazy and return a stream, while terminal operations trigger execution and produce a result.
Intermediate Operations
Intermediate operations:
- Return another Stream
- Are lazy (they don’t execute immediately)
- Execute only when a terminal operation is called
Common Intermediate Operations
filter()map()sorted()distinct()limit()
Example
list.stream()
.filter(n -> n > 10)
.map(n -> n * 2);
No execution happens yet.
Terminal Operations
Terminal operations:
- Produce a result or side effect
- Trigger the execution of the stream
- End the stream (cannot be reused)
Common Terminal Operations
forEach()collect()count()reduce()findFirst()
Example
list.stream()
.filter(n -> n > 10)
.map(n -> n * 2)
.forEach(System.out::println);
Execution starts here.
Key Differences (Interview Table)
| Intermediate | Terminal |
|---|---|
| Returns Stream | Returns result or void |
| Lazy execution | Triggers execution |
| Can be chained | Ends the stream |
| Can have many | Only one per stream |
A Spliterator is a special iterator introduced in Java 8 that is designed to traverse and split elements of a data source (like collections, arrays, or streams), mainly to support efficient parallel processing.
Package: java.util
Interface: Spliterator<T>
Why was Spliterator introduced?
Traditional iterators:
- Traverse elements one by one
- Are mainly designed for sequential processing
Spliterator:
- Improves performance on large data sets
- Can split data into multiple parts
- Enables parallel streams
Spliterator Example:
import java.util.*;
public class SpliteratorExample {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Spliterator<Integer> spliterator = list.spliterator();
Spliterator<Integer> splitPart = spliterator.trySplit();
System.out.println("First part:");
splitPart.forEachRemaining(System.out::println);
System.out.println("Second part:");
spliterator.forEachRemaining(System.out::println);
}
}
Output (may vary):
First part:12Second part:345
Nashorn is a JavaScript engine introduced in Java 8 that allows you to run JavaScript code inside Java applications. It was deprecated in Java 11 and removed in Java 15.
Why was Nashorn introduced?
Before Java 8, Java used the Rhino JavaScript engine, which was:
- Slower
- Less optimized for modern JVMs
Nashorn was introduced to:
- Improve performance
- Provide better JVM integration
- Support modern JavaScript (at that time)
Important: Nashorn Status (Very Important for Interviews)
Deprecated in Java 11
Removed in Java 15
So:
- Java 8 → Fully supported
- Java 11 → Deprecated
- Java 15+ → Not available by default
Alternatives to Nashorn
If you need Java + JavaScript today:
- GraalVM JavaScript (recommended)
- Node.js (external)
- ScriptEngine with other languages (Groovy, Kotlin Script, etc.)
jjs is a command-line JavaScript shell introduced in Java 8 that allows you to execute JavaScript code using the Nashorn JavaScript engine.
The package java.util.function provides built-in functional interfaces used heavily with Lambda expressions and Streams.
A functional interface has exactly one abstract method.
Core Functional Interfaces (Most Important)
1. Predicate<T>
Takes one input and returns boolean
boolean test(T t);
Use case: Filtering conditions
Predicate<Integer> isEven = n -> n % 2 == 0;
2. Function<T, R>
Takes one input, returns one output
R apply(T t);
Use case: Transformation / mapping
Function<String, Integer> length = s -> s.length();
3. Consumer<T>
Takes one input, returns nothing
void accept(T t);
Use case: Performing actions (print, save, log)
Consumer<String> printer = s -> System.out.println(s);
4. Supplier<T>
Takes no input, returns output
T get();
Use case: Lazy value generation
Supplier<Double> random = () -> Math.random();
Two-Argument Functional Interfaces
1. BiPredicate<T, U>
boolean test(T t, U u);
BiPredicate<Integer, Integer> greater = (a, b) -> a > b;
2. BiFunction<T, U, R>
R apply(T t, U u);
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
3. BiConsumer<T, U>
void accept(T t, U u);
BiConsumer<String, Integer> print = (s, i) ->
System.out.println(s + " " + i);
B. Stream API & Coding-Based Questions
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
Using distinct()
List<Integer> list = Arrays.asList(1, 2, 2, 3, 4, 4, 5);
List<Integer> uniqueList = list.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(uniqueList);
Output
[1, 2, 3, 4, 5]
1. Using partitioningBy()
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Map<Boolean, List<Integer>> result =
numbers.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
List<Integer> evenNumbers = result.get(true);
List<Integer> oddNumbers = result.get(false);
System.out.println("Even: " + evenNumbers);
System.out.println("Odd: " + oddNumbers);
Output
Even: [2, 4, 6, 8, 10]Odd: [1, 3, 5, 7, 9]
2. Using groupingBy()
Map<Boolean, List<Integer>> result =
numbers.stream()
.collect(Collectors.groupingBy(n -> n % 2 == 0));
1. Using groupingBy() + counting()
String str = "java stream";
Map<Character, Long> frequencyMap =
str.chars()
.mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(
c -> c,
Collectors.counting()
));
System.out.println(frequencyMap);
Output
{ =1, a=2, j=1, m=1, r=1, s=1, t=1, v=1}
2. Ignore spaces / case-insensitive (Very common follow-up)
String str = "Java Stream";
Map<Character, Long> frequencyMap =
str.toLowerCase()
.chars()
.filter(c -> c != ' ')
.mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.counting()
));
Output
{a=3, r=1, s=1, t=1, e=1, v=1, j=1, m=1}
1. Sort numbers in reverse order
List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 3);
List<Integer> sortedDesc =
numbers.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
System.out.println(sortedDesc);
Output
[8, 5, 3, 2, 1]
2. Sort strings in reverse order
List<String> names = Arrays.asList("Java", "Spring", "Stream", "Lambda");
List<String> sortedDesc =
names.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
Output
[Stream, Spring, Lambda, Java]
Merge two lists and sort (Natural order)
List<Integer> list1 = Arrays.asList(5, 1, 3);
List<Integer> list2 = Arrays.asList(4, 2, 6);
List<Integer> mergedSorted =
Stream.concat(list1.stream(), list2.stream())
.sorted()
.collect(Collectors.toList());
System.out.println(mergedSorted);
Output
[1, 2, 3, 4, 5, 6]
1. Basic example (Most common)
List<Integer> numbers = Arrays.asList(10, 12, 15, 18, 20, 22, 25);
List<Integer> divisibleByFive =
numbers.stream()
.filter(n -> n % 5 == 0)
.collect(Collectors.toList());
System.out.println(divisibleByFive);
Output
[10, 15, 20, 25]
2. Using IntStream (Primitive stream – better performance)
IntStream.of(10, 12, 15, 18, 20, 22, 25)
.filter(n -> n % 5 == 0)
.forEach(System.out::println);
3. From a range of numbers (Interview follow-up)
List<Integer> result =
IntStream.rangeClosed(1, 50)
.filter(n -> n % 5 == 0)
.boxed()
.collect(Collectors.toList());
List<String> names = Arrays.asList("java", "stream", "lambda");
List<String> upperCaseList =
names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upperCaseList);
Output
[JAVA, STREAM, LAMBDA]
List<Integer> numbers = Arrays.asList(10, 25, 5, 40, 15);
Optional<Integer> max =
numbers.stream().max(Integer::compareTo);
Optional<Integer> min =
numbers.stream().min(Integer::compareTo);
System.out.println("Max: " + max.get());
System.out.println("Min: " + min.get());
Output
Max: 40Min: 5
Employee class
class Employee {
private int id;
private String department;
private double salary;
// getters
}
Group employees by department
Map<String, List<Employee>> employeesByDept =
employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
Output
{ IT = [Employee1, Employee2], HR = [Employee3], FINANCE = [Employee4]}
1. Basic example (List → Map)
List<String> names = Arrays.asList("Java", "Spring", "Stream");
Map<String, Integer> map =
names.stream()
.collect(Collectors.toMap(
name -> name,
name -> name.length()
));
Output
{Java=4, Spring=6, Stream=6}
2. Convert List of Objects to Map (Most common interview case)
Employee class
class Employee {
private int id;
private String name;
private double salary;
// getters
}
Map<Integer, Employee> employeeMap =
employees.stream()
.collect(Collectors.toMap(
Employee::getId,
e -> e
));
1. Basic joining (No delimiter)
List<String> words = Arrays.asList("Java", "Stream", "API");
String result =
words.stream()
.collect(Collectors.joining());
System.out.println(result);
Output
JavaStreamAPI
2. Joining with a delimiter (Most common)
String result =
words.stream()
.collect(Collectors.joining(", "));
Output
Java, Stream, API
First, we convert the string into a stream of characters, count how many times each character appears, keep the order using LinkedHashMap, and then find the first character with count 1.
String input = "swiss";
Character result = input.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);
System.out.println(result); // w
Output
w
s → repeated
w → appears once (first non-repeated)
String input = "madam";
boolean isPalindrome = IntStream.range(0, input.length() / 2)
.allMatch(i -> input.charAt(i) == input.charAt(input.length() - i - 1));
System.out.println(isPalindrome); // true
String sentence = "java is easy and java is powerful";
Map<String, Long> wordCount = Arrays.stream(sentence.split("\s+"))
.collect(Collectors.groupingBy(
word -> word,
Collectors.counting()
));
System.out.println(wordCount);
Output
{java=2, is=2, easy=1, and=1, powerful=1}
Sort by Values – Ascending
Map<String, Integer> map = new HashMap<>();
map.put("A", 3);
map.put("B", 1);
map.put("C", 2);
Map<String, Integer> sortedMap = map.entrySet()
.stream()
.sorted(Map.Entry.comparingByValue())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(e1, e2) -> e1,
LinkedHashMap::new
));
System.out.println(sortedMap);
Output
{B=1, C=2, A=3}
Sort by Values – Descending
Map<String, Integer> sortedMap = map.entrySet()
.stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(e1, e2) -> e1,
LinkedHashMap::new
));
Output
{A=3, C=2, B=1}
C. Functional Interfaces & Lambda Expressions
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
One-Line Explanation (Interview Friendly)
- Supplier → Takes no input, returns result
- Predicate → Takes input, returns boolean
- Function → Takes input, returns result
- Consumer → Takes input, returns nothing
Comparison Table (Very Important)
| Interface | Method | Input | Output | Purpose |
|---|---|---|---|---|
Predicate<T> | test(T t) | Yes | boolean | Condition checking |
Function<T,R> | apply(T t) | Yes | R | Transformation |
Consumer<T> | accept(T t) | Yes | void | Perform action |
Supplier<T> | get() | No | T | Provide value |
Both BiFunction and BinaryOperator are functional interfaces introduced in Java 8. They are used when a lambda expression needs to work with two input values.
BiFunction
BiFunction<T, U, R>
- Takes two inputs
- Returns one result
- Input types and return type can be different
Example
BiFunction<Integer, Integer, String> add =
(a, b) -> "Sum: " + (a + b);
System.out.println(add.apply(10, 20)); // Sum: 30
BinaryOperator
BinaryOperator<T>
- Takes two inputs
- Returns a result of the same type
- Special case of
BiFunction<T, T, T>
Example
BinaryOperator<Integer> add = (a, b) -> a + b;
System.out.println(add.apply(10, 20)); // 30
Yes, we can create our own functional interface in Java by defining an interface with exactly one abstract method. A custom functional interface is an interface with one abstract method, used to implement lambda expressions.
How to Create a Functional Interface?
- Create an interface
- Add only one abstract method
- (Optional but recommended) Use
@FunctionalInterfaceannotation
Example: Custom Functional Interface
@FunctionalInterface
interface Calculator {
int add(int a, int b);
}
Using It with Lambda Expression
public class Test {
public static void main(String[] args) {
Calculator calc = (a, b) -> a + b;
System.out.println(calc.add(10, 20));
}
}
Lambda expression is a shorter way to implement a functional interface, while an anonymous class creates a full inner class.
| Lambda Expression | Anonymous Class |
|---|---|
| Introduced in Java 8 | Available since early Java |
| Used only with functional interfaces | Can be used with any interface or abstract class |
| Short and concise syntax | Verbose syntax |
| No method name or class name | Creates an unnamed inner class |
this refers to enclosing class | this refers to anonymous class instance |
| No state (no instance variables) | Can have instance variables |
| Better performance (lightweight) | Slightly heavier |
In Java, effectively final variables are local variables whose values are assigned only once, even though they are not explicitly declared as final.
Lambda expressions can access local variables only if they are final or effectively final.
Example of Effectively Final Variable
int x = 10; // effectively final
Runnable r = () -> {
System.out.println(x);
};
x is not declared final, but since it is not modified, it is effectively final.
Example of NOT Effectively Final
int x = 10;
Runnable r = () -> {
// System.out.println(x);
};
x = 20; // modified
Compilation error if x is used in lambda.
No, lambda expressions cannot throw checked exceptions directly unless the functional interface method declares that exception. It can throw a checked exception only if the functional interface allows it.
Example: Checked Exception (Compile-Time Error)
Runnable r = () -> {
Thread.sleep(1000); // Checked exception
};
Compile-time error because Runnable.run() does not declare throws InterruptedException.
Example: Allowed Checked Exception
@FunctionalInterface
interface Task {
void execute() throws InterruptedException;
}
Task task = () -> {
Thread.sleep(1000); // OK
};
A constructor reference is a special type of method reference that refers to a class constructor using the ::new syntax. It is used when a lambda expression only creates a new object.
Syntax of constructor reference
ClassName::new
Example: Lambda vs Constructor Reference
Lambda Expression
Supplier<List<String>> list = () -> new ArrayList<>();
Constructor Reference
Supplier<List<String>> list = ArrayList::new;
Example of constructor reference with Parameters
BiFunction<Integer, String, User> userCreator = User::new;
User user = userCreator.apply(1, "Prakash");
Method references improve readability by providing a shorter and clearer syntax when a lambda expression only calls an existing method. Basically Method references remove unnecessary lambda code and clearly show which method is being called.
Example: Lambda vs Method Reference
Lambda Expression
list.forEach(n -> System.out.println(n));
Method Reference
list.forEach(System.out::println);
The intent is immediately clear: print each element.
D. Date & Time API (java.time)
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
The old Date and Calendar API in Java had several design problems that made it difficult to use. The APIs were mutable, which caused issues in multi-threaded applications and made them not thread-safe.
Another major issue was the confusing API design. Months were zero-based, years were counted from 1900, and many methods were unclear or deprecated, which often led to bugs.
The old APIs also had poor support for date calculations and time zones. Simple operations like adding days or handling different time zones required complex code.
Because of these problems, Java 8 introduced the new Date and Time API (java.time), which is immutable, thread-safe, and easy to use.
In Java 8, LocalDate, LocalTime, and LocalDateTime are part of the java.time API. They are immutable, thread-safe classes used to represent date and time without timezone.
LocalDate
LocalDate represents only a date — year, month, and day.
Used when you need just the date (for example: birthday).
LocalDate date = LocalDate.now();
LocalTime
LocalTime represents only time — hour, minute, second, nanosecond.
Used when you need just the time (for example: store opening time).
LocalTime time = LocalTime.now();
LocalDateTime
LocalDateTime represents both date and time — without timezone.
Used when date and time are required together.
LocalDateTime dateTime = LocalDateTime.now();
In Java 8, we use the java.time API to get the current date and time. The current date and time are obtained using the now() method of LocalDate, LocalTime, and LocalDateTime.
Get Current Date
LocalDate currentDate = LocalDate.now();
Get Current Time
LocalTime currentTime = LocalTime.now();
Get Current Date and Time
LocalDateTime currentDateTime = LocalDateTime.now();
In Java 8, we use DateTimeFormatter (from java.time.format) to format date and time objects like LocalDate, LocalTime, and LocalDateTime into a readable string.
Example
LocalDate date = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
String formattedDate = date.format(formatter);
System.out.println(formattedDate);
In Java 8, we calculate the difference between two dates using the java.time API, mainly with Period or ChronoUnit. We use Period for years, months, and days, and ChronoUnit for total difference in specific units.
Using Period (Years, Months, Days)
LocalDate startDate = LocalDate.of(2024, 1, 1);
LocalDate endDate = LocalDate.of(2026, 2, 1);
Period period = Period.between(startDate, endDate);
System.out.println(period.getYears()); // 2
System.out.println(period.getMonths()); // 1
System.out.println(period.getDays()); // 0
Use Period when you need date-based differences.
Using ChronoUnit (Total Days, Months, etc.)
long days = ChronoUnit.DAYS.between(startDate, endDate);
System.out.println(days);
Use ChronoUnit when you need exact units.
In Java 8, we convert the old java.util.Date to LocalDate by using Instant and ZoneId. We first convert Date to Instant, then apply a time zone, and finally convert it to LocalDate.
Code Example
Date date = new Date();
LocalDate localDate = date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDate();
System.out.println(localDate);
Why ZoneId Is Needed?
Datecontains date + timeLocalDatecontains only date- Time zone is required to correctly convert time to date
E. Advanced / Experience-Level Questions
Expand a question to read the answer. Tick when you’re prepared (progress saved on this device).
A parallel stream is a type of Java Stream that processes data in concurrently using multiple threads from the Fork/Join framework.
It helps improve performance by dividing tasks across multiple CPU cores.
How to Create a Parallel Stream
list.parallelStream();
or
list.stream().parallel();
Code Example
list.parallelStream()
.filter(n -> n % 2 == 0)
.forEach(System.out::println);
When Should You Use Parallel Streams?
- Large data sets
- CPU-intensive operations
- Independent operations (no shared state)
- Performance testing shows improvement
Sequential stream processes data in order using one thread, while parallel stream processes data using multiple threads.
| Sequential Stream | Parallel Stream |
|---|---|
| Processes elements one by one | Processes elements simultaneously |
| Uses single thread | Uses multiple threads |
| Maintains encounter order | Order not guaranteed |
| Predictable performance | Performance depends on CPU & data |
| Best for small datasets | Best for large datasets |
| Safer with shared data | Risky with shared mutable data |
Lazy evaluation in Java Streams means that stream operations are not executed immediately. They are executed only when a terminal operation is called.
In short we can say Streams don’t process data until a terminal operation is called.
Code Example
list.stream()
.filter(n -> {
System.out.println("Filtering: " + n);
return n > 2;
})
.map(n -> {
System.out.println("Mapping: " + n);
return n * 2;
});
No output because there is no terminal operation.
A short-circuiting operation in Java Streams is an operation that stops processing the stream as soon as the result is found, without processing all elements.
Common Short-Circuiting Operations
findFirst()findAny()anyMatch()allMatch()noneMatch()limit()
Code Example
list.stream()
.filter(n -> n > 5)
.findFirst();
Stream stops once the first matching element is found.
Side effects in Streams occur when a stream operation modifies external state (like changing a variable, collection, or object outside the stream pipeline).
They are dangerous, especially with parallel streams, because they can cause incorrect and unpredictable results.
Side effects should be avoided using collectors and immutable operations.
Example of Side Effect
List<Integer> result = new ArrayList<>();
list.stream()
.filter(n -> n % 2 == 0)
.forEach(n -> result.add(n)); // side effect
Modifying an external list inside stream.
Why Are Side Effects Bad?
- Break functional programming principles
- Cause thread-safety issues
- Lead to unexpected behavior in parallel streams
How to Avoid Side Effects?
Use Collectors
List<Integer> result = list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
Avoid Modifying External Variables
int sum = 0;
list.forEach(n -> sum += n);
int sum = list.stream().mapToInt(Integer::intValue).sum();
Use Immutable Objects
- Prefer immutable data structures
- Avoid shared mutable state
Important Interview Point
- Side effects are especially dangerous in parallel streams
- Streams should be stateless and non-interfering
CompletableFuture is a class introduced in Java 8 for asynchronous programming. It represents a future result of an asynchronous computation and allows us to write non-blocking, reactive code.
It can also define as CompletableFuture is used to run tasks asynchronously and process their results without blocking the main thread.
Basic Example of CompletableFuture
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> "Hello");
System.out.println(future.get()); // Hello
Why CompletableFuture Was Introduced?
- Overcomes limitations of
Future - Supports chaining of tasks
- Provides better exception handling
- Enables non-blocking execution
Commonly Used Methods
| Method | Purpose |
|---|---|
supplyAsync() | Run task and return result |
runAsync() | Run task without result |
thenApply() | Transform result |
thenAccept() | Consume result |
thenCompose() | Chain async tasks |
exceptionally() | Handle errors |
Chaining Example
CompletableFuture.supplyAsync(() -> 10)
.thenApply(n -> n * 2)
.thenAccept(System.out::println);