Java Interview Questions for 10 Years Experience

- Recommended to read and brush up for your interview:
- Must know interview questions and concept for any Java Interview
- Spring boot interview Questions for 10 years experiance
Explain the Java Memory Model in detail, what is happens-before, Volatile Variables, Memory Visibility? Also give example.
The Java Memory Model (JMM) defines how threads in a multi-threaded Java program interact with memory and each other. Understanding JMM is crucial for ensuring thread safety in high-concurrency scenarios.
Concepts in the Java Memory Model: 1. Happens-Before Relationship:The "happens-before" relationship is the core of the JMM. It is a partial ordering on memory operations: if action A happens-before action B, then everything A wrote is guaranteed to be visible to B. Without a happens-before edge, the JVM and CPU are free to reorder and cache, and B may see stale data.
The important part is knowing what actually creates a happens-before edge. These are the rules you should be able to list in an interview:
- Program order: within a single thread, each statement happens-before the statements that follow it.
- Monitor lock: releasing a lock happens-before any subsequent acquisition of that same lock.
- Volatile: a write to a volatile field happens-before every subsequent read of that same field.
- Thread start:
Thread.start()happens-before any action in the started thread. - Thread join: every action in a thread happens-before another thread returning from
join()on it. - Final fields: correctly initialised final fields are visible to any thread that sees the object after the constructor completes, without extra synchronisation.
- Transitivity: if A happens-before B and B happens-before C, then A happens-before C.
Note what is not on this list: simply writing a value in one thread and reading it in another creates no ordering at all. That is exactly the bug most concurrency interviews are probing for.
2. Volatile Variables:Declaring a field volatile gives you two guarantees: the value is never cached in a way that hides it from other threads, and the compiler and CPU may not reorder operations across the volatile access. In happens-before terms, a volatile write happens-before every later volatile read of that field.
What volatile does not give you is atomicity. Any read-modify-write — count++, flag = !flag, total += x — is three separate operations, and volatile does nothing to make them a single indivisible step. Two threads can interleave and lose an update. Volatile is for publishing a value, not for updating one based on its old value.
Here is the pattern volatile is actually correct for — a flag that is written by one thread and only read by another:
package codeKatha;
public class VolatileExample {
// Written by the control thread, read by the worker thread.
private volatile boolean running = true;
public void stop() {
running = false; // a plain write, not a read-modify-write
}
public void runWorker() {
while (running) {
// do work
}
// Without volatile, this loop may never exit: the JIT is
// allowed to hoist the read of `running` out of the loop.
}
}
If you need to toggle a flag safely, volatile is the wrong tool — use AtomicBoolean:
package codeKatha;
import java.util.concurrent.atomic.AtomicBoolean;
public class ToggleExample {
private final AtomicBoolean flag = new AtomicBoolean(false);
public boolean toggle() {
boolean current, next;
do {
current = flag.get();
next = !current;
} while (!flag.compareAndSet(current, next));
return next;
}
}
3. Memory Visibility:
Memory visibility is the property that ensures changes made by one thread to shared variables are visible to other threads. Without a happens-before edge, changes made by one thread may never be visible to others, leading to data inconsistencies and bugs that reproduce only under load. Locks, volatile fields, final fields and the java.util.concurrent classes are the tools that create those edges.
To ensure thread safety in Java applications, especially in high-concurrency scenarios, you can follow these principles:
1. Use Synchronization:Use synchronized blocks or methods to protect critical sections of code. This ensures that only one thread can execute the synchronized region at a time, and it also creates the happens-before edge that makes the changes visible to the next thread that acquires the lock.
package codeKatha;
public class SynchronizationExample {
private int sharedVariable = 0;
public synchronized void increment() {
sharedVariable++;
}
public synchronized int get() {
return sharedVariable; // the read must be synchronized too
}
}
A point candidates often miss: guarding only the write is not enough. If get() were unsynchronized, a reader could still see a stale value.
Use volatile for fields that are shared across threads and are written independently of their previous value — status flags, a cached configuration reference, a "shutdown requested" signal. See the example above.
3. Use Thread-Safe Data Structures:Prefer thread-safe data structures from the java.util.concurrent package, such as ConcurrentHashMap or ConcurrentLinkedQueue, when dealing with shared collections or queues. Note that individual operations on a ConcurrentHashMap are atomic, but a get-then-put sequence is not — use computeIfAbsent or merge for that.
Use atomic classes like AtomicInteger or AtomicLong to perform compound actions atomically without the need for locks or synchronized blocks. Internally these use a compare-and-swap (CAS) instruction rather than a lock.
package codeKatha;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private final AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet();
}
public int get() {
return counter.get();
}
}
Under heavy contention from many threads, LongAdder usually outperforms AtomicLong because it spreads updates across multiple cells instead of having every thread CAS the same variable.
Discuss the differences between the Serializable and Externalizable interfaces in Java. When would you use one over the other for object serialization, and what are the potential performance implications?
In Java, the Serializable and Externalizable interfaces are used for object serialization, but they differ in terms of customization, control and performance. Let's explore the differences and when to use one over the other.
Serializable Interface:The Serializable interface is a marker interface — it declares no methods. It simply indicates that a class can be serialized (converted into a byte stream) and deserialized (reconstructed from a byte stream). The serialization and deserialization process is handled by the Java runtime, and you have limited control over it.
Two things every experienced candidate is expected to mention here:
serialVersionUID: a version identifier for the class. If you do not declare it, the compiler generates one from the class structure — so adding a single field changes the generated ID and every previously serialized object fails to deserialize with anInvalidClassException. Always declare it explicitly.transient: marks a field that should be skipped during serialization — passwords, caches, open connections, or anything not meaningfully restorable. On deserialization such fields come back as the default value (null,0,false).
package codeKatha;
import java.io.*;
public class SerializableExample implements Serializable {
private static final long serialVersionUID = 1L;
private int data;
private transient String cachedToken; // never written to the stream
public SerializableExample(int data) {
this.data = data;
}
public static void main(String[] args) {
// Serialization
try (ObjectOutputStream oos =
new ObjectOutputStream(new FileOutputStream("object.ser"))) {
oos.writeObject(new SerializableExample(42));
} catch (IOException e) {
e.printStackTrace();
}
// Deserialization
try (ObjectInputStream ois =
new ObjectInputStream(new FileInputStream("object.ser"))) {
SerializableExample obj = (SerializableExample) ois.readObject();
System.out.println(obj.data); // 42
System.out.println(obj.cachedToken); // null - it was transient
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Note also that during Serializable deserialization the constructor of the class is never called. The JVM allocates the object and populates the fields directly. This is why invariants you enforce in a constructor can be bypassed by a crafted byte stream.
Externalizable extends Serializable but hands the entire process to you. You must implement writeExternal and readExternal, which define exactly what is written and read.
The difference interviewers actually probe: Externalizable requires a public no-argument constructor. During deserialization the runtime calls that constructor to create an empty instance, and then calls readExternal on it to populate the fields. If the class has no accessible no-arg constructor, deserialization fails with an InvalidClassException.
package codeKatha;
import java.io.*;
public class ExternalizableExample implements Externalizable {
private int data;
private String name;
// REQUIRED: public no-arg constructor, called during deserialization
public ExternalizableExample() {
}
public ExternalizableExample(int data, String name) {
this.data = data;
this.name = name;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// You control the format and the order completely
out.writeInt(data);
out.writeUTF(name);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// Must read back in exactly the same order it was written
data = in.readInt();
name = in.readUTF();
}
public static void main(String[] args) {
try (ObjectOutputStream oos =
new ObjectOutputStream(new FileOutputStream("object.ser"))) {
oos.writeObject(new ExternalizableExample(42, "Advait"));
} catch (IOException e) {
e.printStackTrace();
}
try (ObjectInputStream ois =
new ObjectInputStream(new FileInputStream("object.ser"))) {
ExternalizableExample obj = (ExternalizableExample) ois.readObject();
System.out.println(obj.data + " / " + obj.name);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Quick comparison:
Serializable Externalizable
-------------------------------------------------------------------
Methods to implement none (marker) writeExternal, readExternal
Constructor on read not called public no-arg IS called
Control over format minimal complete
Inherited fields handled automatically you must handle them
Boilerplate almost none significant
Risk serialises too much easy to get read/write order wrong
When to Use Serializable vs. Externalizable:
- Use Serializable when you want a simple and straightforward way to serialize and deserialize objects with minimal effort. It is suitable for most cases.
- Use Externalizable when you need full control — a compact custom wire format, omitting fields, versioning the payload yourself, or encrypting during serialization. Keep in mind it requires more code and the read and write sides must be kept in lockstep.
Performance Implications:
- Serializable relies on reflection and writes class metadata alongside the data, so the output is larger and slower to produce than a hand-written format.
- Externalizable lets you write only what you need and skip the reflective field walk, producing smaller and faster payloads. The gain is real for hot paths and negligible for occasional use.
- In practice: for anything crossing a network or a service boundary, most teams now skip Java serialization entirely and use JSON, Protobuf or Avro. Java's built-in serialization has a long history of deserialization vulnerabilities, and you should never deserialize a byte stream from an untrusted source.
Could you provide the differences between Heap and Stack Memory in the context of Java, and also provide insights into how these memory areas are used?
In Java, memory management plays a crucial role in determining how objects are stored and accessed during program execution. Heap and Stack Memory are two distinct regions where different types of data are managed. Understanding their differences is essential for efficient memory utilisation and managing object lifecycles.
Stack MemoryStack Memory is a region used for storing method calls, local variables, and references to objects. It operates in a Last-In-First-Out (LIFO) manner, resembling a stack of items. Each method call creates a new frame in the stack, containing variables specific to that method. Every thread gets its own stack, which is why local variables are never shared between threads.
Stack Memory is relatively fast for allocation and deallocation because it follows a strict order — a frame is simply popped when the method returns. However, it has limited space; unbounded recursion overflows it and throws StackOverflowError.
A precise point that trips people up: the stack holds local variables — both primitive locals and references to heap objects. It does not hold primitives in general. A primitive that is an instance field lives inside the object on the heap, not on the stack.
Example: Using Stack Memory
package codeKatha;
public class StackMemoryExample {
public static void main(String[] args) {
int a = 5;
int b = 10;
int sum = addNumbers(a, b);
System.out.println("Sum: " + sum);
}
public static int addNumbers(int x, int y) {
int result = x + y;
return result;
}
}
In this example, the variables 'a', 'b', 'x', 'y', 'result', and 'sum' are all local variables and live in Stack Memory. As methods are called and return, their corresponding frames are pushed and popped.
Stack Memory (while addNumbers is executing)
--------------------------------------------------
[addNumbers Frame]
result = 15
y = 10
x = 5
[main Frame]
sum = (not assigned yet - assigned once addNumbers returns)
b = 10
a = 5
Heap Memory
Heap Memory is a region used for dynamic memory allocation, primarily for objects that have varying lifetimes. Unlike Stack Memory, Heap Memory doesn't have a strict order, and objects are reclaimed by the garbage collector rather than on method return.
Objects stored in the Heap are accessed through references stored in the Stack. Objects in Heap Memory can exist beyond the scope of a single method and can be shared among multiple methods or even different threads — which is exactly why heap data needs synchronisation and stack data does not.
Example: Using Heap Memory
package codeKatha;
public class HeapMemoryExample {
public static void main(String[] args) {
Person person1 = new Person("Shanav", 25);
Person person2 = new Person("Advait", 30);
person1.sayHello();
person2.sayHello();
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void sayHello() {
System.out.println("Hello, my name is " + name + " and I'm " + age + " years old.");
}
}
In this example, the 'Person' objects are created in the Heap Memory using the 'new' keyword. The references to these objects ('person1' and 'person2') are stored in the Stack Memory. Note that the age field is a primitive, but because it is an instance field it lives inside the Person object on the heap.
Stack (main frame) Heap
------------------------- ------------------------------
person1 ---------------> Person { name="Shanav", age=25 }
person2 ---------------> Person { name="Advait", age=30 }
Two follow-ups worth knowing
- Metaspace: class metadata used to live in PermGen, which was part of the heap. Since Java 8 it lives in Metaspace, which is native memory outside the heap and grows dynamically.
OutOfMemoryError: PermGen spaceno longer exists; you may seeOutOfMemoryError: Metaspaceinstead. - String pool: the interned string pool moved from PermGen to the heap in Java 7, which means interned strings are now garbage collected normally.
Understanding the distinctions between Heap and Stack Memory is vital for efficient memory usage. Stack Memory is suited for small and short-lived data, while Heap Memory is used for dynamically allocated objects with varying lifetimes. Effective memory management ensures that objects become unreachable when no longer needed, preventing memory leaks and improving program performance.
Explain the differences between abstract classes and interfaces in Java. When should you use one over the other?
- Abstract Classes: Can have both abstract and concrete methods, can have instance fields with any access modifier, can have constructors, can hold state, and support single inheritance. A class can extend only one abstract class.
- Interfaces: Can declare abstract methods, and since Java 8 can also provide
defaultandstaticmethods with a body. Since Java 9 they can additionally haveprivatemethods, used to share code between default methods. Interfaces cannot hold instance state — any field declared in an interface is implicitlypublic static final. They have no constructors, and a class can implement multiple interfaces.
So the sharpest one-line distinction is not "implementation vs no implementation" — default methods blurred that in Java 8. It is state and construction: an abstract class can hold instance fields and run a constructor, an interface cannot.
- If you want to provide a partial implementation or share common functionality across closely related classes.
- If your abstraction needs instance fields and constructor logic.
- If you want to enforce a specific inheritance hierarchy.
- If you want to provide a contract that multiple unrelated classes can implement.
- If you need a class to inherit from multiple abstractions.
- If you want to provide a common API for different implementations.
Since Java 17, sealed interfaces and classes give you a third option: a contract that only a fixed, declared set of types may implement — useful for modelling closed hierarchies that the compiler can exhaustively check in a switch.
Explain the concept of method overloading and method overriding in Java. What are the rules for each?
- Methods must have the same name but different parameter lists.
- Methods can have different return types, access modifiers, and exception lists.
- Return type alone is not enough — two methods that differ only in return type will not compile.
- Constructor overloading is also possible in Java.
- Same method name and same parameter list as the superclass method.
- The return type may be the same or a subtype of the superclass method's return type. This is called a covariant return type and has been legal since Java 5. For example, if the parent declares
Object clone(), the child may declareEmployee clone(). - The access level of the overriding method cannot be more restrictive than the superclass method. It may be wider — a
protectedmethod can be overridden aspublic. - If the superclass method throws checked exceptions, the overriding method can throw the same exceptions, subclasses of those exceptions, or no exception, but it cannot throw a new or broader checked exception. Unchecked exceptions are unrestricted.
private,staticandfinalmethods cannot be overridden. Declaring astaticmethod with the same signature in a subclass is method hiding, not overriding — it is resolved by reference type at compile time, which is a classic trick question.- The
@Overrideannotation is not required, but it makes the compiler verify that you really are overriding something. Use it always; it catches typos and signature drift.
Explain the concept of Java generics. What are some benefits of using generics, and what are some common use cases?
- Type safety: Generics help ensure type safety by checking the types of objects at compile time. This catches type-related errors early and reduces the chances of a runtime
ClassCastException. - Code reusability: Generics enable you to write generic classes, interfaces, and methods that can be reused with different types, reducing code duplication and increasing maintainability.
- Improved readability: Generics make the code more expressive and easier to read, as the types of objects being used are explicitly defined.
Type erasure is the follow-up question that almost always comes next. Generics exist only at compile time — the compiler inserts the checks and the casts, then erases the type parameters. At runtime a List<String> and a List<Integer> are both just List. That is why you cannot write new T[10], cannot use instanceof List<String>, and cannot overload two methods that differ only by their generic parameter.
- Collections: One of the most common use cases for generics is in the Java Collections Framework. Generics allow you to create type-safe collections like List<String>, Set<Integer>, and Map<String, Integer>, ensuring that only the specified type of objects can be added or retrieved from the collection.
- Custom generic classes and interfaces: You can create your own generic classes and interfaces to handle multiple data types while maintaining type safety. For example, you could create a generic Pair class that can store two objects of different types:
public class Pair<K, V> {
private final K key;
private final V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
public static <T extends Comparable<T>> T findMax(List<T> list) {
if (list.isEmpty()) {
throw new IllegalArgumentException("list must not be empty");
}
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) {
max = item;
}
}
return max;
}
Explain the difference between final, finally, and finalize in Java.
- final variables: A final variable can be assigned a value only once, either at declaration or in a constructor. Note that final means the reference cannot be reassigned — a
final Listcan still have elements added to it. - final methods: A final method cannot be overridden by a subclass, ensuring that the method's behavior remains consistent across the class hierarchy.
- final classes: A final class cannot be extended. This is useful for creating immutable classes or classes whose behaviour must not be altered —
Stringis the best-known example.
try {
// Code that might throw an exception
} catch (IOException e) {
// Code to handle the exception
} finally {
// Always executed, whether or not an exception was thrown
}
In modern code, prefer try-with-resources over a finally block for anything that implements AutoCloseable. It closes resources in reverse order, and it correctly handles the case where both the body and the close throw — the close exception is attached as a suppressed exception rather than swallowing the original:
try (BufferedReader reader = Files.newBufferedReader(path)) {
return reader.readLine();
} // reader.close() is called automatically
finalize() was a protected method on java.lang.Object, called by the garbage collector before reclaiming an object. It was deprecated in Java 9 and removed entirely in Java 18. If you are on any recent JDK, it no longer exists and there is nothing to override.It was removed because it was unfixable: there was no guarantee it would ever run, it ran on an unpredictable thread, it could resurrect the object being collected, and it delayed reclamation by at least one GC cycle. The replacements are:
- try-with-resources for anything with a clear scope — this covers the overwhelming majority of cases.
java.lang.ref.Cleaner(Java 9+) as a safety net for native resources, where you register an action that runs after the object becomes unreachable. The cleanup action must not hold a reference to the object it is cleaning, or it will never become unreachable.
package codeKatha;
import java.lang.ref.Cleaner;
public class NativeResource implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
// Must NOT be an inner class and must not reference NativeResource
private static class ReleaseTask implements Runnable {
private final long handle;
ReleaseTask(long handle) {
this.handle = handle;
}
@Override
public void run() {
// release the native handle
}
}
private final Cleaner.Cleanable cleanable;
public NativeResource(long handle) {
this.cleanable = CLEANER.register(this, new ReleaseTask(handle));
}
@Override
public void close() {
cleanable.clean(); // deterministic cleanup - the normal path
}
}
Explain the difference between the Comparable and Comparator interfaces in Java.
Comparable<T> and defines compareTo(), which returns a negative number, zero, or a positive number depending on whether this object sorts before, equal to, or after the argument. public class Employee implements Comparable<Employee> {
private int id;
private String name;
// Constructor, getters, and setters
@Override
public int compareTo(Employee other) {
return Integer.compare(this.id, other.id);
}
}
Two things to get right: use Integer.compare(a, b) rather than a - b, because subtraction overflows for large or negative values and silently produces the wrong order. And your natural ordering should normally be consistent with equals — if compareTo returns 0 for two objects that are not equals, a TreeSet will treat them as duplicates while a HashSet will not.
// Sort by name
employeeList.sort(Comparator.comparing(Employee::getName));
// Sort by department, then by salary descending, then by name
employeeList.sort(
Comparator.comparing(Employee::getDepartment)
.thenComparing(Employee::getSalary, Comparator.reverseOrder())
.thenComparing(Employee::getName));
// Nulls last, using the natural order otherwise
employeeList.sort(Comparator.nullsLast(Comparator.naturalOrder()));
// Reverse the natural order defined by compareTo()
employeeList.sort(Comparator.reverseOrder());
Use comparingInt, comparingLong or comparingDouble when the key is a primitive — it avoids boxing on every comparison.
The older style still compiles and you will meet it in legacy code:
public class EmployeeNameComparator implements Comparator<Employee> {
@Override
public int compare(Employee e1, Employee e2) {
return e1.getName().compareTo(e2.getName());
}
}
Collections.sort(employeeList, new EmployeeNameComparator());
What are the key differences between ArrayList and LinkedList in Java?
- Underlying data structure: a dynamic array.
- Access time: fast random access, O(1), because the index maps directly to an array offset.
- Insertion and deletion: inserting or deleting in the middle is O(n) because the remaining elements must be shifted. Appending at the end is amortised O(1).
- Memory overhead: lower than LinkedList — no per-element node object, though the backing array may have unused capacity.
- Use cases: the correct default. Random access is common, and iteration is fast because the elements sit contiguously in memory.
List<String> arrayList = new ArrayList<>();
- Underlying data structure: a doubly-linked list.
- Access time: slow random access, O(n), since it must walk the chain from whichever end is nearer.
- Insertion and deletion: O(1) only if you already hold the node — for example while iterating with
ListIterator.remove().list.add(index, element)is still O(n), because finding the position requires the same traversal. This is the most common misconception about LinkedList. - Memory overhead: higher — each element needs a node object with two pointers, which is typically 24–40 bytes of overhead per element.
- Use cases: queue or deque behaviour where you add and remove at the ends, and iterator-driven removal from the middle.
List<String> linkedList = new LinkedList<>();
// For queue/deque usage, declare it as a Deque instead
Deque<String> deque = new ArrayDeque<>();
The practical answer: in real benchmarks ArrayList usually wins even for workloads that look like LinkedList's strength, because contiguous memory is far friendlier to the CPU cache than chasing pointers across the heap. If you need a queue or deque, ArrayDeque is faster than LinkedList for almost every operation. Reach for LinkedList rarely and with a measurement in hand.
What is the purpose of the hashCode() and equals() methods in Java?
- If two objects are equal according to
equals(), they must return the samehashCode(). - The reverse is not required — two unequal objects may share a hash code. That is a collision, and it is handled by the collection.
- Therefore: whenever you override
equals(), you must overridehashCode()as well. Overriding one without the other is the single most common source of "my object went into the HashMap and I can never get it back out".
- Reflexive: x.equals(x) should return true for any non-null reference value x.
- Symmetric: x.equals(y) should return the same value as y.equals(x).
- Transitive: if x.equals(y) and y.equals(z) are both true, then x.equals(z) must be true.
- Consistent: repeated invocations return the same value, provided neither object is modified.
- x.equals(null) must return false for any non-null x.
package codeKatha;
import java.util.Objects;
public class Employee {
private final int id;
private final String email;
public Employee(int id, String email) {
this.id = id;
this.email = email;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee other = (Employee) o;
return id == other.id
&& Objects.equals(email, other.email);
}
@Override
public int hashCode() {
// Must use the same fields as equals(), in any order
return Objects.hash(id, email);
}
}
The mutable key trap. If you put an object into a HashMap and then change a field that hashCode() depends on, the object is now sitting in the wrong bucket. A lookup computes the new hash, looks in a different bucket, and finds nothing — the entry is effectively leaked. This is why the fields used in equals and hashCode should be final wherever possible, and why mutable objects make poor map keys.
Records do this for you. Since Java 16, a record automatically generates equals(), hashCode() and toString() from its components, and its fields are final. For a pure data carrier, prefer a record over hand-written boilerplate:
public record Employee(int id, String email) { }
What are some common uses of Java Reflection API?
- Dynamic object creation: Reflection allows you to instantiate objects without knowing their class at compile time. This can be useful for creating objects based on user input or configuration files.
- Method invocation: With Reflection, you can invoke methods on objects without knowing the methods at compile time. This is helpful for implementing features like plugins or scripting languages that interact with Java code.
- Accessing private members: Reflection can be used to access private fields and methods of an object, which can be useful for testing or debugging purposes. However, using Reflection to break encapsulation in regular code is discouraged.
- Inspecting class metadata: Reflection allows you to retrieve information about classes, such as their fields, methods, constructors, and annotations. This is how frameworks like Spring, Hibernate and Jackson do their work.
package codeKatha;
import java.lang.reflect.Field;
class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public void display() {
System.out.println("Name: " + name + ", Age: " + age);
}
}
public class ReflectionExample {
public static void main(String[] args) {
// Create an instance of the Student class
Student student = new Student("Advait", 20);
// Use Reflection to access and modify private fields
Class<?> studentClass = student.getClass();
try {
// Access the 'name' field
Field nameField = studentClass.getDeclaredField("name");
nameField.setAccessible(true); // Allow access to private field
String nameValue = (String) nameField.get(student);
System.out.println("Name Field: " + nameValue);
// Access the 'age' field
Field ageField = studentClass.getDeclaredField("age");
ageField.setAccessible(true);
int ageValue = (int) ageField.get(student);
System.out.println("Age Field: " + ageValue);
// Modify the 'name' field
nameField.set(student, "Shanav");
System.out.print("Updated: ");
student.display(); // display() returns void - call it, don't concatenate it
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
In this example we define a Student class with private fields, obtain its Class object via getClass(), then use getDeclaredField() with setAccessible(true) to read and modify those private fields.
Two caveats worth raising in an interview:
- Performance. Reflective access is significantly slower than a direct call and blocks many JIT optimisations. Fine at startup or in a framework's wiring phase; not something to put in a hot loop.
- Strong encapsulation. Since Java 16,
setAccessible(true)on JDK-internal classes fails by default — it throwsInaccessibleObjectExceptionunless the module is explicitly opened with--add-opens. Reflection on your own classes, as above, still works normally.
Define the role of a ClassLoader in Java, its hierarchy, and its primary functions.
ClassLoader Role and Significance:A ClassLoader is the component responsible for dynamically loading classes at runtime. It locates class files — from the file system, a JAR, or a network location — and turns their bytes into an instance of java.lang.Class that the JVM can use. Classes are loaded lazily, on first use, not all at startup.
Primary functions:
- Loading: reads the binary data of a class and converts it into an instance of
java.lang.Class. - Linking: verifies the bytecode is well-formed and safe, prepares the class by allocating static fields with default values, and resolves symbolic references.
- Initialization: runs static initializer blocks and assigns the declared values to static fields.
ClassLoader Hierarchy:
Java uses a hierarchical system. The names changed in Java 9 with the module system, and interviewers do notice if you give the old ones:
- Bootstrap ClassLoader: the top-level loader, written in native code, responsible for the core Java classes such as
java.lang.*. It has no parent andgetClassLoader()returnsnullfor classes it loaded. - Platform ClassLoader: loads the remaining JDK platform modules. Before Java 9 this was called the Extension ClassLoader and it loaded JARs from the
jre/lib/extdirectory. That extension mechanism was removed in Java 9 — mentioning "Extension ClassLoader" as current is a common giveaway that the answer is a decade old. - Application (System) ClassLoader: loads classes from the application's classpath. This is the one that loads your own code.
- Custom ClassLoaders: you can subclass
ClassLoaderto load from non-standard sources — this is how application servers, plugin systems and hot-reloading frameworks isolate code.
The Parent Delegation Model:
This is the part most answers get backwards. A class loading request does not start at the top and work down. It arrives at the Application ClassLoader, which delegates upward to its parent before trying anything itself. Each parent does the same, so the request climbs all the way to Bootstrap. Bootstrap attempts the load first; if it cannot find the class, control returns downward and each loader gets its turn on the way back:
Request arrives at: Application ClassLoader
| delegates up
v
Platform ClassLoader
| delegates up
v
Bootstrap ClassLoader <-- tries FIRST
| not found, returns
v
Platform ClassLoader <-- tries SECOND
| not found, returns
v
Application ClassLoader <-- tries LAST
|
ClassNotFoundException if still not found
The reason for this design is security and consistency. If you put your own java.lang.String on the classpath, delegation guarantees the real one from Bootstrap wins, so core classes cannot be spoofed. It also guarantees a class is loaded once per loader rather than repeatedly.
A related point: class identity includes the ClassLoader. The same class file loaded by two different loaders produces two distinct types, and casting between them throws ClassCastException. This is what makes application-server isolation possible and what causes the confusing "ClassCastException: Foo cannot be cast to Foo" errors.
ClassLoader Example:
package codeKatha;
public class ClassLoaderExample {
public static void main(String[] args) throws Exception {
// Core JDK class -> loaded by Bootstrap -> prints null
System.out.println("ClassLoader for String: " + String.class.getClassLoader());
// Your own class -> loaded by the Application ClassLoader
System.out.println("ClassLoader for this class: "
+ ClassLoaderExample.class.getClassLoader());
// Loading a class dynamically by name
Class<?> myClass = Class.forName("java.util.ArrayList");
// Class.newInstance() was deprecated in Java 9 - use this instead
Object myInstance = myClass.getDeclaredConstructor().newInstance();
System.out.println("Created: " + myInstance.getClass().getName());
}
}
Note that String.class.getClassLoader() prints null, not "Bootstrap". The Bootstrap loader is native and has no Java object representing it — another small detail interviewers like.
What is the difference between Checked and Unchecked Exceptions in Java?
throws. Examples are IOException, SQLException, and ClassNotFoundException. They extend Exception but not RuntimeException. public void readFile(String fileName) throws IOException {
// Read file and handle IOException
}
public int divide(int a, int b) {
// Throws ArithmeticException if b is 0 - no 'throws' clause needed
return a / b;
}
Exception broadly when you only meant to handle one specific failure.What is the Singleton design pattern in Java, and how can it be implemented? Include a code example.
The Singleton design pattern ensures that a class has only one instance and provides a global point of access to it. It is useful for a shared configuration holder, a connection pool, or a single cache instance.
The naive implementation - and why it is broken:This is the version most tutorials show, and the version interviewers hand you specifically to see whether you spot the problem:
package codeKatha;
public class BrokenSingleton {
private static BrokenSingleton instance;
private BrokenSingleton() {
}
public static BrokenSingleton getInstance() {
if (instance == null) { // Thread A and Thread B can
instance = new BrokenSingleton(); // BOTH pass this check
}
return instance;
}
}
This is not thread-safe. Two threads can evaluate instance == null as true before either assigns, and you end up with two instances — which defeats the entire point of the pattern. It works fine in single-threaded tests and fails in production under load. Here are the three implementations that actually work.
The cleanest lazy singleton. The JVM guarantees that class initialization is thread-safe, and the holder class is not initialized until getInstance() is first called. No locking, no volatile, no cost on subsequent calls.
package codeKatha;
public class Singleton {
private Singleton() {
}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
2. Enum Singleton (the most robust):
Recommended in Effective Java. The JVM guarantees a single instance, and unlike every other approach it is automatically safe against reflection and serialization attacks — you cannot call the constructor reflectively, and deserializing does not create a second instance.
package codeKatha;
public enum ConfigManager {
INSTANCE;
private final Properties config = loadConfig();
public String get(String key) {
return config.getProperty(key);
}
}
// Usage
String value = ConfigManager.INSTANCE.get("timeout");
3. Double-Checked Locking (when you need lazy init with arguments):
The volatile keyword here is not optional. Without it, another thread can see a non-null reference to a partially constructed object, because the JVM is allowed to reorder the assignment of the reference ahead of the constructor finishing.
package codeKatha;
public class LazySingleton {
// volatile is REQUIRED - without it this is still broken
private static volatile LazySingleton instance;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (instance == null) { // first check, no lock
synchronized (LazySingleton.class) {
if (instance == null) { // second check, with lock
instance = new LazySingleton();
}
}
}
return instance;
}
}
Usage:
package codeKatha;
public class SingletonExample {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2); // Output: true
}
}
A closing caveat. Singletons carry global mutable state, which makes code hard to test and hides dependencies. In a Spring application you rarely hand-write one at all — a @Component is already a singleton within the application context, and it is injected rather than reached for globally. Being able to say this is often worth more in an interview than the implementation itself.
What is the purpose of the "synchronized" keyword in Java, and when should it be used? Include a code example.
package codeKatha;
public class BankAccount {
private double balance;
public synchronized void deposit(double amount) {
balance += amount;
}
public synchronized void withdraw(double amount) {
balance -= amount;
}
public synchronized double getBalance() {
return balance;
}
}
Note that getBalance() is synchronized too. Without it a reader could see a stale balance even though the writes are guarded — and on a 32-bit JVM a non-volatile double can even be read half-updated, since 64-bit reads are not guaranteed atomic.
- You have shared mutable state, like the balance above, accessed by multiple threads.
- You need a compound action — read, decide, write — to be indivisible. Volatile cannot do this.
- A synchronized instance method locks on
this; a synchronized static method locks on the class object. They are different locks and do not exclude each other. - Locks are reentrant — a thread already holding a lock can acquire it again without deadlocking itself.
- Prefer a private lock object or a synchronized block over synchronizing the whole method, so external code cannot acquire your lock and interfere.
- For anything needing a timeout, a try-lock, or fairness, use
ReentrantLockfromjava.util.concurrent.locksinstead.
What is the difference between a shallow copy and a deep copy in Java, and when should you use each one? Include a code example.
Shallow Copy:A shallow copy creates a new object that is a copy of the original object, but it does not create new copies of the objects referenced by the original. Instead, it copies references to the same objects. As a result, changes to the objects inside the copy are reflected in the original and vice versa.
package codeKatha;
class Student {
String name;
Course course;
public Student(String name, Course course) {
this.name = name;
this.course = course;
}
}
class Course {
String name;
public Course(String name) {
this.name = name;
}
}
public class ShallowCopyExample {
public static void main(String[] args) {
Course course = new Course("Computer Science");
Student originalStudent = new Student("Advait", course);
Student shallowCopyStudent = new Student(originalStudent.name, originalStudent.course);
// Changes in course name affect both original and shallow copy
shallowCopyStudent.course.name = "Mathematics";
System.out.println(originalStudent.course.name); // Output: Mathematics
}
}
Deep Copy:
A deep copy creates a new object and also recursively creates new copies of the objects referenced by the original. This ensures that changes in the copied objects do not affect the original or vice versa.
package codeKatha;
// Uses the same Student and Course classes defined above
public class DeepCopyExample {
public static void main(String[] args) {
Course course = new Course("Computer Science");
Student originalStudent = new Student("Advait", course);
Student deepCopyStudent =
new Student(originalStudent.name, new Course(originalStudent.course.name));
// Changes in course name of deep copy don't affect the original
deepCopyStudent.course.name = "Mathematics";
System.out.println(originalStudent.course.name); // Output: Computer Science
}
}
Key Differences:
- Shallow Copy: Copies references, changes affect both copies.
- Deep Copy: Creates new objects, changes are isolated.
- The referenced objects are immutable (String, Integer, LocalDate, a record of immutable components) — then there is nothing to protect against, and a deep copy would be wasted work.
- The referenced objects are mutable and the copy must be genuinely independent of the original.
This is also what "defensive copying" means. If a getter returns a reference to a mutable internal field, the caller can modify your object's state from outside. Return a copy instead — or, better, make the field immutable in the first place.
Tell me about clone() function, give some example.
Writing copy logic by hand for every class is repetitive, so Java offers clone(). Getting the details right matters, because this is a favourite source of trick questions.
A correction to a very widespread myth: clone() is not provided by the Cloneable interface. Cloneable is a marker interface — it declares no methods at all. clone() is a protected method on java.lang.Object. All Cloneable does is flip a switch: if a class does not implement it, Object.clone() throws CloneNotSupportedException. And because clone() is protected, you must also override it as public for anyone outside the class to call it.
Object.clone() performs a shallow copy by default — it copies each field bit for bit, so reference fields end up pointing at the same objects.
package codeKatha;
class Person implements Cloneable {
String name;
Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
public Person clone() throws CloneNotSupportedException {
// Covariant return type: Object.clone() returns Object,
// we narrow it to Person so callers need no cast.
return (Person) super.clone();
}
}
class Address implements Cloneable {
String city;
public Address(String city) {
this.city = city;
}
@Override
public Address clone() throws CloneNotSupportedException {
return (Address) super.clone();
}
}
public class CloneShallowExample {
public static void main(String[] args) throws CloneNotSupportedException {
Address address = new Address("Delhi");
Person originalPerson = new Person("Advait", address);
Person clonedPerson = originalPerson.clone();
// Both objects share the SAME Address instance
clonedPerson.address.city = "Muzaffarpur";
System.out.println(originalPerson.address.city); // Output: Muzaffarpur
}
}
Note that Address now implements Cloneable and overrides clone() as public. Without both of those, the deep copy below would not even compile — Object.clone() is protected, so address.clone() is not callable from another class.
To get a deep copy, override clone() and clone the referenced objects inside it, so callers cannot forget:
package codeKatha;
class PersonDeep implements Cloneable {
String name;
Address address;
public PersonDeep(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
public PersonDeep clone() throws CloneNotSupportedException {
PersonDeep copy = (PersonDeep) super.clone(); // shallow copy first
copy.address = this.address.clone(); // then deepen it
return copy;
}
}
public class CloneDeepExample {
public static void main(String[] args) throws CloneNotSupportedException {
Address address = new Address("Delhi");
PersonDeep originalPerson = new PersonDeep("Advait", address);
PersonDeep clonedPerson = originalPerson.clone();
// Each object now has its OWN Address instance
clonedPerson.address.city = "Muzaffarpur";
System.out.println(originalPerson.address.city); // Output: Delhi
}
}
Why modern Java code avoids clone():
Effective Java advises against the Cloneable/clone() mechanism altogether. It bypasses constructors, it cannot assign final fields, it throws a checked exception for a case that is usually impossible, and every class in the hierarchy has to cooperate. Prefer a copy constructor or a static factory method — they are plain Java, they work with final fields, and they can return a different type:
// Copy constructor - simpler and safer than clone()
public Person(Person other) {
this.name = other.name;
this.address = new Address(other.address.city); // deep
}
// Static factory alternative
public static Person copyOf(Person other) {
return new Person(other);
}
How has Java evolved over the years, and what are some key features introduced in recent Java versions (Java 8 onwards)?
- Java 8 (LTS): Lambdas, Functional Interfaces, Streams API, Optional, default and static methods in interfaces, and the new Date and Time API. Still the biggest single leap in the language.
- Java 9: Java Platform Module System (JPMS), JShell (REPL), factory methods for collections, Stream API enhancements, private methods in interfaces, and
Cleaneras the replacement forfinalize(). - Java 10: Local variable type inference (
var), Application Class-Data Sharing, and garbage collector improvements. - Java 11 (LTS): The standard HTTP Client API, new String methods, running a single-file source program directly, and local variable syntax for lambda parameters.
- Java 12: Switch expressions (preview), Shenandoah garbage collector, and the JVM Constants API.
- Java 13: Text blocks (preview) and switch expression refinements.
- Java 14: Switch expressions become standard, Records (preview), Pattern Matching for
instanceof(preview), and much more helpful NullPointerException messages. - Java 15: Text blocks become standard, Sealed Classes (preview), and hidden classes.
- Java 16: Records and Pattern Matching for
instanceofbecome standard, and strong encapsulation of JDK internals becomes the default. - Java 17 (LTS): Sealed Classes become standard, Pattern Matching for switch (preview), and the removal of the experimental AOT and JIT compilers. Long the default choice for enterprise projects.
- Java 18: UTF-8 becomes the default charset, and a simple built-in web server for prototyping.
- Java 19: Virtual Threads (preview), Structured Concurrency (incubator), and Record Patterns (preview).
- Java 20: Scoped Values (incubator) and further refinement of the above previews.
- Java 21 (LTS): A landmark release. Virtual Threads become standard, letting you write blocking code that scales to millions of concurrent tasks. Also standard: Pattern Matching for switch, Record Patterns, Sequenced Collections, and Generational ZGC.
- Java 22: The Foreign Function & Memory API becomes standard, replacing JNI for native interop. Also unnamed variables and patterns.
- Java 23: Markdown documentation comments, and continued pattern matching work including primitive types in patterns (preview).
- Java 24: Stream Gatherers become standard, allowing custom intermediate stream operations, plus the Class-File API.
- Java 25 (LTS): The current Long-Term Support release. It finalises a batch of features that had been in preview through 22–24, including Scoped Values, Module Import Declarations, Flexible Constructor Bodies, and Compact Source Files with instance main methods — which finally makes a beginner's "hello world" a three-line program.
If you are asked what to focus on: Java 8's lambdas and streams are assumed knowledge at this level, so they will not differentiate you. Virtual threads (Java 21) are the feature most worth being able to discuss in 2026 — what they solve, how they differ from platform threads, and why pinning on synchronized blocks used to matter. Records, sealed classes and pattern matching together are the second cluster worth knowing well.
Comments
Post a Comment