Experienced Java Interview Questions
How does HashMap work internally in Java?
A HashMap is an array of buckets. When you call put(key, value):
- Java calls
key.hashCode()and spreads the bits of the hash. - The bucket index is found from the hash and the array length (the default size is 16).
- If the bucket is empty, the entry is stored there. If not, Java walks the bucket and uses
equals()to find the same key. If it finds one, the value is replaced, if not the new entry is added. - When the number of entries goes above capacity x load factor (0.75), the array is doubled and the entries are moved (rehashing).
- When one bucket gets long (8 or more entries, and the table has 64 or more buckets), the linked list becomes a balanced tree, so the search stays fast.
So the keys need a good hashCode() and a matching equals(). Even with a very bad hash code the map still gives right answers, only slow:
import java.util.HashMap;
import java.util.Map;
public class Main {
static class BadKey {
final int id;
BadKey(int id) { this.id = id; }
@Override public int hashCode() { return 1; } // all keys in one bucket
@Override public boolean equals(Object o) { return o instanceof BadKey b && b.id == id; }
}
public static void main(String[] args) {
Map<BadKey, String> map = new HashMap<>();
for (int i = 0; i < 1000; i++) {
map.put(new BadKey(i), "value" + i);
}
System.out.println(map.size());
System.out.println(map.get(new BadKey(777)));
}
}
1000
value777
See Java HashMap.
What is the difference between HashMap, Hashtable and ConcurrentHashMap?
| Point | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| Thread safe | No | Yes (locks the whole table) | Yes (fine grained locks) |
| Null key | One allowed | Not allowed | Not allowed |
| Null values | Allowed | Not allowed | Not allowed |
| Speed | Fastest (one thread) | Slow | Fast with many threads |
| Use | Normal code | Old code only | Shared data between threads |
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class Main {
static void tryNull(String name, Map<String, String> map) {
try {
map.put(null, "x");
System.out.println(name + ": null key allowed");
} catch (NullPointerException e) {
System.out.println(name + ": null key NOT allowed");
}
}
public static void main(String[] args) {
tryNull("HashMap", new HashMap<>());
tryNull("Hashtable", new Hashtable<>());
tryNull("ConcurrentHashMap", new ConcurrentHashMap<>());
}
}
HashMap: null key allowed
Hashtable: null key NOT allowed
ConcurrentHashMap: null key NOT allowed
Why must you override both equals() and hashCode() in Java?
The contract: equal objects must have the same hash code. If you override equals() but not hashCode(), two equal objects get different hash codes, and a HashSet keeps both:
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class Main {
static class Bad {
final String name;
Bad(String name) { this.name = name; }
@Override public boolean equals(Object o) { return o instanceof Bad b && b.name.equals(name); }
}
static class Good {
final String name;
Good(String name) { this.name = name; }
@Override public boolean equals(Object o) { return o instanceof Good g && g.name.equals(name); }
@Override public int hashCode() { return Objects.hash(name); }
}
public static void main(String[] args) {
Set<Bad> bad = new HashSet<>();
bad.add(new Bad("Riya"));
bad.add(new Bad("Riya"));
System.out.println("Without hashCode: " + bad.size()); // wrong: 2
Set<Good> good = new HashSet<>();
good.add(new Good("Riya"));
good.add(new Good("Riya"));
System.out.println("With hashCode: " + good.size()); // right: 1
}
}
Without hashCode: 2
With hashCode: 1
The easy way: use a record, which writes both methods for you (Java Records).
How to create an immutable class in Java?
- Declare the class
final(no subclass can break it). - Make all fields
private final. - Do not write setters.
- Set all fields in the constructor.
- For mutable fields (like a list or a date) make a defensive copy when you get it and when you return it.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Main {
static final class Team {
private final String name;
private final List<String> members;
Team(String name, List<String> members) {
this.name = name;
this.members = List.copyOf(members); // copy in
}
String name() { return name; }
List<String> members() { return members; } // List.copyOf is already unmodifiable
}
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("Riya", "Aman"));
Team t = new Team("Java", list);
list.add("Hacker"); // does not change the team
System.out.println(t.members());
try {
t.members().add("Another");
} catch (UnsupportedOperationException e) {
System.out.println("Cannot change the members");
}
}
}
[Riya, Aman]
Cannot change the members
A record is the shortest way, if you only need the data and you copy the mutable parts yourself. See Java Records.
What is the difference between final, finally and finalize in Java?
| Name | Kind | Meaning |
|---|---|---|
| final | Keyword | A constant variable, a method which can not be overridden, a class which can not be extended |
| finally | Block | Runs after try and catch, with or without an exception. For cleaning |
| finalize() | Method | Called by the GC before it removes an object. Deprecated for removal, do not use it |
Use try with resources or a Cleaner instead of finalize().
public class Main {
static int test() {
try {
return 1;
} finally {
System.out.println("finally runs, even after return");
}
}
public static void main(String[] args) {
final int limit = 10;
System.out.println(limit + " " + test());
}
}
finally runs, even after return
10 1
See Java Exceptions.
What is the difference between stack and heap memory in Java?
| Point | Stack | Heap |
|---|---|---|
| Keeps | Method calls, local variables, references | Objects and arrays |
| Per | One stack for each thread | One heap shared by all threads |
| Cleaned | When the method ends | By the garbage collector |
| Size | Small | Big |
| Error when full | StackOverflowError | OutOfMemoryError |
public class Main {
static void recurse(int n) {
recurse(n + 1);
}
public static void main(String[] args) {
int local = 5; // on the stack
String s = new String("object"); // the object is on the heap, the reference s on the stack
try {
recurse(1);
} catch (StackOverflowError e) {
System.out.println("Stack is full: StackOverflowError");
}
System.out.println(local + " " + s);
}
}
Stack is full: StackOverflowError
5 object
Size options: -Xss for the stack and -Xmx for the maximum heap. See Java Recursion.
What is the string pool in Java and what does intern() do?
Equal string literals share one object from the string pool. A string made with new is a separate object on the heap. intern() returns the pooled version.
public class Main {
public static void main(String[] args) {
String a = "java";
String b = "java";
String c = new String("java");
String d = c.intern();
System.out.println(a == b); // same pooled object
System.out.println(a == c); // c is a different object
System.out.println(a == d); // intern gives the pooled one
System.out.println("ja" + "va" == a); // joined at compile time: pooled
}
}
true
false
true
true
Always compare content with equals(). See Java Strings.
What is the difference between Comparable and Comparator in Java?
| Point | Comparable | Comparator |
|---|---|---|
| Package | java.lang | java.util |
| Method | compareTo(T other) | compare(T a, T b) |
| Where | Inside the class | A separate object or lambda |
| Orders | One natural order | Many orders |
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class Main {
record Student(String name, int marks) implements Comparable<Student> {
@Override
public int compareTo(Student other) {
return this.name.compareTo(other.name); // natural order: by name
}
}
public static void main(String[] args) {
List<Student> list = new ArrayList<>(List.of(new Student("Meera", 95), new Student("Aman", 72), new Student("Riya", 88)));
java.util.Collections.sort(list); // uses compareTo
System.out.println(list);
list.sort(Comparator.comparingInt(Student::marks).reversed()); // another order
System.out.println(list);
}
}
[Student[name=Aman, marks=72], Student[name=Meera, marks=95], Student[name=Riya, marks=88]]
[Student[name=Meera, marks=95], Student[name=Riya, marks=88], Student[name=Aman, marks=72]]
What is the difference between ArrayList and LinkedList in Java?
| Point | ArrayList | LinkedList |
|---|---|---|
| Inside | Resizable array | Doubly linked nodes |
| Get by index | Fast, O(1) | Slow, O(n) |
| Add at the end | Fast (usually) | Fast |
| Add or remove at the start | Slow, O(n) | Fast, O(1) |
| Memory | Less | More (each node has two links) |
| Also a Deque | No | Yes |
In practice ArrayList is the better choice for almost all cases, because of its simple memory layout. For a queue or a stack use ArrayDeque. See Java HashSet and LinkedList.
What is the difference between fail-fast and fail-safe iterators?
Fail-fast iterators (ArrayList, HashMap) check a modification counter and throw ConcurrentModificationException when the collection is changed in the loop. Fail-safe (weakly consistent) iterators, like those of CopyOnWriteArrayList and ConcurrentHashMap, do not throw.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class Main {
public static void main(String[] args) {
List<Integer> failFast = new ArrayList<>(List.of(1, 2, 3, 4));
try {
for (Integer n : failFast) {
if (n == 1) failFast.add(99);
}
} catch (java.util.ConcurrentModificationException e) {
System.out.println("ArrayList: ConcurrentModificationException");
}
List<Integer> failSafe = new CopyOnWriteArrayList<>(List.of(1, 2, 3, 4));
for (Integer n : failSafe) {
if (n == 1) failSafe.add(99);
}
System.out.println("CopyOnWriteArrayList: " + failSafe);
}
}
ArrayList: ConcurrentModificationException
CopyOnWriteArrayList: [1, 2, 3, 4, 99]
To remove items while looping, use removeIf() or the remove() of the iterator.
What is type erasure in Java generics?
Generics are checked by the compiler, and then the type arguments are erased. At run time a List<String> is only a List. This keeps old code working, but it has effects:
- You can not write
new T(),T.classorinstanceof List<String>. - You can not make an array of a generic type:
new List<String>[10]. - Primitives are not allowed as type arguments.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
System.out.println(strings.getClass() == numbers.getClass());
System.out.println(strings.getClass().getName());
}
}
true
java.util.ArrayList
See Java Generics.
What is the difference between map() and flatMap() in Java streams?
map is one to one. flatMap is one to many, and it flattens the result.
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> sentences = List.of("java is fun", "streams are great");
List<String[]> mapped = sentences.stream()
.map(s -> s.split(" "))
.toList();
System.out.println(mapped.size() + " arrays"); // a list of arrays
List<String> words = sentences.stream()
.flatMap(s -> Arrays.stream(s.split(" ")))
.toList();
System.out.println(words); // one flat list
}
}
2 arrays
[java, is, fun, streams, are, great]
See Java Streams.
What is a functional interface in Java?
A functional interface has one abstract method (it can have default and static methods too). The annotation @FunctionalInterface asks the compiler to check this rule.
import java.util.function.Function;
import java.util.function.Predicate;
@FunctionalInterface
interface Validator {
boolean validate(String text);
}
public class Main {
public static void main(String[] args) {
Validator notEmpty = t -> !t.isBlank();
Predicate<Integer> positive = n -> n > 0;
Function<String, Integer> length = String::length;
System.out.println(notEmpty.validate("hi") + " " + notEmpty.validate(" "));
System.out.println(positive.test(-5));
System.out.println(length.apply("functional"));
}
}
true false
false
10
What are the ways to create a thread in Java?
import java.util.concurrent.*;
public class Main {
static class MyThread extends Thread {
public void run() { System.out.println("1. extends Thread"); }
}
public static void main(String[] args) throws Exception {
MyThread t1 = new MyThread();
t1.start();
t1.join();
Thread t2 = new Thread(() -> System.out.println("2. Runnable lambda"));
t2.start();
t2.join();
try (ExecutorService pool = Executors.newFixedThreadPool(2)) {
Future<String> f = pool.submit(() -> "3. Callable returns a value");
System.out.println(f.get());
}
Thread t4 = Thread.ofVirtual().start(() -> System.out.println("4. virtual thread"));
t4.join();
}
}
1. extends Thread
2. Runnable lambda
3. Callable returns a value
4. virtual thread
| Way | Returns a value? | Note |
|---|---|---|
| Extend Thread | No | Uses your only parent class |
| Runnable | No | The usual simple way |
| Callable + ExecutorService | Yes (Future) | Best for real programs |
| Virtual thread | Same as above | Light, for many waiting tasks (Java 21+) |
See Java Threads.
What is the difference between synchronized and volatile in Java?
| Point | synchronized | volatile |
|---|---|---|
| Applies to | Methods and blocks | Variables |
| Mutual exclusion | Yes (one thread at a time) | No |
| Visibility of changes | Yes | Yes |
Atomic count++ | Yes, inside a synchronized block | No |
| Cost | Higher (locking) | Low |
Use volatile for a simple flag which one thread writes and others read. Use synchronized, a Lock or AtomicInteger when several steps must be done together.
public class Main {
static volatile boolean running = true;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
long loops = 0;
while (running) {
loops++;
}
System.out.println("Worker stopped");
});
worker.start();
Thread.sleep(200);
running = false; // the worker sees this change because of volatile
worker.join();
}
}
Worker stopped
What is a deadlock in Java and how to avoid it?
Thread A locks lock1 and waits for lock2. Thread B locks lock2 and waits for lock1. Both wait for ever.
public class Main {
static final Object lock1 = new Object();
static final Object lock2 = new Object();
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(() -> {
synchronized (lock1) {
pause();
synchronized (lock2) { System.out.println("A done"); }
}
});
Thread b = new Thread(() -> {
synchronized (lock2) {
pause();
synchronized (lock1) { System.out.println("B done"); }
}
});
a.setDaemon(true);
b.setDaemon(true);
a.start();
b.start();
a.join(1500);
b.join(1500);
System.out.println("A alive: " + a.isAlive() + ", B alive: " + b.isAlive());
}
static void pause() {
try { Thread.sleep(300); } catch (InterruptedException e) { }
}
}
A alive: true, B alive: true
Both threads are still alive after waiting, so they are in a deadlock. How to avoid it:
- Always take the locks in the same order in all threads.
- Use
tryLock(timeout)ofReentrantLockand give up after some time. - Keep the locked code small and do not call unknown code inside it.
- Prefer high level tools (
ExecutorService, concurrent collections).
How to create a thread safe singleton in Java?
A singleton allows only one object of a class. The two simple and safe ways are:
public class Main {
// 1. Holder idiom: lazy and thread safe, because class loading is thread safe
static class Config {
private Config() { System.out.println("Config created"); }
private static class Holder {
static final Config INSTANCE = new Config();
}
static Config getInstance() { return Holder.INSTANCE; }
}
// 2. Enum singleton: also safe against reflection and serialization
enum Registry {
INSTANCE;
int counter;
}
public static void main(String[] args) {
System.out.println("Start");
Config a = Config.getInstance();
Config b = Config.getInstance();
System.out.println(a == b);
Registry.INSTANCE.counter++;
System.out.println(Registry.INSTANCE.counter);
}
}
Start
Config created
true
1
The object is created when it is used the first time, and only once. Other common ways are double checked locking with a volatile field, but the two above are simpler.
What is new in Java 25?
Java 25 (September 2025) is an LTS release, so it gets updates for many years. Some of the final features:
- Compact source files and instance main methods (JEP 512): small programs without the class and
public static, plusIO.println. - Module import declarations (JEP 511):
import module java.base;imports all the public packages of a module. - Flexible constructor bodies (JEP 513): statements before
super(...), for checks. - Scoped values (JEP 506): a safer way to share data between threads than thread locals.
- Compact object headers (JEP 519) and other performance work.
We can test the first three on a real Java 25:
import module java.base;
void main() {
List<String> names = new ArrayList<>(List.of("Riya", "Aman")); // List and ArrayList without a normal import
IO.println(names);
IO.println(new Child(-5).value());
}
class Parent {
final int value;
Parent(int value) { this.value = value; }
int value() { return value; }
}
class Child extends Parent {
Child(int raw) {
int fixed = Math.max(raw, 0); // a statement before super(...), allowed in Java 25
super(fixed);
}
}
[Riya, Aman]
0
Java 27 is newer, but it is not an LTS. See the chapter Java Introduction for the versions.
What are annotations in Java?
An annotation adds information (metadata) to classes, methods and fields. It does not change the code by itself. The compiler or a framework reads it. Well known ones: @Override, @Deprecated, @FunctionalInterface, @SuppressWarnings. Frameworks like Spring use many.
You can make your own, and read it with reflection:
import java.lang.annotation.*;
import java.lang.reflect.Method;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Run {
String value() default "no name";
}
public class Main {
@Run("first")
public void alpha() { System.out.println("alpha called"); }
public void beta() { }
@Run
public void gamma() { System.out.println("gamma called"); }
public static void main(String[] args) throws Exception {
Main obj = new Main();
for (String name : new String[]{"alpha", "beta", "gamma"}) {
Method m = Main.class.getMethod(name);
Run run = m.getAnnotation(Run.class);
if (run != null) {
System.out.println("Running " + run.value());
m.invoke(obj);
} else {
System.out.println(name + " has no @Run");
}
}
}
}
Running first
alpha called
beta has no @Run
Running no name
gamma called
What are the best practices for exception handling in Java?
- Catch specific exceptions, not just
ExceptionorThrowable. - Never swallow an exception with an empty
catch. At least log it. - Use try with resources to close files, sockets and connections.
- Keep the cause when you wrap an exception:
throw new MyException("message", e). - Add useful details to the message: which value, which file.
- Do not use exceptions for normal flow, like ending a loop. They are slow and unclear.
- Throw early, catch late: check arguments at the start, and handle the error where you can do something about it.
- Use unchecked exceptions for programming errors, and checked ones only when the caller can recover.
public class Main {
static class ConfigException extends RuntimeException {
ConfigException(String message, Throwable cause) { super(message, cause); }
}
static int readPort(String text) {
try {
return Integer.parseInt(text);
} catch (NumberFormatException e) {
throw new ConfigException("Invalid port value '" + text + "'", e); // keeps the cause
}
}
public static void main(String[] args) {
try {
readPort("80x");
} catch (ConfigException e) {
System.out.println(e.getMessage());
System.out.println("Cause: " + e.getCause());
}
}
}
Invalid port value '80x'
Cause: java.lang.NumberFormatException: For input string: "80x"
See Java Exceptions.