Java 8 new features

  1. List out some important features of Java 8 
  2. What is the use of Lambda expressions in Java 8 ?

  3. What is the difference between Lambda expression and an Anonymous inner class ?

  4. Can you explain the concept of effectively final variables in the context of Lambda Expressions?

  5. How would you debug a Lambda Expression in a complex chain of operations?

  6. What is Functional Interface ?

  7. What is Stream API in Java and How does it differ from traditional Java Collections ?

  8. What is the difference between intermediate and terminal operations in Stream API ?

  9. How would you optimize a Stream pipeline for performance in a large dataset?

  10. Serial Stream vs Parallel Stream ?

  11. What are the benefits and pitfalls of using parallelStream()?

  12. How do you handle exceptions in Stream operations?

  13. Can you explain the Collectors class and its commonly used methods?

  14. What is the difference between map() and flatMap() in Streams?

  15. What is the use of Optional Class in Java ?

  16. How would you chain Optional operations to handle complex null checks?

  17. What is the use of default method in the interface?

  18. Default method vs static method of Interfaces ?

  19. What is the use of Date and Time API?

  20. What is Method Reference in Java ?
  21. What is the Nashorn JavaScript Engine introduced in Java 8, and how is it used?
  22. What is CompletableFuture class and its use? 

List out some important features of Java 8 

  1. Functional Interface (Single Abstract Method Interface)

  2.  Allowing implementation for default methods and static methods in the interface 

  3.  Lambda Expressions 

  4.  Method References 

  5.  Stream API 

  6.  Collection API improvements (ex : removeIf, replaceAll)

  7.  Concurrency API improvements (ex : CompletableFuture class)

  8.  Optional Class to handle null pointer exceptions 

  9.  Date/Time API 

  10.  String Joiner
  11. Nashorn JavaScript Engine
  12. 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 ?


Anonymous Inner class

Lambda Expression

Syntax

Requires full declaration

new Interface() { ... }


Concise coding 


(parameters) -> expression 

Example

// 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);




Can implement interfaces or extend classes with multiple methods.


Limited to functional interfaces (Single abstract method Interface).


Debugging

Easier to debug as it resembles a regular class with a clear structure.



Harder to debug due to lack of explicit class structure.


Performance

Less efficient compared to Lambda expressions


Slightly more efficient; uses invokedynamic bytecode, avoiding class creation.


It reduces the boilerplate code


When to use : 

Use for functional interfaces, concise code, and Stream API operations.


Use for non-functional interfaces, complex logic, or when multiple methods need implementation.


We can use @Override also 


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 : 

  1. Intermediate Operations : Operations like filter, map, sorted, etc., that transform the stream and are lazy (not executed until a terminal operation is called).

  2. Terminal Operations : Operations like collect, forEach, reduce, or count that produce a result and trigger the stream processing.

Stream API vs Traditional Collections 

Stream API

Traditional Collections

Designed for Processing data in a pipelined based manner

Designed for storing and managing data

Does not store data. Acts as a pipeline to process the data from a source

Stores data in memory (eg. ArrayList)

Does not modify source data

Can modify the collection (Ex. add, remove elements)

Stream API handles Iteration internally

Explicit iteration needed (eg. for, while loop)

A Stream can be consumed only once. Attempting to reuse it throws an exception

Collections can be reused multiple times


Optimized for bulk datasets and parallel processing. 

Better for small data sets or direct data manipulation

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 ?

Intermediate Operations

Terminal Operations 

Transform or filter the stream, producing another stream as output

Triggers the Intermediate operations in the pipeline and produces the final result

Lazy Evaluation (Executed only when Terminal operation is called)

Eager Evaluation (Triggers the pipeline)

Returns stream as output

Returns a non-stream result (eg. list, int, void) 

Can be chained

Ends the pipeline, no further chaining

Examples : filter(), map(), sorted()

Examples : collect(), forEach(), reduce()

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 ?

Serial Stream

Parallel Stream

=> Sequential execution on single thread
=> Processes elements sequentially in a single thread

=> Parallel execution using ForkJoinPool (common pool).

=> common pool size (Runtime.availableProcessors() - 1)
=> Processes elements concurrently across multiple threads 

All operations (eg. filter, map) are executed in the order defined, one element at a time

Splits the stream into chunks, process them in parallel, and combine results

Better for small datasets

Better for large datasets

No concurrency issues 

Requires thread-safe operations

Method : stream ()

Method : parallelStream() or stream().parallel()

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?

map()

flatMap()

map() transforms elements into a different type or value without changing the stream’s structure

flatMap() transforms elements into a different type or value but flattening the stream’s structure

Visual difference

Stream<T> → map(f: T → R) → Stream<R>

[a, b, c] → map(f: a → x) → [x, y, z]


Visual difference

Stream<T> → flatMap(f: T → Stream<R>) → Stream<R>

[a, b, c] → flatMap(f: a → [x1, x2]) → [x1, x2, y1, y2, z1, z2]


Example :

List<String> sentences = Arrays.asList("Hello World", "Java Streams");

// Using map: Stream of arrays

List<String[]> words = sentences.stream()

                               .map(s -> s.split(" "))

                               .collect(Collectors.toList());

// Result: [[Hello, World], [Java, Streams]]



Example :

List<String> sentences = Arrays.asList("Hello World", "Java Streams");


// Using flatMap: Flattened stream of words

List<String> flatWords = sentences.stream()

                                 .flatMap(s -> Arrays.stream(s.split(" ")))

                                 .collect(Collectors.toList());

// Result: [Hello, World, Java, Streams]

Performs 1-to-1 transformations without flattening the stream structure

Performs 1-to-many transformations and flattening the nested streams into single stream

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, 

  1. empty() method 

  2. of() method 

  3. ofNullable() method 

  4. get() method 

  5. isPresent() method 

  6. isEmpty() method 

  7. ifPresent() method 

  8. ifPresentOrElse() method 

  9. orElse() method 

  10. orElseGet() method 

  11. orElseThrow() method 

  12. or() method 

  13. filter() method with Optional 

  14. 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

default method in Interface

static method in Interface

Instance method with a default implementation

Utility method with implementation but tied to the Interface itself 

keyword default

keyword static

Invoked by using the instance of implementing class

Invoked by using the Interface name

Can be overridden by implementing classes 

Cannot be overridden

Use case example : The java.util.Collection interface uses default methods like forEach and stream to provide iteration and streaming capabilities without requiring implementing classes


Use case example : The java.util.Comparator interface has a static method comparing to create comparators:


Comparator<String> comparator = Comparator.comparing(String::length);


What is the use of Date and Time API?

=> Introduced in Java 8 (Package : java.time) 
=> It provides modern, immutable, thread safe way to handle date, time and durations replacing the flawed java.util.Date and Calendar 
=> We have some mutability and thread safely issues in older APIs 
Key uses : 
=> Simplified Date/Time Operations: Classes like LocalDate, LocalTime, and LocalDateTime handle dates and times without time zones, e.g., LocalDate.now() for current date.
=> Time Zone Support: ZonedDateTime and ZoneId manage time zones, e.g., ZonedDateTime.now(ZoneId.of("America/New_York")).
=> Duration and Period: Calculate time intervals with Duration (time-based, e.g., seconds) and Period (date-based, e.g., days).
=> Formatting and Parsing: DateTimeFormatter for custom date/time formats, e.g., LocalDate.parse("2025-09-13", DateTimeFormatter.ISO_LOCAL_DATE).


What is Method Reference in Java ?

=> A shorthand syntax for a lambda expression
=> Syntax : ClassName::methodName or instance::methodName
Some Examples : 
Integer::parseInt (instead of x -> Integer.parseInt(x))
System.out::println (instead of x -> System.out.println(x))
String::toUpperCase (instead of x -> x.toUpperCase())
ArrayList::new (instead of () -> new ArrayList<>())
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(System.out::println); // Method reference instead of x -> System.out.println(x)

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?

=> CompletableFuture is a class introduced in Java 8 (package : java.util.concurrent)
=> It implements the Future interface  
Key features of ComletableFuture class : 
1. Asynchronous Execution : 
=> Run tasks in background threads (using ForkJoinPool by default).
=> Methods like supplyAsync(), runAsync().
2. Chaining & Composition (Biggest advantage over plain Future) :
=> thenApply() – transform result (like map).
=> thenAccept() – consume result (like forEach).
=> thenCompose() – chain another CompletableFuture (flatten).
=> thenCombine() – combine two independent futures.
=> exceptionally(), handle() – error handling.
3. Completion Control : 
=> We can manually complete it (complete(value), completeExceptionally(ex)) 
4. Non-blocking :  
Doesn't block the calling thread — perfect for reactive/async code
=> Example : 
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // 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) 
Comparison with Old Future :
FeatureFuture (Java 5)CompletableFuture (Java 8+)
ChainingNoYes (thenApply, thenCompose, etc.)
Error handlingget() throws checkedexceptionally(), handle()
Combine multiple futuresManualthenCombine, allOf, anyOf
Manual completionNoYes (complete(), completeExceptionally())
Async executionLimitedsupplyAsync, runAsync