Top 10 Java 8 interview questions for 10 years experience 2024

Java 8 interview questions for 10 years experience

Most commonly asked Java 8 interview questions for 10 years experience

What are Parallel Streams in Java 8?

Parallel streams in Java 8 are a feature that allows you to process data in parallel, utilizing multiple cores of the processor. Unlike traditional sequential streams, which execute operations one after another, parallel streams divide the data into smaller chunks and process them concurrently on separate cores. This can significantly improve the performance of your program, especially for large datasets or computationally intensive operations.

How are Parallel Streams Implemented Internally?

Internally, parallel streams use the fork/join framework to execute operations in parallel. This framework provides support for the thread management necessary to execute the substreams in parallel. The number of threads employed during parallel stream execution is dependent on the CPU cores in the computer.

Creating Parallel Streams

There are two ways to create a parallel stream in Java:

  1. Using the parallel() Method on a Stream: This method returns an equivalent parallel stream. For example:
   File fileName = new File("M:\\Documents\\Textfile.txt");
   Stream<String> textStream = Files.lines(fileName.toPath());
   textStream.parallel().forEach(System.out::println);
  1. Using parallelStream() on a Collection: This method creates a parallel stream directly on a collection. For example:
   List<String> text = Files.readAllLines(fileName.toPath());
   text.parallelStream().forEach(System.out::println);

How Parallel Streams Work

The algorithm that splits a Stream into multiple parts is a recursive process. It starts by invoking the trySplit() method on the first Spliterator and generates a second one. Then, it’s called again on these two Spliterators, which results in a total of four. This recursive splitting process terminates when all Spliterators have returned null to a trySplit() invocation.

Characteristics of Spliterators

The Spliterator interface has several characteristics that can be used to better control and optimize its usage. These characteristics include ORDERED, DISTINCT, SORTED, SIZED, NONNULL, IMMUTABLE, CONCURRENT, and SUBSIZED.

Advantages of Parallel Streams

Parallel streams can significantly improve the performance of your program, especially for large datasets or computationally intensive operations. However, it’s important to note that the order of execution is not guaranteed, and the state of one element does not affect the other.

Example Java Program

Here’s an example Java program that demonstrates the use of parallel streams:

   import java.io.File;
   import java.io.IOException;
   import java.nio.file.Files;
   import java.util.stream.Stream;

   public class GFG {
       public static void main(String[] args) throws IOException {
           File fileName = new File("M:\\Documents\\Textfile.txt");
           Stream<String> textStream = Files.lines(fileName.toPath());
           textStream.parallel().forEach(System.out::println);
       }
   }

In this example, we create a file object and use the Files.lines() method to create a stream of lines from the file. We then use the parallel() method to create a parallel stream and print each line to the console.

Conclusion

In conclusion, parallel streams in Java 8 provide a powerful way to process data in parallel, utilizing multiple cores of the processor. They are implemented internally using the fork/join framework and can significantly improve the performance of your program. However, it’s important to note that the order of execution is not guaranteed, and the state of one element does not affect the other.


Advantages :

  • Improved throughput: Parallel stream processing can leverage multiple CPU cores to process data concurrently, leading to improved throughput and reduced execution time.
  • Scalability: Parallel stream processing can scale well with the number of available CPU cores, making it suitable for multi-core systems.
  • Automatic parallelization: Java’s parallel stream API handles the details of parallelization internally, making it easy to parallelize existing sequential stream code.

Suitable Scenarios:

Processing large datasets or performing computationally intensive operations where parallelism can lead to significant performance gains.

Operations that can be easily parallelized without introducing contention or synchronization overhead, such as mapping, filtering, and reduction.

Disadvantages :

  • Overhead: Parallel stream processing incurs overhead for dividing the stream, coordinating parallel tasks, and merging results, which may outweigh the benefits for small datasets or operations with high contention.
  • Thread Safety: Operations performed in parallel streams must be thread-safe to avoid data races and ensure correctness.
  • Ordering: Parallel stream processing may not preserve the order of elements in the stream, which can affect the correctness of certain operations, such as reduction operations.
  • Contention: In scenarios where shared resources are heavily contended, parallel stream processing may lead to decreased performance due to increased synchronization overhead and contention between threads.
  • Complexity: Parallel stream processing introduces complexity in terms of thread management, potential race conditions, and debugging. Care must be taken to ensure thread safety and avoid potential concurrency issues.

By default parallel streams use common ForkJoinPool, which has a number of threads equal to the number of available processors (as returned by Runtime.getRuntime().availableProcessors()).

Controlling the Number of Parallel Streams

  • You can control the size of the common ForkJoinPool by setting the system property java.util.concurrent.ForkJoinPool.common.parallelism. For example, to set the maximum number of parallel threads to 10, you can use the following JVM argument: -Djava.util.concurrent.ForkJoinPool.common.parallelism=10.

Creating a Custom ForkJoinPool:

If you need more fine-grained control over the parallelism of your streams, you can create a custom ForkJoinPool and use it to execute your parallel streams.

Here’s an example:

final int parallelism = 4;
ForkJoinPool forkJoinPool = new ForkJoinPool(parallelism);
try {
    forkJoinPool.submit(() -> myList.parallelStream()
            .map(this::processElement)
            .collect(Collectors.toList()))
            .get();
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
} finally {
    forkJoinPool.shutdown();
}
  • In this example, we create a ForkJoinPool with a parallelism of 4, and then use it to execute the parallel stream. This allows us to control the maximum number of threads used for the parallel processing.

The key takeaways are:

  1. You can control the size of the common ForkJoinPool using the java.util.concurrent.ForkJoinPool.common.parallelism system property.
  2. You can create a custom ForkJoinPool and use it to execute your parallel streams for more fine-grained control.

In Java, when using lambda expressions, variables from the enclosing scope must be final or effectively final. This requirement ensures the integrity and predictability of the lambda expressions. Here’s a simple explanation with examples:

Final Variables:

  • A final variable is one that cannot be changed once it is assigned a value. When a variable is declared as final, its value remains constant throughout its scope.

    Example:
final int number = 10;
Runnable r = () -> System.out.println("Number is: " + number);

Effectively Final Variables:

  • An effectively final variable is one that is not declared with the final keyword but behaves as if it were final because its value does not change after initialization.
  • Example:
int count = 5;
Runnable r = () -> System.out.println("Count is: " + count);

Reasons for Final or Effectively Final:

  • Concurrency: By requiring variables to be final or effectively final, Java ensures that lambda expressions do not introduce concurrency issues. If the local variables captured by the lambda were allowed to change, it could lead to concurrency issues. Each thread executing the lambda would see a different value for the variable, potentially leads to race conditions and other threading problems.
  • Consistency: The requirement for final or effectively final variables ensures that the lambda expression and the enclosing scope are in sync, maintaining the expected behavior of the code.

Example:

   public class LambdaExample {
       public static void main(String[] args) {
           int value = 20;
           Runnable r = () -> System.out.println("Value is: " + value);
           r.run();
       }
   }

In this example, the variable value is used in a lambda expression. Since it is not modified after initialization, it is effectively final and can be safely used in the lambda.

By enforcing the final or effectively final rule for variables in lambda expressions, Java ensures code reliability, prevents concurrency issues and maintains the expected behavior of the program. This rule is essential for the proper functioning of lambda expressions in Java.


Java 8 streams work internally by using a combination of intermediate and terminal operations to process data. Here’s a detailed explanation of how they work:

Intermediate Operations

Intermediate operations are the methods that are called on a stream to transform it into another stream. These operations are lazy, meaning they do not execute until a terminal operation is called. Examples of intermediate operations include map(), filter(), and sorted().

Terminal Operations

Terminal operations are the methods that mark the end of a stream and return the result. They execute the intermediate operations and produce the final result. Examples of terminal operations include collect(), forEach(), and reduce().

Stream Internals

Internally, Java streams are stored in a LinkedList structure, and each stage of the stream pipeline is assigned a bitmap that follows a specific structure. This bitmap is used to track the state of the stream, such as whether the stream is sorted or contains distinct elements.

Execution

When a terminal operation is called, the stream selects an execution plan. If all stages are stateless, the stream can be executed in a single pass. However, if there are stateful operations like sorted() or distinct(), the stream may need to be executed in multiple passes.

Benefits

Java streams offer several benefits, including:

  • Laziness: Intermediate operations are executed only when a terminal operation is called, which can improve performance by reducing unnecessary computation.
  • Pipeline of Functions: Streams allow for the chaining of multiple operations, making it easier to perform complex data processing tasks.
  • No Storage: Streams do not store data, they only provide the result of the pipelined operations.
  • Can be Infinite: Streams can be infinite, allowing for the processing of large datasets.
  • Can be Parallelized: Streams can be parallelized, making them suitable for processing large datasets on multi-core processors.

Conclusion :

In conclusion, Java 8 streams work internally by using a combination of intermediate and terminal operations to process data. They offer several benefits, including laziness, the ability to pipeline functions, and the ability to be infinite and parallelized. Understanding how streams work internally is important for optimizing their performance and avoiding common pitfalls.


Based on the provided sources, here is an explanation regarding how Spliterator works in Java 8 streams:

Spliterator in Java 8 Streams

In Java 8, Spliterator is an interface introduced to handle efficient parallel traversal of elements in streams. Spliterators are used for traversing the elements of a source, such as collections, IO channels, or generator functions. They support parallel traversal in addition to sequential traversal, making them suitable for parallel programming taks.

Key Concepts:

Creation: Spliterator objects can be created by calling the spliterator() method present in the Collection interface. This allows for efficient traversal of elements in a source data structure.

Methods:

  • trySplit(): Splits the elements of the source into two portions, allowing for parallel processing.
  • estimateSize(): Provides an estimate of the number of elements yet to be traversed.
  • getExactSizeIfKnown(): Returns the exact size of elements if known, otherwise returns -1.
  • hasCharacteristics(): Checks if the Spliterator has specified characteristics.
  • characteristics(): Returns a set of characteristics of elements in an integer form.

Parallel Computation: Spliterator is designed to support efficient parallel traversal, especially in the context of Java streams. It allows for splitting off subtasks until the estimated amount of work is small enough to perform sequentially, enabling parallel processing of elements.

Java 8 Streams and Spliterator Interaction:

In Java 8 streams, Spliterator plays a crucial role in enabling efficient parallel processing of elements. Streams internally use Spliterator to split, process, and combine elements during stream operations. Spliterator helps in dividing the workload for parallel execution, enhancing the performance of stream operations, especially when dealing with large datasets.

Conclusion:

Spliterator in Java 8 streams is a fundamental interface that facilitates efficient traversal and processing of elements, particularly in parallel scenarios. Understanding how Spliterator works internally is essential for optimizing the performance of stream operations and leveraging parallel processing capabilities effectively.


The concept of default methods in interfaces was introduced in Java 8 to address the issue of backward compatibility when adding new methods to existing interfaces.

Prior to Java 8, interfaces could only contain abstract methods, which meant that any new methods added to an interface would require all implementing classes to provide an implementation for that method. This made it difficult to evolve interfaces over time without breaking existing code.

Default methods in Java 8 solve this problem by allowing interfaces to provide a default implementation for a method. This means that when a new method is added to an interface, the implementing classes do not need to provide their own implementation unless they want to override the default behavior.

Here’s how default methods work:

Syntax: A default method is defined in an interface using the default keyword, followed by the method signature and implementation.

public interface MyInterface {
    void abstractMethod();
    default void defaultMethod() {
        System.out.println("This is the default implementation");
    }
}

Backward Compatibility: When a new default method is added to an interface, existing implementations of that interface do not need to be modified. The default implementation will be used unless the implementing class overrides the method.

class MyClass implements MyInterface {
    public void abstractMethod() {
        // implementation
    }
    // no need to implement defaultMethod()
}

Here are a few examples of built-in interfaces in Java 8 that were enhanced with default methods.

  1. List and Collection Interfaces:
    • The List and Collection interfaces were enhanced with default methods like forEach()removeIf()replaceAll(), and sort() in Java 8. These default methods allow you to perform common operations on collections without having to implement them in every class that implements these interfaces.
  2. Comparator Interface:
    • The Comparator interface was given default methods like reversed()thenComparing(), and thenComparingInt() in Java 8. These default methods make it easier to create and compose complex comparators.

Simplicity and Readability:

  • When the lambda expression is a simple method call, using a method reference can make the code more readable and easier to understand.
// Lambda expression
personList.forEach(p -> System.out.println(p.getName()));

// Method reference
personList.forEach(System.out::println);

Reusability:

  • Method references can be reused across different parts of the code, whereas lambda expressions are more specific to the context in which they are defined.

The method reference String::compareToIgnoreCase can be reused across different parts of the code, whereas the lambda expression (p1, p2) -> p1.compareToIgnoreCase(p2) is specific to the context.

// Lambda expression
personList.sort((p1, p2) -> p1.compareToIgnoreCase(p2));

// Method reference
personList.sort(String::compareToIgnoreCase);

The CompletableFuture class in Java 8 significantly improves asynchronous programming compared to the previous Future API in several ways:

  1. Composability: CompletableFuture provides a rich set of methods like thenApply(), thenCompose(), thenCombine(), etc. that allow you to easily chain multiple asynchronous operations together. This enables the creation of complex, multi-stage asynchronous workflows in a fluent and readable manner.
  2. Callbacks and Completions: CompletableFuture allows you to attach callback functions that will be executed when the asynchronous operation completes, either normally or with exceptions. This eliminates the need for explicit polling or blocking calls to check the status of the operation.
  3. Exception Handling: CompletableFuture provides better exception handling capabilities compared to the basic Future API. You can use methods like exceptionally() and handle() to specify fallback actions in case of exceptions.
  4. Parallelism: CompletableFuture integrates well with the Java 8 Streams API, allowing you to easily parallelize the execution of multiple asynchronous tasks.
import java.util.List;
import java.util.concurrent.CompletableFuture;

public class CompletableFutureExample {

    public static void main(String[] args) {
        // Asynchronously fetch the price of a product from a remote API
        CompletableFuture<Double> priceFuture = CompletableFuture.supplyAsync(() -> {
            // Simulate a remote API call
            return fetchProductPrice("product-123");
        });

        // Asynchronously fetch the reviews of the product
        CompletableFuture<List<String>> reviewsFuture = CompletableFuture.supplyAsync(() -> {
            // Simulate fetching reviews from another service
            return fetchProductReviews("product-123");
        });

        // Combine the price and reviews into a single result
        CompletableFuture<ProductDetails> detailsFuture = priceFuture.thenCombine(reviewsFuture, (price, reviews) -> {
            return new ProductDetails(price, reviews);
        });

        // Handle the final result
        detailsFuture.whenComplete((details, exception) -> {
            if (exception == null) {
                System.out.println("Product details: " + details);
            } else {
                System.out.println("Error fetching product details: " + exception.getMessage());
            }
        });

        // Simulate the main thread doing other work
        System.out.println("Main thread is doing some other work...");

        // Ensure the main thread doesn't exit before the asynchronous tasks complete
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static double fetchProductPrice(String productId) {
        // Simulate fetching the price of the product
        return 99.99;
    }

    private static List<String> fetchProductReviews(String productId) {
        // Simulate fetching reviews for the product
        return List.of("Great product!", "Highly recommended!");
    }

    static class ProductDetails {
        private double price;
        private List<String> reviews;

        public ProductDetails(double price, List<String> reviews) {
            this.price = price;
            this.reviews = reviews;
        }

        @Override
        public String toString() {
            return "ProductDetails{" +
                    "price=" + price +
                    ", reviews=" + reviews +
                    '}';
        }
    }
}

PermGen and Metaspace are memory areas in Java that store class metadata, but they differ in several key aspects:

Memory Allocation:

  • PermGen:
    • Default allocation: 64 MB.
    • Fixed maximum size.
    • Tuned using -XXMaxPermSize.
  • Metaspace:
    • Auto-increases its size based on the underlying OS.
    • Maximum size set using -XX:MaxMetaspaceSize.

Memory Area:

  • PermGen:
    • Special heap space.
  • Metaspace:
    • Separate memory area in the native OS.

Garbage Collection:

  • PermGen:
    • Comparatively inefficient garbage collection.
    • May lead to frequent pauses during GC.
  • Metaspace:
    • More efficient garbage collection.
    • Allows for concurrent deallocation of class data.

Dynamic Class Loading:

  • PermGen:
    • Classes were stuck in memory until JVM shutdown.
  • Metaspace:
    • Supports dynamic loading and unloading of classes during the JVM lifespan.

Error Handling:

  • PermGen:
    • Could lead to java.lang.OutOfMemoryError: PermGen space.
    • Fixed maximum size, no auto-increase.
  • Metaspace:
    • Auto-increases its size, reducing the likelihood of OOM errors.
    • Class metadata is allocated in native memory.

Java Version:

  • PermGen:
    • Used in Java 7 and earlier.
  • Metaspace:
    • Introduced in Java 8, replacing PermGen.

In summary, Metaspace in Java 8 offers improvements over PermGen by dynamically adjusting its size, supporting more efficient garbage collection, and allowing for the loading and unloading of classes during the JVM’s lifespan. Understanding these differences is crucial for optimizing memory management and avoiding common memory-related issues in Java applications.


In Java 8, significant changes occurred in memory management, particularly in the handling of memory areas like PermGen and the introduction of Metaspace. Here are the key changes that took place in memory management in Java 8:

PermGen Removal:

  • In Java 8, the Permanent Generation (PermGen) was completely removed and replaced with Metaspace.
  • PermGen, which stored class and method objects, was replaced by Metaspace, a memory area in native memory that is not part of the Java Heap.

Metaspace Introduction:

  • Metaspace in Java 8 stores classes and metadata, unlike PermGen which was part of the Java Heap.
  • Metaspace is allocated out of native memory and can expand dynamically based on the application’s needs, unlike the fixed-size PermGen.

Memory Management Changes:

  • With the removal of PermGen, the memory management landscape changed significantly in Java 8.
  • Metaspace being part of native memory is only limited by the host operating system, allowing for more flexibility in memory allocation.

Garbage Collection Enhancements:

  • Java 8 also brought improvements to garbage collection mechanisms like G1, supporting concurrent unloading of classes and introducing features like Code Cache and Compressed Class Space.

Monitoring and Tuning:

  • With the introduction of Metaspace, new JVM options like MetaspaceSize and MaxMetaspaceSize were added to tune and monitor the Metaspace size, ensuring efficient memory management.

In conclusion, Java 8 witnessed significant changes in memory management, with the removal of PermGen and the introduction of Metaspace, leading to more efficient memory allocation and management in Java applications. These changes have had a profound impact on how memory is handled and optimized in Java 8 and beyond.


If you want to prepare top 50 Java 8 programming interview questions then checkout this
If you want to prepare top 50 Java 8 theoretical interview questions then checkout this

We hope you found the above Java 8 interview questions for 10 years experience useful. Even though the questions are tailored for 10 years of experience, above Java 8 interview questions for 7 years experience can also be highly beneficial.

You can download Java 8 interview questions for 10 years experience pdf here
Share this article with tech community
WhatsApp Group Join Now
Telegram Group Join Now

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *