Article written by Rishabh Dev Choudhary, under the guidance of Marcelo Lotif Araujo, a Senior Software Developer and an AI Engineer. Reviewed by Mrudang Vora, an Engineering Leader with 15+ years of experience.
Java interview questions remain one of the most searched topics among developers preparing for technical rounds, and for good reason. Java powers enterprise backends at companies like Google, Amazon, and Netflix, and continues to dominate software engineering interviews in 2026.
Whether you are a fresher covering OOP and JVM basics or an experienced engineer brushing up on concurrency and Java 8+ features, this guide covers the full spectrum of Java interview questions and answers from foundational concepts to the advanced topics that distinguish mid-level from senior candidates. You will find comparison tables, code snippets, and interview-specific callouts throughout.
For a deeper dive into one of Java’s most tested areas, explore these that frequently come up in senior interviews.
These core Java interview questions test the fundamentals every interviewer expects you to know, from JDK architecture to memory management and language design choices.
These three components are nested, JDK wraps JRE, which wraps JVM. Each layer adds a different layer of capability.
| Component | What It Contains | Purpose |
|---|---|---|
| Java Virtual Machine (JVM) | Bytecode interpreter, JIT compiler, garbage collector, runtime memory areas | Executes compiled Java bytecode on any platform |
| Java Runtime Environment(JRE) | JVM + standard class libraries (java.lang, java.util, etc.) | Provides the minimum environment needed to run Java applications |
| Java Development Kit (JDK) | JRE + compiler (javac), debugger (jdb), profiler, javadoc, and other dev tools | Complete toolkit for developing, compiling, and running Java programs |
Nesting relationship (outer to inner):
JDK ⊃ JRE ⊃ JVM
When you install the JDK, you get everything. End users who only run Java programs need the JRE. The JVM itself is what makes Java platform-independent; it translates bytecode into native machine instructions at runtime.
The JIT (Just-In-Time) compiler is a component of the JVM that improves runtime performance by compiling frequently executed bytecode into native machine code during program execution, rather than interpreting it line by line each time.
Execution flow:
Java Source (.java) → javac → Bytecode (.class) → JVM/JIT → Native Machine Code
The JVM initially interprets bytecode, then identifies ‘hot spots’ (frequently called methods) and compiles them to native code for subsequent calls. This is why Java programs often speed up during warm-up; the JIT is optimizing hot paths in the background.
| Feature | == Operator | .equals() Method |
|---|---|---|
| Compares | References (memory addresses) | Content / logical value |
| Works on | Primitives and object references | Objects only |
| Default behavior (Object) | Reference equality | Reference equality (until overridden) |
| String behavior | True only if same pool object | True if character sequences match |
| Null safety | Safe , null == null is true | Throws NullPointerException if called on null |
The key distinction is that == checks whether two variables point to the exact same memory location, while .equals() checks whether the contents are logically equivalent. This difference matters most with Strings, two new String(“hello”) calls produce objects with identical content but different memory addresses, so == returns false while .equals() returns true.
String a = new String("hello"); String b = new String("hello"); String c = "hello"; String d = "hello"; System.out.println(a == b); // false - different heap objects System.out.println(a.equals(b)); // true - same character content System.out.println(c == d); // true - same String pool reference System.out.println(c.equals(d)); // true - same content
Java achieves platform independence through a two-step compilation model. The Java compiler (javac) does not compile source code to native machine code; it compiles to an intermediate format called bytecode (.class files). Bytecode is platform-neutral and is only converted to native instructions at runtime by the JVM installed on the target machine.
Flow: Java Source → javac → Bytecode → JVM (Windows / Linux / macOS) → Native Code
This is the foundation of Java’s ‘Write Once, Run Anywhere’ (WORA) guarantee that the same .class file runs on any platform that has a compatible JVM, without recompilation.
Java is not purely object-oriented because it supports eight primitive types, int, long, short, byte, float, double, char, and boolean, that are not objects. They are stored directly on the stack as raw values, not as object references on the heap.
// Primitives - not objects, stored on the stack int age = 25; boolean active = true; char grade = 'A'; // Wrapper classes - object equivalents, stored on the heap Integer ageObj = Integer.valueOf(25); Boolean activeObj = Boolean.TRUE; Character gradeObj = Character.valueOf('A');
Java provides autoboxing to convert between primitives and their wrapper class equivalents automatically, but the underlying distinction remains. A purely object-oriented language (like Smalltalk or Ruby) has no primitives at all.
| Feature | Stack Memory | Heap Memory |
|---|---|---|
| Stores | Local variables, method call frames, object references | All objects and instance variables |
| Access speed | Very fast (LIFO structure) | Slower (dynamic allocation, GC overhead) |
| Size | Small and fixed per thread | Large and configurable (-Xmx flag) |
| Thread safety | Thread-local; each thread has its own stack | Shared across all threads; needs synchronization |
| Lifecycle | Freed automatically when the method returns | Managed by Garbage Collector |
| Error on overflow | StackOverflowError (e.g., infinite recursion) | OutOfMemoryError |
Stack and Heap serve entirely different roles in Java’s memory model. The stack is fast, thread-private, and self-managing. It automatically grows and shrinks with method calls and returns. The heap is where all objects live and is shared across threads, which is why concurrent access requires synchronization. Knowing this distinction helps explain why thread safety issues occur and why infinite recursion produces a StackOverflowError.
void createPerson() { // 'p' (the reference) lives on the STACK // The actual Person object lives on the HEAP Person p = new Person("Alice"); } // When createPerson() returns, 'p' is popped off the stack. // The Person object on the heap waits for GC.
Practical rule: when you call new Object(), the reference variable goes on the stack and the object itself goes on the heap. Primitives declared inside a method go on the stack directly.
Encapsulation is the OOP principle of hiding an object’s internal state and requiring all interaction to go through a defined public interface. It is implemented in Java by declaring fields private and providing public getter and setter methods.
public class BankAccount { private double balance; // hidden - not directly accessible public double getBalance() { return balance; } public void deposit(double amount) { if (amount > 0) balance += amount; // validation enforced here } public void withdraw(double amount) { if (amount > 0 && amount <= balance) balance -= amount; } }
The balance cannot be changed directly from outside; all modifications must go through deposit() or withdraw(), which enforces business rules. This is the core benefit of encapsulation: controlled, validated access to internal state.
Strings in Java are immutable and interned in the String pool. A password stored as a String remains in memory until the garbage collector removes it, which could be long after you are done using it. During that window, a memory dump or heap inspection could expose it in plaintext.
A char[] can be explicitly zeroed out as soon as the password is no longer needed:
// RISKY - password lingers in String pool, cannot be cleared String password = "secret123"; // SAFER - char array can be wiped immediately after use char[] password = {'s','e','c','r','e','t','1','2','3'}; // ... use password ... Arrays.fill(password, '�'); // overwrite with null chars
If main() is not declared static, the JVM cannot call it without first creating an instance of the class, but it has no way of knowing how to construct that instance (which constructor arguments to use, etc.). The program will compile but throw a runtime error.
public class Demo { // Missing 'static' - compiles fine, fails at runtime public void main(String[] args) { System.out.println("Will not run"); } } // Runtime output: // Error: Main method is not static in class Demo, please define the main method as: // public static void main(String[] args)
The JVM entry point contract requires: public (accessible), static (callable without an instance), void (no return value), named main, with a String[] parameter.
No, System.exit(0) terminates the JVM immediately. Any finally block that would normally execute after a try or catch block is bypassed entirely because the JVM process itself is shut down before execution can reach it.
public class FinallyTest { public static void main(String[] args) { try { System.out.println("In try"); System.exit(0); } finally { System.out.println("In finally"); // NEVER prints } } } // Output: In try
By contrast, a return statement inside a try block does allow the finally block to execute before the method returns. System.exit() is unique in bypassing finally because it kills the JVM process outright.
String immutability is a deliberate design decision in Java, justified by four separate benefits. Interviewers expect you to know at least three:
String s = "hello"; s.concat(" world"); // Does NOT modify s System.out.println(s); // Prints: hello String s2 = s.concat(" world"); // Creates a NEW String object System.out.println(s2); // Prints: hello world
String handling is one of the most tested areas in Java interviews, from immutability and the String pool to common manipulation methods. For a focused set of questions on this topic, explore Java String interview questions covering pool behaviour, comparison traps, and StringBuilder vs StringBuffer trade-offs.
| Feature | String | StringBuilder | StringBuffer |
|---|---|---|---|
| Mutability | Immutable; every change creates a new object | Mutable, modifies in place | Mutable, modifies in place |
| Thread safety | Inherently thread-safe (immutable) | Not thread-safe | Thread-safe (synchronized methods) |
| Performance | Slow in loops (new object each concat) | Fastest for single-threaded use | Slower than StringBuilder due to synchronization |
| Use case | Constant strings, map keys, config values | String building in loops or single-threaded code | String building in multi-threaded environments |
The three classes form a spectrum of trade-offs between mutability, thread safety, and speed. String is best for values that never change. StringBuilder is the go-to for building strings in single-threaded code, especially in loops, since it mutates one object rather than creating many. StringBuffer adds synchronized methods for thread safety but pays a performance cost in modern multi-threaded code, better concurrency designs often make StringBuffer unnecessary.
// String in a loop - creates 1000 objects (BAD for performance) String result = ""; for (int i = 0; i < 1000; i++) result += i; // StringBuilder in a loop - modifies one object (GOOD) StringBuilder sb = new StringBuilder(); for (int i = 0; i < 1000; i++) sb.append(i); String result = sb.toString();
These hands-on Java coding interview questions test your ability to write clean, efficient code – expect these in technical phone screens and onsite coding rounds.
// Approach 1: StringBuilder.reverse() - clean and readable public static String reverseString(String s) { return new StringBuilder(s).reverse().toString(); } // Approach 2: Manual char array swap - common in interviews public static String reverseManual(String s) { char[] chars = s.toCharArray(); int left = 0, right = chars.length - 1; while (left < right) { char temp = chars[left]; chars[left++] = chars[right]; chars[right--] = temp; } return new String(chars); }
Both approaches are O(n) time and O(n) space. The StringBuilder approach is preferred for production code. The manual approach demonstrates understanding of two-pointer technique, useful to mention in interviews.
// Recursive approach public static int fibonacci(int n) { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2); } // Iterative approach - always mention this alternative public static int fibIterative(int n) { if (n <= 1) return n; int a = 0, b = 1; for (int i = 2; i <= n; i++) { int temp = a + b; a = b; b = temp; } return b; }
The recursive version is elegant but expensive. It recalculates the same sub-problems repeatedly, leading to O(2^n) time complexity. The iterative version keeps track of only two previous values, reducing this to O(n) time and O(1) space. In interviews, writing the recursive solution first and then proactively offering the iterative or memoized alternative demonstrates strong algorithmic awareness and performance thinking.
Time complexity: Recursive = O(2^n), exponential, impractical for large n. Iterative = O(n) time, O(1) space.
String raw = " hello world "; // Java 1.0+: trim() - removes ASCII whitespace only (t, n, r, space) System.out.println(raw.trim()); // "hello world" // Java 11+: strip() - Unicode-aware, handles all whitespace characters System.out.println(raw.strip()); // "hello world" System.out.println(raw.stripLeading()); // "hello world " System.out.println(raw.stripTrailing()); // " hello world"
strip() is preferred in modern Java (11+) because it uses Character.isWhitespace() which handles Unicode whitespace characters that trim() would miss. For codebases on Java 8 or below, trim() remains the standard.
A deadlock occurs when Thread A holds Lock 1 and waits for Lock 2, while Thread B holds Lock 2 and waits for Lock 1, creating a circular wait that never resolves.
public class DeadlockDemo { static final Object lock1 = new Object(); static final Object lock2 = new Object(); public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (lock1) { System.out.println("T1 acquired lock1, waiting for lock2"); try { Thread.sleep(50); } catch (InterruptedException e) {} synchronized (lock2) { // waits - T2 holds lock2 System.out.println("T1 acquired both locks"); } } }); Thread t2 = new Thread(() -> { synchronized (lock2) { System.out.println("T2 acquired lock2, waiting for lock1"); synchronized (lock1) { // waits - T1 holds lock1 System.out.println("T2 acquired both locks"); } } }); t1.start(); t2.start(); // Both threads block forever - deadlock } }
Prevention: always acquire multiple locks in the same fixed order across all threads. If T1 and T2 both acquired lock1 before lock2, the circular wait cannot form.
// Iterative binary search - O(log n) time, O(1) space public static int binarySearch(int[] arr, int target) { int left = 0, right = arr.length - 1; while (left <= right) { int mid = left + (right - left) / 2; // avoids integer overflow if (arr[mid] == target) return mid; else if (arr[mid] < target) left = mid + 1; else right = mid - 1; } return -1; // not found }
Time complexity: O(log n), each iteration halves the search space.
Important: The array must be sorted before binary search can be applied. Note that mid = left + (right – left) / 2 avoids integer overflow that mid = (left + right) / 2 can cause on very large arrays.
import java.util.Arrays; public static boolean sameElements(int[] a, int[] b) { if (a.length != b.length) return false; Arrays.sort(a); Arrays.sort(b); return Arrays.equals(a, b); } // Example: int[] arr1 = {3, 1, 2}; int[] arr2 = {2, 3, 1}; System.out.println(sameElements(arr1, arr2)); // true
Sorting both arrays brings them to the same canonical order, then Arrays.equals() compares element by element.
Time complexity: O(n log n) for sorting. Alternative: use a frequency map (HashMap) for O(n) time if the sort-then-compare approach is not acceptable.
import java.time.LocalDate; import java.time.format.DateTimeFormatter; // Java 8+ (preferred): DateTimeFormatter - immutable and thread-safe LocalDate date = LocalDate.now(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy"); System.out.println(date.format(formatter)); // e.g., "15-03-2026" // Legacy (pre-Java 8): SimpleDateFormat - NOT thread-safe import java.text.SimpleDateFormat; import java.util.Date; SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy"); System.out.println(sdf.format(new Date()));
Prefer DateTimeFormatter from the java.time package (Java 8+), it is immutable and thread-safe. SimpleDateFormat is a known source of concurrency bugs and should be avoided in new code.
import java.util.List; import java.util.Map; List<String> names = List.of("Alice", "Bob", "Carol"); // forEach with lambda names.forEach(name -> System.out.println(name) ); // forEach with method reference (more concise) names.forEach(System.out::println); // Map.forEach with key and value Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 87); scores.forEach((name, score) -> System.out.println(name + ": " + score) );
forEach() is defined on the Iterable interface and on Map. It accepts a Consumer<T> functional interface typically supplied as a lambda or method reference. It is part of Java 8’s internal iteration model and works seamlessly with Streams.
// Java 14+ feature (stable in Java 16+) public record Point(int x, int y) { } // The compiler auto-generates: // - Canonical constructor: Point(int x, int y) // - Accessor methods: p.x(), p.y() // - equals(), hashCode(), toString() Point p1 = new Point(3, 4); Point p2 = new Point(3, 4); System.out.println(p1); // Point[x=3, y=4] System.out.println(p1.equals(p2)); // true System.out.println(p1.x()); // 3
These questions probe your understanding of Java internals, classloaders, constructors, and object mechanics.
A ClassLoader is responsible for loading Java class files into the JVM at runtime. Java uses a delegation hierarchy with three built-in classloaders:
Bootstrap ClassLoader → Extension ClassLoader → Application ClassLoader
| Feature | Constructor | Method |
|---|---|---|
| Name | Must match the class name exactly | Any valid identifier |
| Return type | None; not even a void | Must declare a return type (or void) |
| Invocation | Called automatically by the new keyword | Called explicitly on an object |
| Inheritance | Not inherited by subclasses | Inherited (unless private or static) |
| Purpose | Initialize object state at creation | Define object behaviour/operations |
If you do not define any constructor, Java provides a default no-argument constructor. If you define any constructor with parameters, the default is no longer generated automatically.
The following are the restrictions of using static methods in Java:
public class Counter { private int count = 0; // instance variable private static int total = 0; // class variable public static void showTotal() { System.out.println(total); // OK - static variable // System.out.println(count); // COMPILE ERROR - instance variable // this.count++; // COMPILE ERROR - 'this' not available } }
The this keyword refers to the current object instance. It has three main uses in Java:
public class Person { private String name; private int age; // Use 1: disambiguate field from parameter with same name public Person(String name, int age) { this.name = name; // 'this.name' = field; 'name' = parameter this.age = age; } // Use 2: constructor chaining - call another constructor in same class public Person(String name) { this(name, 0); // calls Person(String, int) } // Use 3: pass current object as an argument public void register(Registry r) { r.add(this); } }
Constructor chaining is the practice of calling one constructor from another to avoid duplicating initialization logic. In Java it is done using this() (same class) or super() (parent class).
public class Vehicle { String type; int speed; Vehicle(String type, int speed) { this.type = type; this.speed = speed; } } public class Car extends Vehicle { int doors; Car(int doors) { this(doors, 120); // chains to Car(int, int) } Car(int doors, int speed) { super("Car", speed); // calls Vehicle(String, int) this.doors = doors; } }
this() or super() must be the first statement in a constructor. This means only one chained call is allowed per constructor.
| Feature | this | super |
|---|---|---|
| Refers to | Current class instance | Immediate parent class instance |
| Constructor call | this() invokes another constructor in the same class | super() invokes the parent class constructor |
| Method call | this.method() calls current class method | super.method() calls overridden parent method |
| Variable access | this.field disambiguates from local variable | super.field accesses hidden parent field |
| Position in constructor | Must be first statement | Must be first statement |
this and super both serve as references within a class, but they point in different directions of the inheritance hierarchy. this always refers to the current object, while super provides a window into the parent class, essential when a subclass overrides a method but still needs to invoke the parent’s implementation. Their constructor-call forms (this() and super()) are mutually exclusive in any single constructor since both must be the first statement.
Object cloning creates a copy of an existing object. In Java, the Object class provides a protected clone() method. To use it, a class must implement the Cloneable marker interface, otherwise clone() throws CloneNotSupportedException.
public class Employee implements Cloneable { String name; int[] scores; // reference type @Override public Employee clone() throws CloneNotSupportedException { return (Employee) super.clone(); // SHALLOW copy } } // Shallow copy: 'name' is copied, but 'scores' array is SHARED // Deep copy: you must manually clone the array too public Employee deepClone() throws CloneNotSupportedException { Employee copy = (Employee) super.clone(); copy.scores = scores.clone(); // clone the array separately return copy; }
Shallow copy: Primitive fields are copied by value; reference fields share the same underlying object.
Deep copy: All nested objects are also cloned, and both copies are fully independent.
Aggregation is a HAS-A relationship where one class contains a reference to another, but both objects have independent lifecycles, the contained object can exist without the container.
// Department HAS-A list of Employees // Employees can exist independently of the Department public class Department { private String name; private List<Employee> employees; // aggregation public Department(String name, List<Employee> employees) { this.name = name; this.employees = employees; } } // If Department is deleted, Employee objects continue to exist
Aggregation differs from Composition: in Composition, the child object cannot exist independently (e.g., a House and its Rooms, if the House is destroyed, the Rooms cease to exist). In Aggregation, the contained object’s lifecycle is independent.
OOP questions reveal how you think about code architecture, expect these in every Java interview.
| Feature | Abstract Class | Interface |
|---|---|---|
| Methods | Can have abstract AND concrete methods | Abstract by default; Java 8+ allows default and static methods; Java 9+ allows private methods |
| Constructors | Yes, called via super() from subclass | No constructors |
| Multiple inheritance | No, a class can extend only one abstract class | Yes, a class can implement multiple interfaces |
| State (fields) | Can have instance variables | Only public static final constants |
| Access modifiers | Any modifier on members | Members are public by default |
| When to use | Shared base implementation + partial contracts | Pure contracts, cross-type capability grants |
The practical rule of thumb is: use an abstract class when classes share state or behavior (IS-A relationship), and use an interface when you want to define a capability that unrelated classes can share (CAN-DO relationship). Note that Java 8 and 9 have blurred this distinction somewhat by adding default, static, and private methods to interfaces; a critical point interviewers specifically test to see if your knowledge is up to date.
// Abstract class with shared state and behaviour public abstract class Animal { protected String name; public void breathe() { System.out.println("Breathing"); } // concrete method public abstract void speak(); // must be implemented } // Interface - pure contract, Java 8+ allows defaults public interface Flyable { void fly(); // abstract by default default void land() { System.out.println("Landing"); } // default method }
Yes, abstract classes can have constructors, even though you cannot instantiate an abstract class directly. The constructor exists to be called by subclass constructors via super(), allowing the abstract class to initialize its own fields.
public abstract class Shape { protected String color; public Shape(String color) { // constructor in abstract class this.color = color; } public abstract double area(); } public class Circle extends Shape { private double radius; public Circle(String color, double radius) { super(color); // calls abstract class constructor this.radius = radius; } public double area() { return Math.PI * radius * radius; } }
| Prefer Abstract Class When… | Prefer Interface When… |
|---|---|
| Multiple related classes share state (instance variables) | You want to define a capability that unrelated classes can share |
| You have common method implementations to share (reduce code duplication) | Multiple inheritance of type is needed (implement multiple interfaces) |
| You need non-public methods (protected, package-private) | You want to define a pure contract with no shared state |
| The relationship is IS-A (Dog is an Animal) | The relationship is CAN-DO (Bird can Fly, Plane can Fly) |
The IS-A vs CAN-DO distinction is the most reliable heuristic for this decision. An abstract class models a family of related things that share identity and state. Animal is the natural parent of Dog and Cat. An interface models a capability that can cut across unrelated hierarchies, both Bird and Airplane can fly, but they share no common ancestry. When in doubt, prefer interfaces because they allow more flexible code and easier testing.
Yes, an abstract class can contain a main() method and can be run directly from the command line, even though you cannot create an instance of it. The JVM calls main() as a static entry point, which does not require instantiation.
public abstract class AbstractMain { public abstract void doSomething(); public static void main(String[] args) { // Cannot do: new AbstractMain() - compile error // But can run this class with: java AbstractMain System.out.println("Running from an abstract class"); } }
| Feature | Method Overloading | Method Overriding |
|---|---|---|
| When resolved | Compile time (static binding) | Runtime (dynamic binding) |
| Parameters | Must differ (number or type) | Must be identical |
| Return type | Can differ | Must be same or covariant (subtype) |
| Access modifier | Can change freely | Cannot reduce visibility |
| Polymorphism type | Compile-time / static polymorphism | Runtime / dynamic polymorphism |
| Where | Same class | Subclass overrides parent method |
The compile-time vs runtime resolution is the most important distinction here. Overloading is resolved by the compiler based on the method signature. The correct method is selected before the program runs. Overriding is resolved at runtime based on the actual type of the object, not the reference type. This is the mechanism that makes polymorphism work. A subclass object assigned to a parent reference will still call the subclass’s overridden method at runtime.
// OVERLOADING: same name, different parameters (compile-time) 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; } } // OVERRIDING: subclass redefines parent method (runtime) class Animal { public void speak() { System.out.println("..."); } } class Dog extends Animal { @Override public void speak() { System.out.println("Woof"); } }
Collections are tested in nearly every Java interview. Knowing the hierarchy, performance trade-offs, and when to use which implementation can be very helpful in the interview.
The Java Collections Framework is a unified architecture of interfaces and classes for storing, manipulating, and retrieving groups of objects. The core hierarchy:
Map is NOT a sub-interface of Collection, it maps keys to values and has its own hierarchy. The framework is in java.util and provides algorithm utilities (Collections.sort(), Collections.binarySearch(), etc.) that work across implementations.
| Feature | Array | Collection |
|---|---|---|
| Size | Fixed at creation, cannot resize | Dynamic, grows and shrinks automatically |
| Type safety | Compile-time type check | Generics provide compile-time safety |
| Primitives | Can store primitives directly | Stores only objects (autoboxing for primitives) |
| Utility methods | None, manual loops required | Rich API: sort, search, filter, iterate |
| Null handling | Can contain null values | Depends on implementation (HashMap allows one null key) |
| Feature | ArrayList | LinkedList |
|---|---|---|
| Internal structure | Dynamic array (contiguous memory) | Doubly-linked list (nodes with prev/next pointers) |
| Random access (get) | O(1), direct index access | O(n), must traverse from head or tail |
| Insertion/deletion (middle) | O(n), shifts subsequent elements | O(1) once the node is found (O(n) to find it) |
| Memory overhead | Low, just the element + array slot | High, each node stores an element + two pointers |
| Best use case | Read-heavy access by index | Frequent insertions/deletions at the head or tail |
// ArrayList - optimized for fast random access (reads) List<String> arrayList = new ArrayList<>(); // LinkedList - optimized for fast insert/delete at head/tail List<String> linkedList = new LinkedList<>(); // LinkedList also implements Deque - can act as queue or stack Deque<String> deque = new LinkedList<>();
| Feature | HashMap | TreeMap |
|---|---|---|
| Ordering | No guaranteed order | Sorted by natural key order or custom Comparator |
| Null keys | One null key allowed | No null keys (throws NullPointerException) |
| Performance | O(1) average for get/put | O(log n) for get/put (Red-Black tree) |
| Implementation | Hash table (bucket array + linked list / tree) | Red-Black balanced BST |
| Best use case | Fast key-value lookups with no ordering needs | Sorted map, range queries, navigable operations |
Fail-fast iterators throw ConcurrentModificationException immediately if the underlying collection is modified while being iterated (other than through the iterator itself). Most standard Java iterators (ArrayList, HashMap) are fail-fast.
Fail-safe iterators work on a copy of the collection, so modifications to the original do not affect the iteration. No exception is thrown, but the iterator may not reflect the latest changes. CopyOnWriteArrayList and ConcurrentHashMap use fail-safe iterators.
// FAIL-FAST: throws ConcurrentModificationException List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c")); for (String s : list) { list.remove(s); // modifying during iteration - EXCEPTION } // FAIL-SAFE: iterates over a snapshot copy, no exception List<String> cowList = new CopyOnWriteArrayList<>(Arrays.asList("a", "b", "c")); for (String s : cowList) { cowList.remove(s); // safe - iterating over snapshot }
Generics enable type-safe data structures and methods that work with any type while providing compile-time type checking. Without generics, collections stored Object references, requiring explicit casts and risking ClassCastException at runtime.
// Generic class public class Box<T> { private T value; public Box(T value) { this.value = value; } public T get() { return value; } } // Usage Box<String> strBox = new Box<>("Hello"); Box<Integer> intBox = new Box<>(42); // Bounded type: T must extend Number public <T extends Number> double sum(List<T> list) { return list .stream() .mapToDouble(Number::doubleValue) .sum(); } // Wildcards // ? extends T → read-only (covariant) // ? super T → write-only (contravariant)
Important: generics use type erasure, generic type information is removed at compile time and replaced with Object (or bounds). At runtime, List<String> and List<Integer> are both just List. This is why you cannot do instanceof List<String> or create generic arrays.
| Feature | HashMap | ConcurrentHashMap |
|---|---|---|
| Thread safety | Not thread-safe, concurrent modifications cause data corruption | Thread-safe, designed for concurrent use |
| Null keys/values | One null key, multiple null values allowed | No null keys or values |
| Locking mechanism | No locking | Segment-based locking (Java 7) / bucket-level CAS (Java 8+) |
| Performance | Fastest for single-threaded use | High concurrent throughput with minimal contention |
| Iteration | Fail-fast (ConcurrentModificationException) | Weakly consistent, no exception |
Multithreading separates mid-level from senior Java developers. These questions test your understanding of concurrency, synchronization, and thread safety. For an extended set of problems and scenarios, see Java multithreading interview questions covering thread lifecycle, executor frameworks, and advanced coordination patterns.
A thread is the smallest unit of execution within a program. Java supports true multi-threading through the java.lang.Thread class and the Runnable and Callable interfaces, allowing concurrent task execution within the same process.
Thread lifecycle:
NEW → RUNNABLE → RUNNING → WAITING/BLOCKED/SLEEPING → TERMINATED
// Method 1: Extend Thread class class MyThread extends Thread { public void run() { System.out.println("Thread running"); } } new MyThread().start(); // Method 2: Implement Runnable (preferred) Thread t = new Thread(() -> System.out.println("Lambda thread running") ); t.start();
Prefer implementing Runnable over extending Thread, as it keeps your class free to extend another class and separates the task logic from thread management.
| Feature | Process | Thread |
|---|---|---|
| Memory | Separate memory space, isolated from other processes | Shares memory space with other threads in the same process |
| Overhead | Heavy, OS-level resource allocation | Lightweight; shares process resources |
| Communication | IPC required (pipes, sockets, shared memory) | Direct; shared heap memory (requires synchronization) |
| Creation cost | High | Low |
| Failure isolation | A crash does not affect other processes | A thread crash can crash the whole process |
Synchronization is the mechanism that ensures only one thread at a time can execute a critical section of code, preventing race conditions when multiple threads share mutable state. Java implements synchronization through monitor locks; every Java object has an implicit monitor.
// Shared counter with synchronization public class Counter { private int count = 0; // Synchronized method: locks entire object public synchronized void increment() { count++; } // Synchronized block: finer control public void decrement() { synchronized (this) { count--; } } }
Synchronized methods lock the entire object. Synchronized blocks lock only the specified object reference. Use them when you need finer control or want to reduce contention by using a dedicated lock object instead of this.
A deadlock is a situation where two or more threads are permanently blocked, each waiting for a resource held by the other. None can proceed, and the application hangs.
Circular wait (the root cause): Thread A holds Lock1, waits for Lock2 ↔ Thread B holds Lock2, waits for Lock1
Prevention strategies:
A thread pool is a collection of pre-created, reusable threads managed by an ExecutorService. Instead of creating a new thread for every task (expensive), tasks are submitted to the pool and executed by available worker threads.
// Import concurrency utilities import java.util.concurrent.*; // Fixed thread pool: always 4 worker threads ExecutorService fixedPool = Executors.newFixedThreadPool(4); // Cached pool: grows as needed, reuses idle threads ExecutorService cachedPool = Executors.newCachedThreadPool(); // Submit tasks fixedPool.submit(() -> System.out.println("Task 1") ); fixedPool.submit(() -> System.out.println("Task 2") ); // Shutdown pool fixedPool.shutdown();
Benefits: reduced thread creation overhead, bounded resource usage, task queuing. Use newFixedThreadPool for CPU-bound tasks (size = CPU cores). Use newCachedThreadPool for short-lived I/O-bound tasks.
volatile is a field modifier that guarantees visibility across threads; any write to a volatile variable is immediately visible to all other threads that subsequently read it. It establishes a happens-before relationship, preventing stale cached values.
// Visibility control using volatile public class StatusMonitor { private volatile boolean running = true; // Stop signal public void stop() { running = false; } // Monitor loop public void monitor() { while (running) { // do work } } }
Without volatile, the JVM may cache running in a CPU register or L1 cache, and a thread calling monitor() may never see the update from stop(). volatile forces every read to go directly to main memory.
⚠️ Pro Tip: volatile guarantees visibility but NOT atomicity. Incrementing a volatile int (i++) is a three-step operation (read → increment → write) and is still unsafe without synchronization. For atomic operations on a single variable, use AtomicInteger or AtomicBoolean instead.
Topics like volatile, locks, AtomicInteger, CountDownLatch, and the java.util.concurrent package are covered in depth in Java concurrency interview questions, the dedicated resource for senior-level concurrency preparation.
Java 8 revolutionized the language with functional programming features; these questions are standard in modern Java interviews. To test your knowledge of these topics and beyond, Advanced Java MCQs with answers provides a practical question bank covering Java 8+ features, generics, collections, and JVM internals.
A functional interface is an interface with exactly one abstract method. It can be used as the target type for a lambda expression or method reference. The @FunctionalInterface annotation is optional but recommended; it causes a compile error if the interface accidentally gains a second abstract method.
// Custom functional interface @FunctionalInterface public interface Transformer<T, R> { R transform(T input); // single abstract method // default methods allowed } // Built-in functional interfaces Predicate<String> isLong = s -> s.length() > 5; Function<String, Integer> len = String::length; Consumer<String> printer = System.out::println; Supplier<String> greeting = () -> "Hello";
A lambda expression is a concise, anonymous function that implements a functional interface. It eliminates the boilerplate of anonymous inner classes for single-method implementations.
// Before Java 8: anonymous inner class Runnable r1 = new Runnable() { @Override public void run() { System.out.println("Running"); } }; // Java 8+: lambda expression Runnable r2 = () -> System.out.println("Running"); // Lambda with parameters Comparator<String> byLength = (a, b) -> a.length() - b.length(); // Lambda in a stream pipeline List<String> names = List.of("Alice", "Bob", "Charlie"); names.stream() .filter(n -> n.length() > 3) .forEach(System.out::println);
The Stream API provides a functional, declarative way to process sequences of elements, filtering, transforming, and aggregating in a pipeline. Streams are lazy (intermediate operations are not executed until a terminal operation is called) and can be parallelized with a single method call.
// Input list List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // Stream pipeline int sumOfEvenSquares = numbers.stream() .filter(n -> n % 2 == 0) // keep even numbers .map(n -> n * n) // square each .reduce(0, Integer::sum); // sum all System.out.println(sumOfEvenSquares); // 220 // Parallel stream long count = numbers.parallelStream() .filter(n -> n > 5) .count();
Key characteristics: streams are not data structures (they do not store elements), are lazily evaluated, can only be consumed once, and support sequential and parallel execution modes.
Optional<T> is a container object that may or may not hold a non-null value. It is designed to replace the null return pattern and make the possibility of absence explicit in the API contract, reducing NullPointerException risk.
import java.util.Optional; // Creating Optional objects Optional<String> present = Optional.of("hello"); Optional<String> empty = Optional.empty(); Optional<String> maybeNull = Optional.ofNullable(null); // safe for null // Check and retrieve (not recommended) if (present.isPresent()) System.out.println(present.get()); // Provide a default String value = empty.orElse("default"); // Transform if present Optional<Integer> length = present.map(String::length); // Throw if absent String result = present.orElseThrow(() -> new RuntimeException("Missing value") );
A record is a special kind of class designed as an immutable data carrier. The compiler automatically generates a canonical constructor, accessor methods (not getters, just field name with no ‘get’ prefix), and overrides for equals(), hashCode(), and toString().
// Java 16+: record public record Point(int x, int y) { } // Usage Point p = new Point(3, 4); System.out.println(p); // Point[x=3, y=4] System.out.println(p.x()); // 3
Records are implicitly final (cannot be extended) and their fields are final (immutable). They can implement interfaces but cannot extend other classes (except Record implicitly).
Sealed classes restrict which classes can extend or implement them. The sealed modifier + permits clause defines an exhaustive, closed hierarchy. This enables the compiler to verify exhaustive pattern matching without a default branch.
// Java 17+: sealed class public sealed class Shape permits Circle, Rectangle, Triangle { } // Allowed subclasses public final class Circle extends Shape { double radius; } public final class Rectangle extends Shape { double width, height; } public non-sealed class Triangle extends Shape { double base, height; } // Exhaustive switch (pattern matching) double area = switch (shape) { case Circle c -> Math.PI * c.radius * c.radius; case Rectangle r -> r.width * r.height; case Triangle t -> t.base * t.height * 0.5; };
Sealed classes are a key building block for algebraic data types in Java. They pair with pattern matching (Java 16+) and record patterns (Java 21) for clean, type-safe data processing without instanceof chains.
Exception handling is a guaranteed interview topic; know the hierarchy, checked vs unchecked, and the try-catch-finally nuances. For a deeper set of scenario-based questions on this topic, explore Java exception handling interview questions covering custom exceptions, multi-catch blocks, and exception chaining patterns.
| Feature | Checked Exception | Unchecked Exception |
|---|---|---|
| When detected | Compile time must be handled | Runtime, optional to handle |
| Must handle? | Yes, declare with throws or wrap in try-catch | No; can propagate silently |
| Extends | Exception (but not RuntimeException) | RuntimeException or Error |
| Examples | IOException, SQLException, FileNotFoundException | NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentException |
| Intent | Recoverable conditions (missing file, network down) | Programming bugs (null dereference, bad array index) |
Java’s exception hierarchy is rooted at Throwable:
Throwable
├── Error (OutOfMemoryError, StackOverflowError), JVM errors, generally unrecoverable
└── Exception
├── Checked exceptions (IOException, SQLException …)
└── RuntimeException (NullPointerException, ClassCastException …)
Errors are thrown by the JVM for serious system-level problems; you generally should not catch them. Checked exceptions must be declared or caught. RuntimeException (unchecked) subclasses represent programming errors.
Yes, try can be used without catch as long as it is paired with finally, or used as a try-with-resources statement.
// try-finally: cleanup always runs try { riskyOperation(); } finally { cleanup(); // always executes } // try-with-resources: auto-close try ( BufferedReader br = new BufferedReader( new FileReader("file.txt") ) ) { return br.readLine(); } // br.close() called automatically
| Keyword | Type | Purpose | Example |
|---|---|---|---|
| final | Modifier keyword | Variable: constant. Method: cannot override. Class: cannot extend. | final int MAX = 100; |
| finally | Block in exception handling | Code that always runs after try/catch, regardless of exception | finally { conn.close(); } |
| finalize | Object method (deprecated) | Called by GC before an object is garbage collected to allow cleanup | @Override protected void finalize() { } |
Senior Java interviews often touch on distributed systems, garbage collection, and design patterns. Here are the most common questions.
The CAP theorem states that a distributed system can only guarantee two of the following three properties simultaneously:
Triangle: C, A, P (choose two)
CA systems (e.g., traditional RDBMS): consistent and available, but fail under network partitions.
CP systems (e.g., HBase, ZooKeeper): consistent under partitions, but may deny requests to maintain consistency.
AP systems (e.g., Cassandra, DynamoDB): available under partitions, but may return stale data.
In practice, network partitions in distributed systems are unavoidable, so real systems must choose between C and A when a partition occurs.
G1 (Garbage-First) is Java’s default garbage collector since Java 9. Unlike older generational collectors (Young/Old/Permanent generation), G1 divides the heap into equal-sized regions and independently collects whichever regions contain the most garbage first, hence the name.
For latency-critical applications, ZGC (Java 15+, stable in Java 17) provides sub-millisecond pauses and is increasingly used at scale.
Dependency Injection (DI) is a design pattern where an object’s dependencies are provided from outside (injected) rather than created internally. It is the primary mechanism behind Inversion of Control (IoC) containers like Spring.
// Without DI: tightly coupled public class OrderService { private PaymentService payment = new PaymentService(); // hardcoded dependency } // With DI: loosely coupled public class OrderService { private final PaymentService payment; @Autowired public OrderService(PaymentService payment) { this.payment = payment; } }
Benefits: testability (mock the dependency in unit tests), flexibility (swap implementations without changing the class), and cleaner architecture. Spring supports constructor injection (preferred), setter injection, and field injection.
Dependency injection is the foundation of the Spring framework, and Spring is the most commonly tested enterprise Java topic at the senior level. To prepare for Spring-specific questions on beans, application context, AOP, and Spring Boot, explore Spring interview questions that cover the full Spring ecosystem interviewers probe.
A RESTful API is a web service that follows REST (Representational State Transfer) architectural constraints, using HTTP as the communication protocol.
| HTTP Method | Operation | Example URL | Idempotent? |
|---|---|---|---|
| GET | Read a resource | GET /users/42 | Yes |
| POST | Create a resource | POST /users | No |
| PUT | Replace a resource | PUT /users/42 | Yes |
| PATCH | Partially update | PATCH /users/42 | Yes |
| DELETE | Remove a resource | DELETE /users/42 | Yes |
Singleton ensures a class has exactly one instance throughout the application and provides a global access point to it. There are several implementation approaches; the enum-based approach is the recommended one (Joshua Bloch, Effective Java).
// Approach 1: Enum Singleton (recommended) public enum DatabaseConnection { INSTANCE; public void connect() { // ... } } DatabaseConnection.INSTANCE.connect(); // Approach 2: Double-Checked Locking public class Config { private static volatile Config instance; private Config() {} public static Config getInstance() { if (instance == null) { synchronized (Config.class) { if (instance == null) { instance = new Config(); } } } return instance; } }
The volatile keyword is essential in double-checked locking; without it, the JVM may partially publish the object (another thread sees a non-null reference before the constructor completes). The enum approach avoids this entirely.
A structured preparation roadmap, from core fundamentals to advanced topics, is the most efficient path to interview readiness.
Cracking top software engineering interviews takes more than knowing how to code. The Software Engineering Interview Prep program by Interview Kickstart is designed to help you master both technical skills and how to present them effectively in interviews. With in-depth training, individualized support, and 1:1 coaching, you’ll learn what top companies actually look for and how to stand out.
You’ll train with FAANG+ instructors who bring real hiring experience, practice through mock interviews in realistic environments, and get structured feedback to improve quickly. The program also supports your overall career growth with resume building, personal branding, and interview strategy. If you’re serious about landing a top software engineering role, this is where preparation turns into results.
This guide has covered the full breadth of Java interview questions, from JVM fundamentals and memory management, through OOP and Collections, to advanced topics including multithreading, Java 8+ features, exception handling, and design patterns. Whether you are preparing as a fresher or reviewing for a senior role, the 60+ questions and answers above reflect what Java interviewers consistently test across companies of all sizes.
Java’s continued dominance in enterprise development is well-documented. According to the Oracle Java developer survey and multiple independent developer reports, Java remains the primary language for large-scale backend systems at companies like Google, Amazon, and Netflix, and shows no signs of slowing in adoption.
For hands-on preparation, Interview Kickstart’s Java curriculum is built and taught by engineers who have been hired at FAANG companies, so your preparation is aligned with what interviewers actually look for, not just what textbooks describe.
Ready to level up your Java interview readiness? Explore and practice deeper on the most tested Java sub-topics.
The most consistently tested topics are JDK vs JRE vs JVM, == vs equals(), String immutability, ArrayList vs LinkedList, HashMap internals, multithreading basics, and exception handling. For senior roles, concurrency utilities, JVM garbage collection, and design patterns are added to the mix.
Start with core Java syntax and OOP fundamentals (classes, inheritance, interfaces, polymorphism), then learn the Collections framework and exception handling. Practice writing code daily, LeetCode and HackerRank have Java-specific tracks. Follow the preparation roadmap in the section above for a structured sequence.
Mid-to-senior interviews focus on concurrency (locks, volatile, ExecutorService), design patterns, JVM internals (classloading, GC algorithms, heap tuning), garbage collection strategies (G1, ZGC), microservices architecture with Spring Boot, and system design questions that require Java-specific implementation decisions.
Yes, Java remains one of the top three most-used languages for enterprise development and is the primary backend language at companies like Google, Amazon, and Netflix. The JetBrains Developer Ecosystem Survey consistently places Java in the top three most widely used languages globally, and Spring Boot adoption continues to grow in microservices architectures.
Core Java covers the language fundamentals: OOP, collections, exceptions, I/O, generics, multithreading, and Java 8+ features. Advanced Java covers enterprise features: JDBC (database connectivity), Servlets and JSP (web layer), Spring and Spring Boot (application framework), REST APIs, and microservices architecture.
Recommended Reads:
Time Zone:
Master ML interviews with DSA, ML System Design, Supervised/Unsupervised Learning, DL, and FAANG-level interview prep.
Get strategies to ace TPM interviews with training in program planning, execution, reporting, and behavioral frameworks.
Course covering SQL, ETL pipelines, data modeling, scalable systems, and FAANG interview prep to land top DE roles.
Course covering Embedded C, microcontrollers, system design, and debugging to crack FAANG-level Embedded SWE interviews.
Nail FAANG+ Engineering Management interviews with focused training for leadership, Scalable System Design, and coding.
End-to-end prep program to master FAANG-level SQL, statistics, ML, A/B testing, DL, and FAANG-level DS interviews.
Learn to build AI agents to automate your repetitive workflows
Upskill yourself with AI and Machine learning skills
Prepare for the toughest interviews with FAANG+ mentorship
Time Zone:
Join 25,000+ tech professionals who’ve accelerated their careers with cutting-edge AI skills
25,000+ Professionals Trained
₹23 LPA Average Hike 60% Average Hike
600+ MAANG+ Instructors
Webinar Slot Blocked
Register for our webinar
Learn about hiring processes, interview strategies. Find the best course for you.
ⓘ Used to send reminder for webinar
Time Zone: Asia/Kolkata
Time Zone: Asia/Kolkata
Hands-on AI/ML learning + interview prep to help you win
Explore your personalized path to AI/ML/Gen AI success
The 11 Neural “Power Patterns” For Solving Any FAANG Interview Problem 12.5X Faster Than 99.8% OF Applicants
The 2 “Magic Questions” That Reveal Whether You’re Good Enough To Receive A Lucrative Big Tech Offer
The “Instant Income Multiplier” That 2-3X’s Your Current Tech Salary
Join 25,000+ tech professionals who’ve accelerated their careers with cutting-edge AI skills
Join 25,000+ tech professionals who’ve accelerated their careers with cutting-edge AI skills
Webinar Slot Blocked
Time Zone: Asia/Kolkata
Hands-on AI/ML learning + interview prep to help you win
Time Zone: Asia/Kolkata
Hands-on AI/ML learning + interview prep to help you win
Explore your personalized path to AI/ML/Gen AI success
See you there!