As a senior Java developer, you're expected to have a deep understanding of the language's intricacies and advanced concepts. To help you prepare for your next interview, we've compiled a list of 20 complex Java interview questions that are frequently asked during technical interviews for senior positions. Let's dive in!
Both volatile
and synchronized
are used for thread synchronization, but they serve different purposes:
volatile
ensures that a variable is always read from and written to main memory, preventing thread caching. It's useful for simple flags or status indicators.synchronized
provides exclusive access to a block of code or method, ensuring that only one thread can execute it at a time.Example:
public class Counter { private volatile int count = 0; public synchronized void increment() { count++; } }
In this example, count
is declared as volatile
to ensure visibility across threads, while the increment()
method is synchronized
to prevent race conditions.
The Java Memory Model (JMM) defines how the Java virtual machine (JVM) interacts with the computer's memory. It specifies how and when different threads can see values written by other threads, and how to synchronize access to shared data.
Key aspects of the JMM include:
Understanding the JMM is crucial for writing thread-safe code and avoiding concurrency issues like race conditions and visibility problems.
The Fork/Join framework, introduced in Java 7, is designed for efficient parallel processing of recursive algorithms. It's based on the divide-and-conquer approach, where a large task is broken down into smaller subtasks that can be processed concurrently.
Key components:
Example use case: Parallel merge sort
public class ParallelMergeSort extends RecursiveTask<int[]> { private int[] array; private int start; private int end; // Constructor and other methods omitted for brevity @Override protected int[] compute() { if (end - start <= THRESHOLD) { return sequentialSort(array, start, end); } int mid = (start + end) / 2; ParallelMergeSort leftTask = new ParallelMergeSort(array, start, mid); ParallelMergeSort rightTask = new ParallelMergeSort(array, mid, end); leftTask.fork(); int[] rightResult = rightTask.compute(); int[] leftResult = leftTask.join(); return merge(leftResult, rightResult); } }
ConcurrentHashMap is a thread-safe implementation of the Map interface, designed for high concurrency scenarios. Its internal structure is divided into segments, each of which can be locked independently, allowing multiple threads to access different segments simultaneously.
Key features:
Advantages over HashMap:
Example usage:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); map.put("A", 1); map.put("B", 2); // Multiple threads can safely access the map concurrently Runnable task = () -> { String key = Thread.currentThread().getName(); map.compute(key, (k, v) -> (v == null) ? 1 : v + 1); }; // Start multiple threads for (int i = 0; i < 10; i++) { new Thread(task).start(); }
Soft leaks occur when objects are no longer needed by the application but are still being referenced, preventing them from being garbage collected. Unlike hard leaks, soft leaks don't cause immediate OutOfMemoryErrors but can lead to performance degradation over time.
Common causes of soft leaks:
Prevention strategies:
Example of preventing a soft leak with WeakHashMap:
public class Cache<K, V> { private final Map<K, V> cache = new WeakHashMap<>(); public V get(K key) { return cache.get(key); } public void put(K key, V value) { cache.put(key, value); } }
CompletableFuture, introduced in Java 8, is an enhancement over the Future interface, providing more flexibility and powerful features for asynchronous programming.
Key differences:
Example of chaining CompletableFuture operations:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello") .thenApply(s -> s + " World") .thenApply(String::toUpperCase) .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + "!")); future.thenAccept(System.out::println);
This example demonstrates how CompletableFuture can be used to chain multiple asynchronous operations, transforming the result at each stage.
Garbage Collection (GC) in Java is the automatic process of identifying and removing objects that are no longer needed by the application. The GC process generally consists of two phases:
Common GC algorithms:
Example of selecting a GC algorithm:
java -XX:+UseG1GC -jar myapp.jar
This command starts a Java application using the G1 garbage collector.
The Diamond Problem is an ambiguity that arises in multiple inheritance scenarios when a class inherits from two interfaces that have a method with the same signature. Java addresses this problem through its multiple inheritance of interfaces and default methods introduced in Java 8.
When a class implements two interfaces with conflicting default methods, the compiler forces the implementing class to override the method, resolving the ambiguity.
Example:
interface A { default void foo() { System.out.println("A's foo"); } } interface B { default void foo() { System.out.println("B's foo"); } } class C implements A, B { @Override public void foo() { A.super.foo(); // Choose A's implementation } }
In this example, class C must override the foo()
method to resolve the conflict between A and B's default implementations.
Method references in Java 8 provide a way to refer to methods or constructors without invoking them. They are a shorthand notation for lambda expressions that call a specific method.
Types of method references:
ClassName::staticMethodName
objectInstance::instanceMethodName
ClassName::instanceMethodName
ClassName::new
Examples:
// Static method reference List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); numbers.forEach(System.out::println); // Instance method reference of a particular object String str = "Hello"; Predicate<String> startsWithH = str::startsWith; // Instance method reference of an arbitrary object List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); names.sort(String::compareToIgnoreCase); // Constructor reference Supplier<List<String>> listSupplier = ArrayList::new;
While both abstract classes and interfaces are used to define abstract types, they have some key differences:
Example:
abstract class Animal { protected String name; public Animal(String name) { this.name = name; } abstract void makeSound(); } interface Swimmable { default void swim() { System.out.println("Swimming..."); } } class Duck extends Animal implements Swimmable { public Duck(String name) { super(name); } @Override void makeSound() { System.out.println("Quack!"); } }
In this example, Duck
extends the abstract class Animal
and implements the interface Swimmable
, showcasing the differences between the two.
The Java ClassLoader is responsible for loading Java classes and interfaces into the Java Virtual Machine (JVM) during runtime. It's part of the Java Runtime Environment (JRE) and plays a crucial role in Java's dynamic class loading feature.
Types of ClassLoaders:
The ClassLoader follows these principles:
Example of creating a custom ClassLoader:
public class CustomClassLoader extends ClassLoader { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { if (name.startsWith("com.myapp")) { return findClass(name); } return super.loadClass(name); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // Custom class loading logic here byte[] classData = loadClassData(name); return defineClass(name, classData, 0, classData.length); } private byte[] loadClassData(String name) { // Load the class data from a file or network // Implementation omitted for brevity } }
Fail-fast and fail-safe iterators differ in how they handle concurrent modifications to the collection they're iterating over:
Fail-fast iterators:
Fail-safe iterators:
Example of fail-fast iterator:
List<String> list = new ArrayList<>(); list.add("A"); list.add("B"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String element = iterator.next(); if (element.equals("B")) { list.remove(element); // This will throw ConcurrentModificationException } }
Example of fail-safe iterator:
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); list.add("A"); list.add("B"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String element = iterator.next(); if (element.equals("B")) { list.remove(element); // This won't throw an exception } }
Method overloading and method overriding are two important concepts in Java that allow for polymorphism.
Method Overloading:
Method Overriding:
Example of method overloading:
public class Calculator { public int add(int a, int b) { return a + b; } public double add(double a, double b) { return a + b; } public int add(int a, int b, int c) { return a + b + c; } }
Example of method overriding:
class Animal { public void makeSound() { System.out.println("Animal makes a sound"); } } class Dog extends Animal { @Override public void makeSound() { System.out.println("Dog barks"); } }
The Java Module System, introduced in Java 9 as part of Project Jigsaw, is a major enhancement to the Java platform. It allows developers to create modular applications and libraries, improving encapsulation, dependency management, and security.
Key features of the Module System:
Example of a module declaration:
module com.myapp.core { requires java.sql; requires com.myapp.utils; exports com.myapp.core.api; }
This module declaration specifies that the module:
The Module System improves application development by:
16/10/2024 | Java
24/09/2024 | Java
16/10/2024 | Java
30/10/2024 | Java
23/09/2024 | Java
30/10/2024 | Java
24/09/2024 | Java
30/10/2024 | Java
24/09/2024 | Java
30/10/2024 | Java
23/09/2024 | Java
28/09/2024 | Java