- List out some important features of Java 8
What is the use of Lambda expressions in Java 8 ?
What is the difference between Lambda expression and an Anonymous inner class ?
Can you explain the concept of effectively final variables in the context of Lambda Expressions?
How would you debug a Lambda Expression in a complex chain of operations?
What is Functional Interface ?
What is Stream API in Java and How does it differ from traditional Java Collections ?
What is the difference between intermediate and terminal operations in Stream API ?
How would you optimize a Stream pipeline for performance in a large dataset?
Serial Stream vs Parallel Stream ?
What are the benefits and pitfalls of using parallelStream()?
How do you handle exceptions in Stream operations?
Can you explain the Collectors class and its commonly used methods?
What is the difference between map() and flatMap() in Streams?
What is the use of Optional Class in Java ?
How would you chain Optional operations to handle complex null checks?
What is the use of default method in the interface?
Default method vs static method of Interfaces ?
What is the use of Date and Time API?
- What is Method Reference in Java ?
- What is the Nashorn JavaScript Engine introduced in Java 8, and how is it used?
- What is CompletableFuture class and its use?
List out some important features of Java 8
Functional Interface (Single Abstract Method Interface)
Allowing implementation for default methods and static methods in the interface
Lambda Expressions
Method References
Stream API
Collection API improvements (ex : removeIf, replaceAll)
Concurrency API improvements (ex : CompletableFuture class)
Optional Class to handle null pointer exceptions
Date/Time API
- String Joiner
- Nashorn JavaScript Engine
- CompletableFuture class
What is the use of Lambda expressions in Java 8
=> Concise Code due to its simplified syntax
=> It can be used to implement the Functional Interface, We can use Lambda expressions instead of Anonymous class so that we can reduce boilerplate code
=> Enhances the APIs like Streams
Syntax : (parameters) -> expression
Functional Interface implementation
package com.tutorialspoint;
public class Tester {
public static void main(String[] args) {
// Functional Interface implementation using anonymous class
Calculator sum = new Calculator() {
@Override
public int operate(int a, int b) {
return a + b;
}
};
int result = sum.operate(2,3);
System.out.println(result);
// Functional Interface implementation using lambda expression
Calculator sum1 = (a,b) -> a + b;
result = sum1.operate(2,3);
System.out.println(result);
}
interface Calculator {
int operate(int a, int b);
}
}
Stream API using Lambda Expression
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
numbers.stream()
.filter(n -> n % 2 == 0)
.forEach(n -> System.out.println(n));
Limitations :
=> Can make debugging harder due to its concise syntax
=> Overuse may reduce code clarity
What is the difference between Lambda expression and an Anonymous inner class ?
Can you explain the concept of effectively final variables in the context of Lambda Expressions?
A variable is effectively final in the following two cases
=> Variable is explicitly declared with the keyword final
=> Variable is not modified after its initialization even though its not declared with the keyword final. Ie That variable is immutable.
What will happen if we try to modify the local variable that is used in lambda expression ?
=> If we try to modify the local variable used in lambda expression, compiler will throw an error “Variable used in Lambda expression should be final or effectively final”
Example :
public class LambdaExample {
public static void main(String[] args) {
int number = 10; // Not declared final, but effectively final if not modified
Runnable r = () -> System.out.println("Number: " + number); // Lambda captures 'number'
r.run();
// number = 20; // If uncommented, this causes a compilation error
}
}
Why is this effectively final rule applicable only for Local variables but not for Instance/Static variables ?
=> Because, Instance/Static variables are tied to the Object or Class lifecycle, not the local scope. Modifying them does not introduce the same concurrency risks as local variables in lambda expressions
Example :
class Test {
int instanceVar = 100; // Not subject to effectively final rule
void method() {
int localVar = 10; // Must be effectively final
Runnable r = () -> System.out.println(localVar + instanceVar);
instanceVar = 200; // Allowed
// localVar = 20; // Compilation error if modified
r.run();
}
}
What is the benefit of effectively final rule :
=> It provides thread safety and code clarity
Is there any workaround to modify the local variable that used in Lambda expression ?
=> By using wrapper classes ( For Example, AtomicInteger (for integers) or AtomicReference (for any object). These are thread safe. As there reference not changing, it works
=> We can use ArrayList, Arrays also, same logic, that reference not changing
How would you debug a Lambda Expression in a complex chain of operations?
=> Breakdown the chain into smaller steps
=> Use method peek to log the values during chain intermediate operations. This is called Intermediate inspection
Example : import java.util.Arrays;
import java.util.List;
public class DebugLambda {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> intermediate = numbers.stream()
.filter(n -> n % 2 == 0) // Step 1: Filter even numbers
.peek(n -> System.out.println("Filtered: " + n)) // Debug output
.map(n -> n * 2) // Step 2: Double the numbers
.peek(n -> System.out.println("Mapped: " + n)) // Debug output
.toList();
System.out.println("Result: " + intermediate);
}
}
Output :
Filtered: 2
Mapped: 4
Filtered: 4
Mapped: 8
Result: [4, 8]
=> Use method limit to test with smaller data sets
Example : List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
numbers.stream()
.limit(3) // Debug with first 3 elements
.filter(n -> n % 2 == 0)
.map(n -> n * 2)
.forEach(System.out::println);
Output: Processes only [1, 2, 3], making it easier to verify logic.
=> Use System.out.println or a logging framework (e.g., SLF4J, Log4j) inside lambda expressions.
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DebugLambda {
private static final Logger logger = LoggerFactory.getLogger(DebugLambda.class);
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "", "Charlie");
names.stream()
.filter(s -> {
logger.debug("Filtering: {}", s);
return !s.isEmpty();
})
.map(s -> {
String result = s.toUpperCase();
logger.debug("Mapping {} to {}", s, result);
return result;
})
.forEach(s -> logger.debug("Final: {}", s));
}
}
=> Use a Debugger with Breakpoints (Modern IDEs (e.g., IntelliJ IDEA, Eclipse) allow setting breakpoints inside or around lambda expressions to inspect variables and flow)
=> Move complex lambda logic to named methods for clarity and testability.
=> Wrap risky operations in try-catch to catch and log exceptions.
=> Verify each operation in the chain before combining them.
What is Functional Interface or SAM Interface ?
=> Functional Interface is also known as Single Abstract Method (SAM) Interface
=> Functional Interface must have Single Abstract Method only but can multiple default and static methods
=> Methods inherited from Object class (Eg. toString(), equals() ) won’t come under SAM restriction
=> Lambda expression can be used to implement the Functional Interface
=> Functional Interface is the backbone for the features Lambda Expressions, Method References and Stream API
=> Functional Interface improves code readability and reduces boiler plate code
=> Annotation @FunctionalInterface is optional but recommended because it ensures compile-time check for Single Abstract Method rule
=> Examples for Predefined Functional Interface :
Predicate<T>: Represents a function that takes an argument and returns a boolean (test(T t)).
Example for Functional Interface :
@FunctionalInterface
public interface MyFunctionalInterface {
void performAction(); // Single abstract method
// Default method (does not count as abstract)
default void defaultMethod() {
System.out.println("This is a default method.");
}
// Static method (does not count as abstract)
static void staticMethod() {
System.out.println("This is a static method.");
}
}
Implementation of Functional Interface using Lambda Expression :
MyFunctionalInterface action = () -> System.out.println("Action performed!");
action.performAction(); // Output: Action performed!
Example with Built-in Functional Interface:
import java.util.function.Predicate;
public class Main {
public static void main(String[] args) {
Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // Output: true
System.out.println(isEven.test(5)); // Output: false
}
}
What is Stream API in Java and How does it differ from traditional Java Collections ?
=> Stream API Designed for processing data in a pipelined based manner
=> Stream API performing two types of operations :
Intermediate Operations : Operations like filter, map, sorted, etc., that transform the stream and are lazy (not executed until a terminal operation is called).
Terminal Operations : Operations like collect, forEach, reduce, or count that produce a result and trigger the stream processing.
Stream API vs Traditional Collections
Example: Stream API vs. Traditional CollectionsTask: Filter even numbers from a list and double them.Using Traditional Collections (Imperative Approach):java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> result = new ArrayList<>();
// External iteration with a loop
for (Integer num : numbers) {
if (num % 2 == 0) { // Filter even numbers
result.add(num * 2); // Double the number
}
}
System.out.println(result); // Output: [4, 8, 12]
}
}
Using Stream API (Declarative Approach):java
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
// Stream-based processing
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * 2)
.collect(Collectors.toList());
System.out.println(result); // Output: [4, 8, 12]
}
}
What is the difference between intermediate and terminal operations in Stream API ?
Example for Intermediate Operations :
1. filter() : Select elements based on a predicate
2. map() : Transforms elements into new form
3. sorted() : Sorts the stream elements
4. distinct() : Removes duplicates
Example for Terminal Operations :
1. collect() : Gathers elements into a collection (eg. toList())
2. forEach() : Performs an action on each element
3. reduce() : Combines elements into a single result
4. count() : Returns the number of the elements
5. findFirst() : Returns the first element if present
Example code :
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> result = names.stream()
.filter(s -> s.startsWith("A")) // Intermediate
.map(String::toUpperCase) // Intermediate
.collect(Collectors.toList()); // Terminal
// result = ["ALICE"]
NOTE : If we don’t put the code for Terminal operation, then nothing will be triggered in the pipeline, no result
How would you optimize a Stream pipeline for performance in a large dataset?
=> Use parallelStream() for CPU-bound tasks on large datasets.
=> Minimize and combine intermediate operations.
=> Filter early to reduce dataset size.
=> Leverage short-circuiting operations.
=> Choose efficient collectors and data structures.
=> Avoid stateful operations like sorted or distinct unless necessary.
=> Use primitive streams (IntStream, etc.) for numeric data.
=> Profile and test with realistic data (Noting down the time taken)
=> Avoid multiple stream creations.
=> Monitor memory usage and GC impact.
=> Consider custom collectors for complex reductions.
Serial Stream vs Parallel Stream ?
Example for Serial Stream :
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> result = names.stream()
.filter(s -> s.startsWith("A"))
.map(String::toUpperCase)
.collect(Collectors.toList()); // ["ALICE"]
Example for Parallel Stream :
List<Integer> largeNumbers = // millions of numbers
long sum = largeNumbers.parallelStream()
.mapToInt(n -> n * 2) // CPU-intensive
.sum();
What are the benefits and pitfalls of using parallelStream()?
Benefits :
=> Splits the stream into chunks, process them in parallel, and combine results. i.e the Parallel Stream utilizing multiple CPU cores
=> Effective on large data sets
=> Efficient for stateless operations (Operations like map and filter are stateless and independent, making them ideal for parallel execution without synchronization overhead.)
Pitfalls :
=> Overhead for small datasets
=> Non-deterministic behavior
=> Thread safety issues
=> Ineffective for stateful operations (Operations like sorted are stateful as they need to hold the elements in the memory)
=> Debugging complexity
How do you handle exceptions in Stream operations?
=> Checked Exceptions : Wrap in try-catch within lambdas or use utility functions to convert to unchecked exceptions.
=> Unchecked Exceptions : Handle locally to prevent pipeline termination, using nulls, default values, or wrapper classes. (In catch we can return null or default values)
=> Parallel Streams : Ensure thread-safe handling and consider custom ForkJoinPool for better control.
Example : Combining approaches
List<String> strings = Arrays.asList("123", "abc", "456");
List<Result<Integer>> results = strings.stream()
.map(s -> {
try {
return Result.success(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Result.failure(e);
}
})
.collect(Collectors.toList());
What is ForkJoinPool?
ForkJoinPool is an implementation of the ExecutorService interface that manages a pool of worker threads to execute tasks that are split into smaller subtasks (forked) and later combined (joined) to produce a final result.
Can you explain the Collectors class and its commonly used methods?
=> Collector class provides factory methods to create collector instances for common reduction operations
=> collect() terminal operation used in stream to provide the result after terminal operations
=> Commonly used methods:
Collections: toList(), toSet(), toMap(), toConcurrentMap(), toCollection().
Aggregation: counting(), summingInt(), averagingInt(), summarizingInt().
Grouping/Partitioning: groupingBy(), partitioningBy().
String Joining: joining().
Custom Reduction: reducing().
Example :
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> filtered = names.stream()
.filter(s -> s.startsWith("A"))
.collect(Collectors.toList());
// Result: [Alice]
NOTE : when we need a collection without duplicates, we have to use Set
List<String> names = Arrays.asList("Alice", "Bob", "Alice");
Set<String> uniqueNames = names.stream()
.collect(Collectors.toSet());
// Result: [Alice, Bob]
What is the difference between map() and flatMap() in Streams?
What is the use of Optional Class in Java ?
=> Optional Class introduced in Java 8
=> It is used to handle absent values in the safe way, reducing the risk of nullPointerException
=> Optional is designed for return type
Commonly used Optional class methods are,
empty() method
of() method
ofNullable() method
get() method
isPresent() method
isEmpty() method
ifPresent() method
ifPresentOrElse() method
orElse() method
orElseGet() method
orElseThrow() method
or() method
filter() method with Optional
map() method with Optional
Refer https://www.youtube.com/watch?v=4BUKaazoYyg
Refer Java Optional Class Methods with Examples
Methods for Creating Optional Objects :
=> empty() method - Optional<Object> emptyOptional = Optional.empty();
=> of() method - Optional<String> emailOptional = Optional.of("ramesh@gmail.com");
=> ofNullable() method - Optional<String> stringOptional = Optional.ofNullable("ramesh@gmail.com");
Example :
import java.util.Optional;
public class OptionalDemo {
public static void main(String[] args) {
String email = "ramesh@gmail.com";
// of, empty, ofNullable
Optional<Object> emptyOptional = Optional.empty();
System.out.println(emptyOptional);
Optional<String> emailOptional = Optional.of(email);
System.out.println(emailOptional);
/ / Note : Optional.of(null); // Throws NullPointerException
Optional<String> stringOptional = Optional.ofNullable(email);
System.out.println(stringOptional);
//Note : The ofNullable() static method returns an Optional describing the specified value, if non-null, otherwise returns an empty Optional.
}
}
Output :
Optional.empty
Optional[ramesh@gmail.com]
Optional[ramesh@gmail.com]
Methods to Get value from Optional :
get() method
=> The get() method returns a value if it is present in this Optional otherwise throws NoSuchElementException
Example :
String email = "ramesh@gmail.com";
Optional<String> stringOptional = Optional.ofNullable(email);
String value = stringOptional.get();
System.out.println(value); // output : ramesh@gmail.com
Methods for checking value presence :
isPresent() method
=> The isPresent() method returns true if the Optional contains a non-null value, otherwise false
Example :
Optional<String> optional = Optional.of("Java");
System.out.println(optional.isPresent()); // Output: true
isEmpty() method
=> isEmpty() method returns true if the Optional is empty, otherwise false.
=> It is the inverse of isPresent()
=> Example :
Optional<String> emptyOptional = Optional.empty();
System.out.println(emptyOptional.isEmpty()); // Output: true
ifPresent() method
=> ifPresent() method performs the given action with the value if present; otherwise, does nothing.
=> Example :
Optional<String> optional = Optional.of("Hello");
optional.ifPresent(value -> System.out.println("Value: " + value)); // Output: Value: Hello
ifPresentOrElse() method
=> ifPresentOrElse() method performs the given action if a value is present; otherwise, executes the empty action.
=> Example :
Optional<String> optional = Optional.empty();
optional.ifPresentOrElse(
value -> System.out.println("Value: " + value),
() -> System.out.println("No value present")
); // Output: No value present
Methods to retrieve default values :
=> orElse() Method
=> The orElse() method returns the value if present, otherwise returns the other (default value).
Example :
String email = null;
Optional<String> stringOptional = Optional.ofNullable(email);
String defaultOptional = stringOptional.orElse("default@gmail.com");
System.out.println(defaultOptional); //output : default@gmail.com
=> orElseGet() method
=> The orElseGet() method returns the value if present, otherwise invoke other and return the result of that invocation.
=> Example 1 :
String email = null;
Optional<String> stringOptional = Optional.ofNullable(email);
String defaultOptional2 = stringOptional.orElseGet(() -> "default@gmail.com");
System.out.println(defaultOptional2); // output : default@gmail.com
=> Example 2 :
String email = "ramesh@gmail.com";
Optional<String> stringOptional = Optional.ofNullable(email);
String defaultOptional2 = stringOptional.orElseGet(() -> "default@gmail.com");
System.out.println(defaultOptional2); // output : ramesh@gmail.com
=> orElseThrow() method
=> The orElseThrow() method returns the contained value, if present, otherwise throws an exception to be created by the provided supplier.
=> Example :
String email = null;
Optional<String> stringOptional = Optional.ofNullable(email);
String optionalObject = stringOptional.orElseThrow(() -> new IllegalArgumentException("Email is not exist"));
System.out.println(optionalObject);
Output :
Exception in thread "main" java.lang.IllegalArgumentException: Email is not exist
at com.java.lambda.optional.OptionalDemo.lambda$main$0(OptionalDemo.java:10)
at java.base/java.util.Optional.orElseThrow(Optional.java:403)
at com.java.lambda.optional.OptionalDemo.main(OptionalDemo.java:10)
=> or() method
=> or() method returns the current Optional if it contains a value; otherwise, returns the Optional produced by the supplier.
=> Example :
Optional<String> optional = Optional.empty();
Optional<String> result = optional.or(() -> Optional.of("Fallback"));
System.out.println(result.get()); // Output: Fallback
Optional filter() and map() Methods :
=> filter() method : If a value is present, and the value matches the given predicate, return an Optional describing the value, otherwise return an empty Optional.
=> map() method : If a value is present, apply the provided mapping function to it, and if the result is non-null, return an Optional describing the result. otherwise, returns an empty Optional.
=> Example :
String result = " abc ";
if(result != null && result.contains("abc")){
System.out.println(result);
}
Optional<String> optionalStr = Optional.of(result);
optionalStr.filter(res -> res.contains("abc"))
.map(String::trim)
.ifPresent((res) -> System.out.println(res));
Output :
abc
abc
How would you chain Optional operations to handle complex null checks?
=> Use business rules filter() operations early to simplify the chain
=> Break long chains into multiple lines, use either lambda or Method references for clarity
=> Always use flatMap when a method returns an Optional. This is to avoid nested Optional<Optional<T>>
=> Avoid overusing get(), prefer orElse, orElseGet, orElseThrow
=> Ensure the chain finally produces meaningful result by using or, orElse, IfPresentOrElse
=> Example :
Suppose you’re working with a system where a Department has an Optional<Manager>, a Manager has an Optional<Employee>, and an Employee has an Optional<String> email. You want to get the manager’s employee’s email domain if the email is valid (contains "@").
class Employee {
private final String email;
Employee(String email) {
this.email = email;
}
Optional<String> getEmail() {
return Optional.ofNullable(email);
}
}
class Manager {
private final Employee employee;
Manager(Employee employee) {
this.employee = employee;
}
Optional<Employee> getEmployee() {
return Optional.ofNullable(employee);
}
}
class Department {
private final Manager manager;
Department(Manager manager) {
this.manager = manager;
}
Optional<Manager> getManager() {
return Optional.ofNullable(manager);
}
}
public class OptionalChaining {
public static void main(String[] args) {
Department department = new Department(new Manager(new Employee("john.doe@company.com")));
String domain = department.getManager()
.flatMap(Manager::getEmployee)
.flatMap(Employee::getEmail)
.filter(email -> email.contains("@"))
.map(email -> email.substring(email.indexOf("@") + 1))
.orElse("no-domain.com");
System.out.println(domain); // Output: company.com
}
}
What is the use of default method in the interface?
=> Before Java 8, adding a new method to interface would break all existing implementing classes as they need to implement the new method.
=> Java 8 introduced default methods in Interface. default methods can have implementation in the interface. So the implementing classes can use the default method as it is or can override. It provides backward compatibility and reduces boiler plate code.
=> Note : Multiple Inheritance Conflicts: If a class implements multiple interfaces with conflicting default methods (same method signature), the class must explicitly override the method to resolve the conflict.
=> Example use case :
The Java 8 java.util.Collection interface added default methods like stream() and forEach() without breaking existing implementations.
interface Collection<T> {
default void forEach(Consumer<? super T> action) {
for (T item : this) {
action.accept(item);
}
}
}
=> Syntax of Default Methods :
interface MyInterface {
// Abstract method (no implementation)
void abstractMethod();
// Default method with implementation
default void defaultMethod() {
System.out.println("This is a default implementation");
}
}
Default method vs static method of Interfaces ?
=> Both are introduced in Java 8
What is the use of Date and Time API?
Key uses :
What is Method Reference in Java ?
What is the Nashorn JavaScript Engine introduced in Java 8, and how is it used?
=> Nashorn JavaScript Engine, introduced in Java 8
=> It replaced the older Rhino engine
=> Nashorn allows developers to execute JavaScript code within Java applications and enables seamless interaction between Java and JavaScript.
=> Allows calling Java methods from JavaScript and vice versa.
=> Built on invokedynamic (Java 7 feature), Nashorn optimizes JavaScript execution for speed.
=> Nashorn is accessed via the javax.script API
=> Example: Basic JavaScript Execution :
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class NashornExample {
public static void main(String[] args) throws ScriptException {
// Create ScriptEngineManager
ScriptEngineManager manager = new ScriptEngineManager();
// Get Nashorn engine
ScriptEngine engine = manager.getEngineByName("nashorn");
// Execute JavaScript code
engine.eval("print('Hello from Nashorn!');");
}
}
What is CompletableFuture class and its use?
=> thenAccept() – consume result (like forEach).
=> thenCompose() – chain another CompletableFuture (flatten).
=> thenCombine() – combine two independent futures.
=> exceptionally(), handle() – error handling.
// Simulate long task
try { Thread.sleep(1000); } catch (Exception e) {}
return "Hello";
})
.thenApply(s -> s + " World") // Transform
.thenApply(String::toUpperCase); // Chain another transform
System.out.println(future.join()); // HELLO WORLD (join waits for result from running thread to be in sync)
| Feature | Future (Java 5) | CompletableFuture (Java 8+) |
|---|---|---|
| Chaining | No | Yes (thenApply, thenCompose, etc.) |
| Error handling | get() throws checked | exceptionally(), handle() |
| Combine multiple futures | Manual | thenCombine, allOf, anyOf |
| Manual completion | No | Yes (complete(), completeExceptionally()) |
| Async execution | Limited | supplyAsync, runAsync |