Top 60 Java 8 Interview Questions & Answers For All Levels (2024)
Java 8 is a significant update to the Java programming language, with its first release in 2014. It introduced important features like lambda expressions, the Stream API, and new date and time APIs. As these updates have transformed the way we write and optimize Java code, it's crucial to be well-versed in them for any Java-related interview.
In this article, we’ll explore key Java 8 interview questions and answers that will help you understand and articulate these new features, ensuring you're well-prepared to showcase your knowledge and skills in your next Java interview.
Java 8 Interview Questions: Beginner
1. What is Java 8? When was it introduced?
Java 8 is a major release of the Java programming language, introduced by Oracle on March 18, 2014. This version brought significant enhancements to the language, including lambda expressions for more concise and functional-style code, the Stream API for efficient data processing, and new features like the Optional class to handle null values more gracefully. Java 8's updates marked a shift towards more modern programming practices and improved performance, setting a new standard for Java development.
2. What are the main features introduced in Java 8?
Java 8 introduced several key features that significantly enhanced the language and its ecosystem:
- Lambda Expressions: Provide a concise way to express instances of single-method interfaces (functional interfaces) using a clear and readable syntax.
- Stream API: Offers a powerful and flexible framework for processing sequences of elements (e.g., collections) in a functional style, enabling operations like filtering, mapping, and reducing.
- Optional Class: Helps avoid NullPointerException by representing a value that may or may not be present, promoting more robust code handling.
- Default Methods: Allow interfaces to have method implementations, enabling the addition of new methods to interfaces without breaking existing implementations.
- Method References: Provide a shorthand notation for calling methods, making code more readable and expressive.
- New Date and Time API: Introduces a comprehensive and immutable date-time library (java.time) that replaces the old java.util.Date and java.util.Calendar classes.
- Nashorn JavaScript Engine: Replaces the older Rhino engine, offering improved performance and compatibility for executing JavaScript code within Java applications.
- Parallel Streams: Enable parallel processing of data streams across different operating system, leveraging multi-core processors for improved performance in data-heavy operations.
3. What is a Lambda expression, and how is it used in Java 8?
A lambda expression in Java 8 is a concise way to represent an instance of a functional interface (an interface with a single abstract method) using a more readable and expressive syntax. It allows us to pass behavior as parameters and perform operations in a functional style.
Syntax of Lambda Expression:
(parameters) -> expression
Usage: Lambda expressions are commonly used with functional interfaces such as Runnable, Callable, or custom interfaces. They are especially useful with the Stream API for operations like filtering, mapping, and reducing collections.
4. What is a Functional Interface in Java 8?
A functional interface in Java 8 is an interface that has exactly one abstract method. It can have multiple default or static methods, but only one abstract method. Functional interfaces are used primarily with lambda expressions and method references, providing a target type for these constructs.
Key Characteristics:
- Single Abstract Method: A functional interface contains exactly one abstract method. This method represents the behavior that lambda expressions or method references will implement.
- Default and Static Methods: Functional interfaces can have any number of default or static methods. These methods have a body and do not count towards the abstract method count.
- @FunctionalInterface Annotation: Although not required, it’s a best practice to use the @FunctionalInterface annotation to indicate that the interface is intended to be functional. This annotation helps catch errors at compile-time if the interface does not meet the functional interface criteria.
5. What is the purpose of the default method in interfaces?
The purpose of default methods in interfaces, introduced in Java 8, is to allow interfaces to provide method implementations directly. This feature enables the following benefits:
- Backward Compatibility: Default methods help maintain backward compatibility by allowing developers to add new methods to interfaces without breaking existing implementations. This is crucial for evolving APIs and libraries without forcing all existing classes to be updated.
- Code Reusability: By providing a default implementation, interfaces can include common functionality that can be shared across multiple implementing classes. This reduces code duplication and promotes consistency.
- Enhanced Flexibility: Default methods allow interfaces to offer some level of behavior implementation while still adhering to the principle of abstraction. Implementing classes can either use the default implementation or override it with a specific one.
6. How does the Stream API work, and why is it used?
The Stream API in Java 8 is a powerful tool designed to process sequences of elements (such as collections) in a functional and declarative style. It simplifies complex data manipulations and improves code readability and performance by leveraging parallel processing capabilities.
Here's how the Stream API Works:
1. Stream Creation: Streams can be created from various data sources, including collections (e.g., lists, sets), arrays, or other input sources. For example:
List<String> names = Arrays.asList("Alia", "Bhaskar", "Chandra");
Stream<String> stream = names.stream();
2. Intermediate Operations: These operations transform the stream into another stream. They are lazy, meaning they don’t process the data until a terminal operation is invoked.
- filter(Predicate<T> predicate): Filters elements based on a condition.
- map(Function<T, R> mapper): Transforms elements into another form.
- sorted(): Sorts the elements.
Example:
Stream<String> filteredStream = stream.filter(name -> name.startsWith("A"));
3. Terminal Operations: These operations produce a result or a side-effect and trigger the processing of the stream.
- forEach(Consumer<T> action): Performs an action for each element.
- collect(Collector<T, A, R> collector): Accumulates elements into a collection or other results.
- reduce(T identity, BinaryOperator<T> accumulator): Combines elements using a binary operator.
Example:
filteredStream.forEach(System.out::println); // Output: Alia
4. Parallel Streams: Streams can be processed in parallel to leverage multi-core processors for better performance. This is achieved by calling .parallelStream() on the collection or using .parallel() on an existing stream. For Example:
List<String> largeList = //... very large list
largeList.parallelStream().forEach(System.out::println);
Why the Stream API is Used:
- Declarative Style: Stream API allows you to describe what you want to achieve (e.g., filter, map, sort) without explicitly specifying how the operations are performed, making code more readable and expressive.
- Concise Code: It reduces boilerplate code compared to traditional loops and conditionals, making your code cleaner and easier to understand.
- Functional Programming: It supports functional programming concepts like lambda expressions and method references, which can simplify complex data manipulations.
- Parallel Processing: The ability to process streams in parallel makes it easier to take advantage of modern multi-core processors, improving performance for large data sets.
- Lazy Evaluation: Intermediate operations are lazily evaluated, meaning computations are deferred until needed. This allows for efficient processing of large data sets.
7. Explain method references in Java 8 and give an example.
Method references in Java 8 offer a concise way to refer to methods of classes or objects, simplifying lambda expressions that only call a method. They enhance readability by replacing verbose lambda expressions with a more direct syntax.
For instance, instead of using a lambda to pass a method call, you can use a method reference to point to the existing method. This approach is especially useful when the lambda expression only performs a single method invocation, streamlining the code and improving clarity.
Consider a list of strings where we want to print each string. Using method references, we can simplify the process:
In this example, Main::print is a method reference that replaces the lambda expression message -> print(message), making the code more concise and readable.
8. What are some standard Java pre-defined functional interfaces?
Java 8 introduced several standard pre-defined functional interfaces in the java.util.function package. These interfaces are designed to represent common function types and are widely used with lambda expressions and method references. Here are some of the key ones:
- Predicate<T>: Represents a boolean-valued function of one argument.
- Function<T, R>: Takes one argument and returns a result.
- Consumer<T>: Takes an argument and performs an operation without returning a result.
- Supplier<T>: Supplies a result without taking any input.
- BinaryOperator<T>: Performs a binary operation on two arguments of the same type.
- BiFunction<T, U, R>: Takes two arguments and returns a result.
- UnaryOperator<T>: Performs an operation on a single argument of the same type.
9. What are static methods in Interfaces?
Static methods in interfaces, introduced in Java 8, are methods that belong to the interface itself rather than to instances of classes that implement the interface. These methods can be called directly on the interface without needing an object of a class that implements the interface.
Key Points:
- Belong to Interface: Static methods are part of the interface and cannot be overridden by implementing classes.
- Direct Invocation: They are invoked using the interface name, like InterfaceName.methodName().
- Utility Functions: Static methods in interfaces are often used to define utility or helper functions related to the interface's operations.
Example:
10. How can you remove duplicates from a list using Streams?
You can remove duplicates from a list using Java Streams by converting the list into a stream and then using the distinct() method, which returns a stream with duplicate elements removed. After applying distinct(), you can collect the results back into a list.
Example:
In this example, distinct() filters out duplicate elements from the stream of numbers, and collect(Collectors.toList()) gathers the filtered elements back into a new list.
11. What are the advantages of Lambda Expression?
Lambda expressions in Java offer several advantages, including:
- Conciseness: They reduce boilerplate code by allowing you to write functions in a compact, readable format, especially when implementing interfaces with a single abstract method (functional interfaces).
- Improved Readability: Code becomes more expressive and easier to understand, as lambda expressions often replace anonymous classes and lengthy function declarations.
- Support for Functional Programming: Lambda expressions enable functional programming constructs, allowing you to pass behavior (functions) as arguments to methods, improving flexibility and reusability.
- Ease of Use with Streams API: Lambda expressions integrate seamlessly with the Streams API, allowing for more declarative and readable data processing.
- Parallel Processing: They can simplify parallel processing by enabling more concise and straightforward code when used with streams, enhancing performance on multi-core systems.
- Event Handling: In GUI applications, lambda expressions simplify event handling by reducing the need for verbose inner classes.
12. What are functional or SAM interfaces?
Functional interfaces, also known as Single Abstract Method (SAM) interfaces, are interfaces in Java that contain exactly one abstract method. These interfaces are central to Java's support for lambda expressions, as they allow you to treat a lambda expression as an instance of the interface.
Key Characteristics:
- Single Abstract Method: A functional interface has only one abstract method, though it can have multiple default or static methods.
- Lambda Compatibility: Lambda expressions can be used to instantiate functional interfaces, making the code more concise and expressive.
- Annotation: The @FunctionalInterface annotation can be used to indicate that an interface is intended to be a functional interface. This annotation is optional but helps prevent accidental addition of more abstract methods.
13. How can you sort a collection using Java 8 Streams?
You can sort a collection using Java 8 Streams by converting the collection into a stream, applying the sorted() method, and then collecting the sorted elements back into a collection. The sorted() method sorts the stream's elements in natural order or using a custom comparator if specified.
Example: Sorting a List of Strings in Natural Order
14. What is the significance of the Collectors class in Streams?
The Collectors class in Java 8 is essential for working with streams, as it provides a range of utility methods to collect and aggregate stream elements into various collections or perform operations like grouping, partitioning, and summarizing data. It simplifies the process of converting streams into lists, sets, or maps and enables more complex operations like joining strings or collecting elements into custom data structures. By using Collectors, you can efficiently handle and transform data in a concise and readable manner within the Streams API.
15. What is the purpose of the Double Colon (::) operator in Java 8?
The double colon (::) operator in Java 8, also known as the method reference operator, is used to refer to methods or constructors directly without invoking them. It provides a more concise and readable way to pass methods or constructors as arguments to functions, particularly when used with lambda expressions and streams. By using ::, you can refer to static methods, instance methods of specific objects, instance methods of arbitrary objects of a specific type, or constructors. This operator enhances code clarity by replacing verbose lambda expressions with a direct reference to the existing method or constructor, streamlining functional programming operations in Java.
Example:
In this example, Main::print is a method reference that replaces a lambda expression, making the code more concise and easier to understand.
16. Explain the Optional class and its use cases.
The Optional class in Java 8 is a container object that may or may not contain a value. It is designed to handle the possibility of null values more gracefully, avoiding null pointer exceptions and making code more readable and maintainable.
Here are the uses of the Optional class in Java 8:
- Avoid Null Checks: Use Optional as a return type to indicate that a value might be absent, eliminating explicit null checks.
- Provide Default Values: Use orElse() to specify a default value when the Optional is empty.
- Perform Actions Conditionally: Use ifPresent() to execute code only if a value is present.
- Transform Values: Use map() to apply transformations to the contained value if present.
- Chain Operations: Chain methods like map() and flatMap() for more expressive and readable handling of possible values.
- Handle Absence Gracefully: Use Optional to represent and handle the absence of a value without resorting to nulls.
17. Can return types differ between different methods in a class within Java SE8?
In Java SE 8, return types of methods within a class can differ as long as they match the method signatures defined by their names and parameters. Each method in a class can have a unique return type and is determined by its method signature, which includes the method name and parameter list. This flexibility allows for the definition of various methods with different return types within the same class, catering to different functional requirements.
However, if a method is intended to override a method from a superclass or implement an interface method, its return type must be compatible with the method it overrides or implements, adhering to the rules of covariance. This ensures that the subclass method can return a type that is either the same as or a subtype of the return type declared in the superclass or interface.
18. How can you filter a collection using Streams in Java 8?
You can filter a collection using Streams in Java 8 by converting the collection into a stream and applying the filter() method. The filter() method takes a Predicate (a functional interface representing a condition) and returns a new stream containing only the elements that match the specified condition. You then collect the filtered elements back into a collection or perform further operations on the stream.
Steps to Filter a Collection Using Streams:
- Convert the Collection to a Stream: Use the stream() method to create a stream from the collection.
- Apply the filter() Method: Pass a Predicate to filter() to specify the condition for including elements.
- Collect or Process the Results: Use collect() or other terminal operations to gather or further process the filtered elements.
19. Explain the Stream method reduce() and its purpose.
The reduce() method in Java 8 Streams is a terminal operation that performs a reduction on the elements of a stream using an associative accumulation function and returns an Optional describing the result. It is used to combine all the elements of a stream into a single result, such as calculating a sum, product, or concatenation of elements.
Purpose of reduce():
- Aggregation: The reduce() is used to aggregate or combine elements of a stream into a single value based on a provided binary operation.
- Flexibility: It can be used for various operations like summing numbers, finding a maximum or minimum value, concatenating strings, and more.
- Optional Handling: It returns an Optional because the result might be empty if the stream is empty, providing a way to handle cases where no reduction could be performed.
20. What is the Predicate functional interface?
The Predicate functional interface in Java 8 is a part of the java.util.function package and represents a single argument function that returns a boolean value. It is used primarily for testing or evaluating conditions on objects. The Predicate interface is functional because it has a single abstract method, test(), which takes an argument of type T and returns a boolean result.
Common Methods:
- and(Predicate<? super T> other): Combines two predicates with a logical AND operation.
- or(Predicate<? super T> other): Combines two predicates with a logical OR operation.
- negate(): Inverts the result of the predicate.
Java 8 Interview Questions: Intermediate
21. What new features were added to facilitate parallel and stream operations in Java SE 8?
Java SE 8 introduced several new features to facilitate parallel and stream operations, enhancing the ability to process collections of data more efficiently and concisely. Here are the key features:
- Streams API: Provides a high-level abstraction for processing sequences of elements, including operations like filtering, mapping, and reducing.
- Parallel Streams: Allows for concurrent processing using parallelStream(), leveraging multi-core processors.
- Lazy Evaluation: Delays computation until a terminal operation is executed, optimizing performance.
- forEach Method: Processes each element in a stream, usable in both sequential and parallel contexts.
- Fork/Join Framework: Enhances parallel processing by breaking tasks into smaller subtasks for concurrent execution.
- Spliterator Interface: Facilitates traversal and partitioning of elements for parallel processing.
- CompletableFuture Class: Supports asynchronous programming with non-blocking, parallel computation.
- Collectors Class: Provides predefined collectors for accumulating results from streams into various data structures.
22. How do you create a custom functional interface in Java 8?
To create a custom functional interface in Java 8, follow these steps:
- Define an Interface: Create a new interface using the interface keyword/identifier.
- Use the @FunctionalInterface Annotation: Optionally, annotate the interface with @FunctionalInterface to indicate that it is intended to be a functional interface. This annotation helps the compiler enforce that the interface has exactly one abstract method.
- Add Abstract Method: Define a single abstract method within the interface. This method defines the functional behavior that the interface will represent.
Example: Creating a Custom Functional Interface
@FunctionalInterface
public interface MathOperation {
// Single abstract method
int operate(int a, int b);// Default method (optional)
default int add(int a, int b) {
return a + b;
}// Static method (optional)
static int subtract(int a, int b) {
return a - b;
}
}
23. What are the differences between peek() and map() in Streams?
Here's a table outlining the key differences between peek() and map() in Java Streams:
Feature | peek() | map() |
---|---|---|
Purpose | Primarily used for debugging and inspecting elements in the stream. | Transforms each element of the stream into another form. |
Return Type | Returns a stream with the same elements, allowing further operations. | Returns a new stream with elements transformed by the given function. |
Operation | Performs a non-terminal operation that does not modify the elements but allows you to see intermediate values. | Performs a terminal operation that applies a function to each element, modifying their values. |
Side Effect | Typically used for side effects like logging; does not alter the stream. | Transforms elements of the stream; the output stream contains the results of the transformation. |
Example Usage | stream.peek(System.out::println) to print elements for debugging. | stream.map(String::toUpperCase) to convert each string element to uppercase. |
Execution | Often used in conjunction with other operations to inspect the flow of data. | Directly affects the data flow by changing element values. |
24. How can you convert a Stream to a collection like List or Set?
To convert a Stream to a collection such as List or Set, you use the collect() method with a collector from the Collectors class. The collect() method is a terminal operation that transforms the elements of a stream into a collection. For converting to a List, you use Collectors.toList(), which gathers all the elements into a List. For converting to a Set, you use Collectors.toSet(), which collects elements into a Set and automatically removes duplicates. These conversions are useful for further processing or when you need to store the results in a specific collection type.
Example:
25. What are the new Date and Time APIs introduced in Java 8, and why are they better?
Java 8 introduced a new Date and Time API in the java.time package to address the limitations of the old java.util.Date and java.util.Calendar classes. This API includes:
- LocalDate: Represents a date without time zone (e.g., 2024-09-04).
- LocalTime: Represents a time without date (e.g., 15:30).
- LocalDateTime: Combines date and time without time zone (e.g., 2024-09-04T15:30).
- ZonedDateTime: Combines date and time with time zone (e.g., 2024-09-04T15:30:00+01:00[Europe/Paris]).
- Instant: Represents a specific moment on the timeline (e.g., 2024-09-04T15:30:00Z).
- Duration: Represents a time-based amount of time (e.g., 2 hours, 30 minutes).
- Period: Represents a date-based amount of time (e.g., 1 month, 10 days).
Advantages:
- Immutability: Classes are immutable, ensuring safer and predictable code.
- Thread-Safety: The immutability also makes them thread-safe.
- Clear API Design: Separates date and time concerns into distinct classes.
- Time Zone Support: Handles time zones effectively with ZonedDateTime.
26. What is MetaSpace? How does it differ from PermGen?
MetaSpace is a memory area introduced in Java 8 that replaces the older PermGen (Permanent Generation) space used in previous Java versions. Here's a breakdown of the differences:
Feature | MetaSpace | PermGen |
---|---|---|
Memory Location | Native memory (outside Java heap) | Part of the JVM heap |
Size Management | Dynamically resizable, grows as needed | Fixed size, specified at JVM startup |
Configuration | -XX:MetaspaceSize and -XX:MaxMetaspaceSize | -XX:PermSize and -XX:MaxPermSize |
Error Handling | Reduced risk of OutOfMemoryError related to metadata | Commonly experienced OutOfMemoryError for class metadata |
27. How can you handle checked exceptions in Lambda expressions?
Handling checked exceptions in lambda expressions can be challenging because lambda expressions in Java can only directly handle unchecked exceptions (subclasses of RuntimeException). However, there are ways to manage checked exceptions within lambda expressions. Here’s how you can handle them:
- Wrap Exceptions: Use a method to catch and rethrow checked exceptions as unchecked exceptions for lambda expressions.
// Functional interface for checked exceptions
@FunctionalInterface
interface CheckedFunction<T, R> {
R apply(T t) throws Exception;
}// Wrap checked exception into unchecked exception
private static <T, R> Function<T, R> wrapCheckedException(CheckedFunction<T, R> checkedFunction) {
return t -> {
try {
return checkedFunction.apply(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
- Helper Methods: Handle checked exceptions outside the lambda expression using a separate method to simplify the lambda's functional interface.
// Functional interface for checked exceptions
@FunctionalInterface
interface CheckedConsumer<T> {
void accept(T t) throws Exception;
}// Helper method to handle exceptions
private static void process(String fileName, CheckedConsumer<String> consumer) {
try {
consumer.accept(fileName);
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
}
}
28. What does the String::ValueOf expression mean?
The String::valueOf expression is a method reference in Java that refers to the valueOf method of the String class. Method references provide a way to refer to methods by their names without executing them. They can be used to simplify lambda expressions and make the code more readable.
- String::valueOf: Refers to the static valueOf method in the String class, which converts various types of data (e.g., int, boolean, char, Object) to their String representation.
- Static Method Reference: Since valueOf is a static method, String::valueOf is a reference to a static method. It can be used to replace lambda expressions that perform similar functionality.
29. What is the difference between findFirst() and findAny()?
Here’s a comparison between findFirst() and findAny() methods in Java Streams:
Feature | findFirst() | findAny() |
---|---|---|
Purpose | Returns the first element in the stream. | Returns any element from the stream. |
Order Dependence | Yes, respects the order of elements in the stream. | No, can return any element and does not guarantee order. |
Performance | Might be slower for parallel streams due to its need to find the first element in the encounter order. | Typically faster for parallel streams as it doesn't need to respect the order and can return any element. |
Use Case | Useful when the order of elements matters, or when the first element is needed based on encounter order. | Useful when the specific element doesn’t matter and you need a performance advantage in parallel streams. |
Return Value | Optional<T> containing the first element or empty if the stream is empty. | Optional<T> containing any element or empty if the stream is empty. |
30. How does the parallelStream() method work, and when should it be used?
The parallelStream() method in Java is used to process elements of a collection in parallel, leveraging multiple threads to potentially improve performance for large datasets. Here's how it works and when it should be used:
How It Works:
- Parallel Processing: Divides tasks into smaller subtasks processed by different threads.
- Task Splitting: Uses ForkJoinPool to balance workload across processors.
- Result Combining: Aggregates results from parallel threads.
When to Use:
- Large Datasets: For significant performance gains with large collections.
- Independent Operations: When operations are thread-safe and can be processed in parallel.
- High Computational Cost: For tasks where parallel execution reduces overall processing time manipulation.
31. What is the difference between forEach() and forEachOrdered() methods?
The forEach() and forEachOrdered() methods in Java Streams are used to iterate over elements, but they handle element ordering differently. Given below is a comparison between the forEach() and forEachOrdered() methods in Java Streams:
Feature | forEach() | forEachOrdered() |
---|---|---|
Order Guarantee | No guarantee of order in parallel streams. | Guarantees processing elements in encounter order. |
Parallel Streams | Elements may be processed out of order. | Maintains encounter order even in parallel streams. |
Performance | Generally faster as it does not enforce order. | May be slower due to the additional overhead of preserving order. |
Use Case | Suitable when element order is not important or when performance is prioritized. | Suitable when the order of processing must be preserved. |
32. What are the advantages of using Optional over traditional null checks?
Here are the key advantages of using Optional over traditional null checks:
- Explicit Null Handling: Makes the possibility of null values explicit and forces handling.
- Improved Readability: Provides clearer and more expressive code.
- Functional Operations: Supports functional programming methods like map(), flatMap(), and filter().
- Reduces NullPointerExceptions: Encourages handling of missing values, reducing runtime errors.
- Safe Defaults: Offers methods like orElse() to provide default values.
- Method Chaining: Enables concise and readable data processing pipelines.
33. What are the key considerations when designing APIs with Java 8 features?
When designing APIs with Java 8 features, consider the following:
- Functional Interfaces: Use single-method interfaces to support lambda expressions.
- Default Methods: Implement default methods in interfaces for backward compatibility.
- Stream API: Use streams for efficient data processing, and be cautious with parallel streams.
- Optional: Utilize Optional to handle optional values and avoid nulls.
- Method References: Use method references for cleaner, more readable code.
- Immutable Structures: Prefer immutable data for simplicity and thread safety.
- Backward Compatibility: Ensure changes do not break existing code.
- Error Handling: Handle errors gracefully and use Optional to manage absent values.
34. What is the purpose of the Supplier functional interface, and how is it used?
The Supplier functional interface in Java provides a way to generate or supply values without taking any input. It represents a function that takes no arguments and returns a result.
Purpose:
- Value Generation: It is used to supply values or objects, often used for lazy or deferred execution.
- Factory Methods: Commonly used in factory patterns or methods where the generation of instances is needed.
- Configuration and Defaults: Useful for providing default values or configurations.
Usage:
- Method Signature: Supplier<T> has a single method T get() that returns a value of type T.
- Lambda Expression: It can be used with lambda expressions to supply values.
For Example:
In this example, Supplier is used to provide a default string and generate a random integer.
35. What is ArrayList forEach() method in Java?
The forEach() method in Java's ArrayList class is a default method introduced in Java 8 that allows for iterating over the elements of the list using a lambda expression or method reference. It is a part of the Iterable interface, which ArrayList implements. This method provides a way to perform a specified action on each element of the list, making it a convenient and concise way to handle elements without explicitly using a loop.
Example:
In this example, forEach() is used to print each item in the ArrayList. The lambda expression and method reference both achieve the same result, providing a clear and succinct way to iterate over list elements.
36. Explain how the default method in interfaces solves the diamond problem in Java 8.
In Java 8, default methods in interfaces provide a way to address the "diamond problem" associated with multiple inheritance. The diamond problem arises when a class inherits from two interfaces that both have a method with the same signature, leading to ambiguity about which method should be inherited.
How Default Methods Solve the Diamond Problem:
-
Default Method Resolution: Default methods provide a way to define method implementations directly in interfaces. When a class implements multiple interfaces with conflicting default methods, Java's method resolution process determines which method to use.
-
Method Resolution Order:
- Class Overrules: If the class itself provides an implementation of the method, this implementation takes precedence over the default methods from interfaces.
- Most Specific Interface: If the class does not provide an implementation, Java resolves the method to the most specific interface (i.e., the interface that is closest in the inheritance hierarchy).
-
Explicit Override: The class implementing the interfaces can explicitly override the conflicting default methods to resolve ambiguities and provide its own implementation.
Example:
In this example:
- Interface A and Interface B both have a default method method().
- Class C implements both interfaces and provides its own implementation of the method().
- The output is "Method from Class C", showing that the class’s implementation takes precedence and resolves the diamond problem effectively.
37. What are the potential pitfalls of using parallelStream()?
Using parallelStream() in Java can offer performance benefits for large datasets by processing elements concurrently, but it also comes with potential pitfalls:
- Overhead Costs: Performance gains may be negated by the overhead of managing threads.
- Thread Safety Issues: Requires thread-safe operations to avoid data corruption.
- Order Preservation: No guarantee of element order; use forEachOrdered() if needed.
- Resource Contention: Can lead to contention for CPU and other functional units, affecting system performance.
- Complexity: Makes debugging more challenging due to concurrent behavior.
- Unpredictable Gains: Performance improvements are not always guaranteed and can vary.
38. How can you implement a custom collector for Streams?
To implement a custom collector for Streams in Java, you need to define a class that implements the Collector interface. This involves providing implementations for several methods that describe how to collect elements into a result. Here’s a brief overview of how to create a custom collector:
-
Implement the Collector Interface: Create a class that implements the Collector interface, specifying the types of input and result.
-
Provide Collector Methods:
- Supplier: Provides an initial container (mutable result container).
- Accumulator: Defines how to accumulate elements into the container.
- Combiner: Specifies how to merge two containers into one.
- Finisher: Converts the final result into the desired form (optional).
- Characteristics: Provides properties about the collector, such as whether it is concurrent or unordered.
Let's look at an example of a custom collector that joins strings with a delimiter:
39. Discuss the performance implications of using Streams vs traditional loops.
When comparing Streams to traditional loops in Java, performance implications can vary based on the use case and context. Here’s a brief comparison:
Streams:
- Parallel Processing: Can utilize multiple cores for better performance on large datasets but introduces overhead.
- Declarative: More readable and maintainable but may have additional overhead compared to loops.
- Lazy Evaluation: Avoids unnecessary computations but can complicate performance analysis.
Traditional Loops:
- Direct Control: Offers efficient and straightforward iteration with minimal overhead.
- Performance: Generally faster for simple tasks due to less abstraction but requires more manual coding for complex operations.
40. How does the CompletableFuture API enhance concurrency in Java 8?
The CompletableFuture API in Java 8 enhances concurrency by providing a more flexible and powerful way to handle asynchronous computations and manage concurrency compared to traditional approaches. Here’s how it improves concurrency:
- Asynchronous Execution: Allows non-blocking execution of tasks with methods like supplyAsync().
- Flexible Composition: Supports chaining and combining tasks using methods like thenApply() and thenCompose().
- Exception Handling: Provides robust error management with methods such as exceptionally().
- Parallel Execution: Facilitates parallel task execution and result synchronization using allOf() and anyOf().
Java 8 Interview Questions: Advanced
41. Explain the concept of Spliterator in Java 8 and how it differs from Iterator.
A Spliterator in Java 8 is an enhanced version of an Iterator designed to efficiently traverse and partition elements, particularly for parallel processing. Here’s how it works and how it differs from an Iterator:
Key features of Spliterator:
- Parallelism: Spliterator is designed to support parallel processing. It can split a data source into multiple parts, allowing concurrent traversal using streams.
- Characteristics: Spliterator provides metadata like ORDERED, SORTED, SIZED, etc., which can describe the data structure and help optimize performance.
- Functional Support: Works seamlessly with the Stream API, supporting operations like tryAdvance() (single element processing) and forEachRemaining() (bulk processing).
- Splitting: The key feature of Spliterator is the trySplit() method, which splits the data into smaller parts for parallel processing.
Differences between Spliterator and Iterator:
Feature | Spliterator | Iterator |
---|---|---|
Parallelism | Supports parallel processing via splitting. | Does not support parallel processing. |
Splitting | Can split itself using trySplit() for concurrent execution. | Cannot split itself for parallel use. |
Functional Methods | Supports both tryAdvance() and forEachRemaining(). | Only supports forEachRemaining(). |
Characteristics | Provides metadata (e.g., ORDERED, SIZED). | No metadata about the collection. |
Usage with Streams | Works well with the Stream API for parallelism. | Not designed for Stream API. |
42. How does the Fork/Join framework work in conjunction with Streams in Java 8?
The Fork/Join Framework in Java 8 works in conjunction with Streams (especially parallel streams) to enable efficient parallel processing of tasks. The framework breaks a task into smaller subtasks, executes them in parallel, and then combines the results. This process is seamless with parallel streams.
How the Fork/Join Framework works with Streams:
- Parallel Stream Processing: When you call .parallelStream(), the Fork/Join framework under the hood splits the data into smaller chunks and processes them in parallel using the ForkJoinPool.
- Task Splitting: The stream pipeline splits the source of data using a Spliterator, which divides the data into manageable pieces for concurrent processing.
- Recursive Task Handling: The Fork/Join framework recursively divides the tasks (forks), processes them in parallel, and then merges (joins) the results back together to form the final output.
- ForkJoinPool: The ForkJoinPool is the core of the Fork/Join framework. It uses a pool of worker threads to process tasks, dynamically managing the distribution of work. By default, the parallel stream uses the common ForkJoinPool with a number of threads equal to the number of available CPU cores.
- Work Stealing: The framework uses a technique called work stealing, where idle threads can "steal" tasks from busy threads, ensuring efficient use of resources.
43. Discuss the limitations of Lambda expressions and how they can be addressed.
Some of the limitations of Lambda expressions and their solutions are as follows:
1. No Checked Exceptions Handling:
- Issue: Lambda expressions do not directly handle checked exceptions. If a method within a lambda throws a checked exception, the lambda needs additional handling.
- Solution: Use try-catch within the lambda or create a custom wrapper to handle exceptions.
// Using try-catch inside a lambda
Runnable r = () -> {
try {
throw new Exception("Checked Exception");
} catch (Exception e) {
System.out.println("Handled exception");
}
};
2. Limited Readability for Complex Logic:
- Issue: Lambdas are concise and intended for short, simple tasks. When logic becomes complex, readability suffers.
- Solution: For more complex logic, consider using traditional methods or breaking down the lambda into smaller, readable pieces.
3. No Named Lambdas:
- Issue: Lambda expressions are anonymous functions, which means they can't be named or reused across multiple places.
- Solution: If reusability is required, consider using method references or defining separate methods.
4. Limited Debugging Support:
- Issue: Debugging lambda expressions can be harder because of the lack of meaningful names and the inlining of code.
- Solution: Use method references or expand complex lambdas into named methods for easier debugging.
5. Type Inference Limitations:
- Issue: Sometimes, the compiler may not infer the lambda parameter types correctly in complex generics situations.
- Solution: Explicitly declare the types of the lambda parameters to resolve type inference issues.
BiFunction<Integer, Integer, Integer> add = (Integer a, Integer b) -> a + b;
44. How can you create an infinite stream in Java 8?
In Java 8, you can create an infinite stream using the Stream interface's generate() and iterate() methods. Here's how each works:
1. Using Stream.generate(): The generate() method creates an infinite stream by repeatedly applying a Supplier function. This function provides a new value each time it is called.
Stream<Double> randomNumbers = Stream.generate(Math::random);
randomNumbers.limit(5).forEach(System.out::println); // Outputs 5 random double values
2. Using Stream.iterate(): The iterate() method generates an infinite stream by repeatedly applying a unary function to an initial value. This method uses an initial element and a function to generate the next element, creating a sequence where each element depends on the previous one.
Stream<Integer> numbers = Stream.iterate(1, n -> n + 1);
numbers.limit(5).forEach(System.out::println); // Outputs: 1, 2, 3, 4, 5
45. What is the significance of @FunctionalInterface annotation?
The @FunctionalInterface annotation in Java 8 is used to indicate that an interface is intended to be a functional interface, which means it has exactly one abstract method. Here’s why it’s significant:
1. Ensures Single Abstract Method:
- Purpose: The annotation helps ensure that the interface adheres to the functional interface contract by allowing only one abstract method.
- Benefit: Provides compile-time checking to prevent accidental addition of multiple abstract methods.
2. Enables Lambda Expressions and Method References:
- Purpose: Functional interfaces are used as the target types for lambda expressions and method references.
- Benefit: Facilitates concise and expressive code by leveraging lambda expressions for instantiating functional interfaces.
3. Improves Code Readability:
- Purpose: By explicitly marking an interface as functional, it makes the code more readable and self-documenting.
- Benefit: Helps developers understand the intent of the interface and its intended use with lambda expressions.
4. Optional but Recommended:
- Purpose: Using the annotation is optional but recommended to clearly document the intention and enforce the single abstract method rule.
- Benefit: Provides an additional level of safety and clarity in code.
46. How can you implement custom equality checks in Streams using Collectors.toMap()?
You can implement custom equality checks in Streams using Collectors.toMap() by following these steps:
- Define a Custom Equality Condition: Create a key extractor function that captures the custom equality logic.
- Use Collectors.toMap(): Implement a Collector with Collectors.toMap(), providing custom key and value mappings.
- Handle Duplicate Keys: Use a merge function to resolve duplicate keys according to custom equality rules.
- Apply the Collector: Apply the collector to your stream to generate a map with custom equality handling.
For Example:
47. Explain how to use Stream.reduce() for complex data processing tasks.
The Stream.reduce() method in Java 8 provides a powerful tool for performing complex data processing operations on streams of elements. It's particularly useful when you need to combine elements of a stream into a single result, or when you want to perform calculations that involve multiple elements.
The basic syntax of reduce() is as follows:
Optional<T> reduce(BinaryOperator<T> accumulator)
Things to take care while using strea.redunce() for complex data processing tasks:
- Use reduce() to aggregate stream elements into a single result.
- Provide Identity and Accumulator: For setting initial values and combining elements.
- Handle Complex Processing: Aggregate operations, concatenate, or process custom data.
- Manage Optional Results: Use .orElse() to handle cases where no result is present.
48. How can you optimize performance in Stream operations, particularly with large datasets?
Optimizing performance in Stream operations, especially with large datasets, involves several strategies:
- Use Parallel Streams: Leverage parallelStream() for multi-core processing, but ensure thread safety and that the dataset size justifies parallelization.
- Prefer Intermediate Operations: Apply filter(), map(), etc., before terminal operations to reduce data processed in the final step.
- Minimize State Changes: Avoid stateful operations and prefer stateless ones to reduce overhead.
- Use Efficient Data Structures: Choose appropriate data structures for fast access and manipulation.
- Limit Intermediate Collections: Use Stream.limit() to control the size of processed data and avoid memory overhead.
- Avoid Unnecessary Computations: Use peek() only for debugging and avoid expensive operations within it.
- Optimize Terminal Operations: Select efficient terminal operations, like findFirst() for single results.
- Use Short-Circuiting Operations: Benefit from operations like anyMatch() or findFirst() that stop processing early.
- Profile and Benchmark: Measure performance with tools to identify and fix bottlenecks.
49. Explain how Collectors.partitioningBy() works and give an example.
Collectors.partitioningBy() is a collector provided by Java Streams that partitions a stream of elements into two groups based on a given predicate. It returns a Map<Boolean, List<T>>, where the key is true or false, depending on whether each element satisfies the predicate. Here's how it works:
- Predicate: A boolean-valued function that determines the partition.
- Map Result: The resulting map has two entries:
- Key true: Contains elements that match the predicate.
- Key false: Contains elements that do not match the predicate.
Suppose we have a list of integers and we want to partition them into even and odd numbers.
Code:
In the above code, Collectors.partitioningBy() is used to divide the list of integers into two groups: even and odd numbers. The resulting map has keys true for even numbers and false for odd numbers.
50. What is the difference between allMatch(), anyMatch(), and noneMatch() methods?
Here’s a table summarizing the key differences between allMatch(), anyMatch(), and noneMatch() methods in Java Streams:
Method | Description | Returns True If | Returns False If |
---|---|---|---|
allMatch() | Checks if all elements satisfy a given predicate. | Every element matches the predicate. | At least one element does not match. |
anyMatch() | Checks if any element satisfies a given predicate. | At least one element matches the predicate. | No elements match the predicate. |
noneMatch() | Checks if no elements satisfy a given predicate. | No elements match the predicate. | At least one element matches. |
Therefore:
- allMatch(): True if every element matches the predicate.
- anyMatch(): True if at least one element matches the predicate.
- noneMatch(): True if no elements match the predicate.
51. Discuss the pros and cons of using Stream.generate() for creating infinite streams.
Using Stream.generate() for creating infinite streams has both advantages and disadvantages:
Pros:
- Simplicity: Simple syntax to create an infinite stream of values based on a supplier.
- Flexibility: Allows for the creation of complex sequences by defining custom suppliers.
- Lazy Evaluation: Elements are generated on demand, which can be efficient for processing only required elements.
Cons:
- Potential for Infinite Loops: Without a limiting operation like limit(), the stream can lead to infinite loops or excessive resource consumption.
- Memory and Performance Impact: If not handled properly, generating and processing an unbounded stream can lead to high memory usage and performance issues.
- Complexity in Control: Managing and terminating infinite streams requires careful use of limiting operations to avoid unintended behaviors.
52. What are UnaryOperator and BinaryOperator interfaces in Java 8?
In Java 8, UnaryOperator and BinaryOperator are specialized functional interfaces designed to handle operations on single and pairs of operands, respectively.
UnaryOperator:
- UnaryOperator is a functional interface that extends Function<T, T>, meaning it operates on a single input and produces an output of the same type. This makes it ideal for functions that transform data without changing the data type.
- For instance, if you have a function that squares an integer, it takes an integer as input and returns an integer as output. The UnaryOperator interface provides a method apply(T t), which performs this transformation.
- An example of using UnaryOperator would be to create a function that doubles an integer value, where both the input and output are integers.
UnaryOperator<Integer> square = x -> x * x;
int result = square.apply(5); // result is 25
BinaryOperator:
- BinaryOperator is a functional interface that extends BiFunction<T, T, T>, designed for operations that take two arguments of the same type and return a result of the same type.
- This interface is particularly useful for operations that combine or compare two values of the same type, such as adding two numbers together or finding the maximum of two values.
- The BinaryOperator interface provides a method apply(T t1, T t2), which performs the operation on the two inputs. For example, if you want to create a function that computes the sum of two integers, you would use BinaryOperator where both the inputs and the result are integers.
BinaryOperator<Integer> sum = (a, b) -> a + b;
int result = sum.apply(3, 4); // result is 7
53. What is a BiFunction, and how does it differ from a Function?
In Java 8, BiFunction and Function are functional interfaces used to represent functions that process inputs to produce outputs.
- BiFunction is used when you need to work with two input values of potentially different types and produce an output. For instance, you might use a BiFunction to combine two integers into their sum or to process two different objects and produce a combined result.
- Function, on the other hand, is used for single-argument functions. It takes one input and produces an output, making it suitable for operations that transform a single value. For example, a Function could convert a string to its length or transform an integer to its string representation.
Difference Table:
Aspect | BiFunction | Function |
---|---|---|
Number of Arguments | Takes two arguments (T and U) | Takes one argument (T) |
Functional Method | R apply(T t, U u) | R apply(T t) |
Use Case | Combining or processing two inputs to produce a result. | Transforming or mapping a single input to a result. |
Example | BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b; int sum = add.apply(5, 3); // sum is 8 | Function<String, Integer> lengthFunction = s -> s.length(); int length = lengthFunction.apply("Hello"); // length is 5 |
54. How can you chain multiple Comparator instances in Java 8?
In Java 8, you can chain multiple Comparator instances using the thenComparing() method. This method allows you to create a new Comparator that first compares elements using the existing Comparator, and then, if the elements are equal, uses the provided Comparator to compare them further.
Here's an example of chaining multiple Comparator instances:
Comparator<Person> nameComparator = Comparator.comparing(Person::getName);
Comparator<Person> ageComparator = Comparator.comparing(Person::getAge);
Comparator<Person> combinedComparator = nameComparator.thenComparing(ageComparator);
In this example,
- We compare the nameComparator people based on their names.
- If two people have the same name, we use the ageComparator to compare them based on their ages.
- Finally, the combinedComparator combines these two comparators, ensuring that people are first sorted by name, and then by age if their names are equal.
You can chain multiple Comparator instances using multiple thenComparing() calls:
Comparator<Person> combinedComparator = nameComparator.thenComparing(ageComparator)
.thenComparing(Person::getCity);
This combined Comparator will first sort people by name, then by age, and finally by city if the name and age are equal.
55. What is Stream Pipelining in Java 8, and how can it be used to improve code performance?
Stream Pipelining is a technique in Java 8 that allows you to chain multiple operations on a stream of elements in a declarative manner. It involves creating a stream, applying various intermediate operations to transform the elements, and finally performing a terminal operation to produce a result.
How it works:
- Create a Stream: Start by creating a stream from a java collection, array, or generator function.
- Apply Intermediate Operations: Chain together multiple intermediate operations that transform the elements of the stream. These operations are lazy, meaning they are not executed until a terminal operation is encountered.
- Perform a Terminal Operation: Apply a terminal operation that produces a result or side effect. This triggers the execution of the pipeline, and the intermediate operations are evaluated.
Example:
List<String> words = Arrays.asList("apple", "banana", "orange", "pear");
List<String> filteredWords = words.stream()
.filter(w -> w.length() > 5)
.map(String::toUpperCase)
.collect(Collectors.toList());
Benefits of Stream Pipelining:
- Improved Readability: Stream pipelines provide a declarative style, making code more concise and easier to understand.
- Enhanced Performance: The lazy evaluation nature of intermediate operations can optimize performance, especially for large datasets.
- Parallel Processing: Java 8 introduced parallel streams, which can automatically distribute the processing of elements across multiple threads, potentially improving performance.
56. Explain the differences and use cases between Stream.iterate() and Stream.generate() for creating streams.
Both Stream.iterate() and Stream.generate() are used to create infinite streams in Java 8. However, they have distinct use cases and mechanisms:
Stream.iterate():
- Purpose: Creates a stream based on a seed element and a function that generates subsequent elements.
- Mechanism: Starts with a seed value and repeatedly applies a function to the previous element to produce the next.
- Use Cases:
- Generating sequences that follow a pattern or formula (e.g., Fibonacci numbers, factorial series).
- Creating streams of consecutive numbers or values.
Example:
Stream<Integer> fibonacci = Stream.iterate(new int[]{0, 1}, pair -> new int[]{pair[1], pair[0] + pair[1]})
.map(pair -> pair[0]);
Stream.generate():
- Purpose: Creates a stream by repeatedly applying a supplier function to generate elements.
- Mechanism: Applies a supplier function to produce each element independently.
- Use Cases:
- Generating random numbers, UUIDs, or other values that don't depend on previous elements.
- Creating streams of constant values.
Example:
Stream<String> greetings = Stream.generate(() -> "Hello, world!");
Key Differences:
Feature | Stream.iterate() | Stream.generate() |
---|---|---|
Mechanism | Recursive | Supplier function |
Use Cases | Sequences, patterns | Independent values |
Dependence | Previous elements | No dependence |
57. What is the purpose of the Function.compose() and Function.andThen() methods in Java 8, and how do they differ?
The Function.compose() and Function.andThen() methods in Java 8 are used to combine functions into a single function. They are particularly useful for creating complex function pipelines and expressing functional compositions in a concise and readable way.
Function.compose()-
- Purpose: Applies one function after another.
- Syntax: Function<V, R> compose(Function<U, V> before)
- Behavior: Given two functions f and g, f.compose(g) creates a new function that first applies g to the input, and then applies f to the result.
Function.andThen()-
- Purpose: Applies one function before another.
- Syntax: Function<T, R> andThen(Function<R, U> after)
- Behavior: Given two functions f and g, f.andThen(g) creates a new function that first applies f to the input, and then applies g to the result.
Key Differences-
Aspect | Function.compose() | Function.andThen() |
---|---|---|
Order of Application | Applies the before function first, then the current function. | Applies the current function first, then the after function. |
Method Signature | Function<T, V> compose(Function<? super V, ? extends T> before) | Function<V, R> andThen(Function<? super R, ? extends V> after) |
Use Case | When you need to transform data through a sequence of functions starting with the last function. | When you need to transform data through a sequence of functions starting with the first function. |
58. Describe a scenario where Stream.concat() would be preferred over Stream.of() when creating a stream from multiple sources.
Consider a scenario where you have two existing streams of data:
Stream<Integer> stream1 = Stream.of(1, 2, 3);
Stream<Integer> stream2 = Stream.of(4, 5, 6);
If you want to create a single stream that contains all elements from both stream1 and stream2, you would use Stream.concat():
Stream<Integer> combinedStream = Stream.concat(stream1, stream2);
Advantages of Stream.concat() over Stream.of():
- Flexibility: Stream.concat() allows you to combine any two existing streams, regardless of their source or creation method.
- Efficiency: When dealing with large streams, Stream.concat() can be more efficient than creating a new stream from scratch using Stream.of().
- Readability: Stream.concat() clearly expresses the intent to combine multiple streams, making the code more understandable.
59. How does the CompletableFuture class handle exception handling and recovery, and what are the different methods available for this purpose?
The CompletableFuture class in Java 8 provides a range of methods to handle exceptions and ensure recovery in asynchronous tasks. Here's how it manages exception handling and recovery:
- exceptionally(): Handles exceptions and returns a fallback value if an error occurs during the computation. Usage: Simplifies error recovery by providing a default value.
- handle(): Deals with both the result and exception, allowing the ability to manage the output or handle the exception. Usage: Can return a custom result in case of either success or failure.
- whenComplete(): Executes a callback after the computation, regardless of success or failure. Usage: Typically used for logging or side effects but doesn't modify the result.
- thenApply() with exceptionally(): Transforms the result but falls back on exceptionally() if there's an exception during the transformation. Usage: Enables further result processing with fallback behavior for exceptions.
- exceptionallyCompose(): Chains another CompletableFuture to recover from a failure by continuing with an alternative asynchronous computation.
Example:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("Error");
});// Using exceptionally to recover from the exception
future.exceptionally(ex -> "Recovered")
.thenAccept(System.out::println); // Output: Recovered
These methods ensure flexible error handling and recovery for asynchronous tasks in CompletableFuture.
60. What are the potential side effects of using Stream.peek() in a stream pipeline, and how can they be mitigated?
The potential side effects of using Stream.peek() include:
- Unintended Modifications: Avoid using peek() to modify data as it may lead to unpredictable behavior, especially in parallel streams. Instead use peek() only for debugging or logging, not for modifying source elements.
- Lazy Evaluation: peek() operations won’t run unless a terminal operation is present. Ensure a terminal operation like collect() or forEach() is used.
- Parallel Stream Issues: Side effects in peek() may behave inconsistently in parallel streams. Only use peek() for actions that don’t depend on execution order.
For Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// Using peek correctly for debugging
numbers.stream()
.peek(System.out::println) // Safe for logging
.map(n -> n * 2)
.forEach(System.out::println); // Terminal operation triggers peek
In this example, peek() is safely used for debugging/logging, ensuring there are no unintended side effects.
Conclusion
Java 8 introduced by oracle led to significant enhancements to the language, including functional programming constructs, lambda expressions, and streams. These features have transformed the way Java developers write code, making it more concise, expressive, and efficient.
By understanding the concepts and techniques covered in this article, you'll be well-prepared to tackle Java 8 interview questions and demonstrate your proficiency in modern Java development. Remember to practice using these features in your projects to solidify your understanding and gain hands-on experience.
You may also like to read:
- 55+ Data Structure Interview Questions For 2024 (Detailed Answers)
- 70+ PL/SQL Interview Questions With Answers (2024) For All Levels
- 30+ Walmart Interview Questions With Answers (Technical + HR)
- Top 50+ TCS HR Interview Questions And Answers 2023
- Top Netflix Interview Questions For Software Engineer And SRE Roles