Java Interview Questions and Answers — Part 2

Java interview questions for 2026 (part 2): collections, GC, serialization, threading, records, virtual threads—grouped questions with answers.

Published

Updated

Read time 37 min read

Reviewed byDeepak Prasad

Java Interview Questions and Answers — Part 2

Experienced Java interviews move past syntax into runtime behavior: how objects survive serialization, what the GC can reclaim, why String immutability matters in concurrent code, and when to pick HashMap over TreeSet. Interviewers often trace a small code snippet—literal pooling, iterator fail-fast, or a transient field—and ask you to explain what happens on the heap.

This page is part two of a two-part set (63 questions here). Start with part one for JVM basics, OOP, static members, and polymorphism; this page continues with serialization, reflection, collections, threading, and Java 8+ features. For dedicated OOP, SOLID, and design-pattern prep, see OOP interview questions. For operating system fundamentals (processes, threads, virtual memory), see operating system interview questions. For Spring Boot framework depth, see Spring Boot interview questions for experienced developers. For Apache Kafka and event streaming, see Kafka interview questions. For full-stack integration questions, see full stack developer interviews.

NOTE
Prep tip: Open each answer after you try the question yourself. Narrate your reasoning aloud—interviewers care as much about how you think as about the final keyword.

Interview context and how to prepare

What does Java interview part two cover?

Part two is where most experienced loops spend their time—after part one confirms you understand the JVM and OOP foundation.

Topics on this page:

Area What interviewers probe
Serialization Serializable, transient, Externalizable, version UID
Reflection Runtime introspection, frameworks, access modifiers
Memory & GC Reachability, reference types, Runtime, finalize
Strings Immutability, pool, intern, StringBuilder vs StringBuffer
Exceptions Checked vs unchecked, throw vs throws
Collections List/Set/Map trade-offs, thread-safe classes, conversions
Modern Java Lambdas, streams, Optional, records, virtual threads

How to use both parts:

  • Rusty on JVM/OOP? Read part one first, then return here.
  • Shipping production Java? Skim part one for gaps; drill part two daily.

Imports, locale, and static utilities

What is a static import in Java?

A static import lets you use static members of a class without qualifying them with the class name—similar to how a normal import lets you use a class without the package prefix.

Import type What it imports Example usage
Normal import Class or interface List<String> list = new ArrayList<>();
Static import Static field or method assertEquals(4, add(2, 2)); (after import static org.junit.Assert.assertEquals;)

Common in tests and utilities:

java
import static java.lang.Math.max;
import static java.util.Collections.emptyList;

double bigger = max(3.0, 7.0);
List<String> none = emptyList();

Caution: Overusing static imports hurts readability—reserve them for well-known constants (PI) or test assertions.

A strong answer is:

Static import brings static members into scope without the class prefix. I use it sparingly—mostly in tests or for obvious constants like Math.PI.

What is the difference between import static com.test.Fooclass and import com.test.Fooclass?

These are two different import mechanisms:

Statement Type Effect
import com.test.FooClass; Normal import Brings the type FooClass into scope
import static com.test.FooClass.*; Static import Brings static members of FooClass into scope

Example:

java
import com.test.FooClass;
import static com.test.FooClass.bar;

FooClass.foo();   // instance or static via type name
bar();            // static method without FooClass prefix

You still need the normal import (or FQCN) to reference the class as a type. Static import only affects static fields and methods.

A strong answer is:

Normal import is for the class type; static import is for static members only. I can call bar() directly after static import, but I still import the class if I need to declare variables of that type.

What is Locale in Java?

A Locale represents a specific geographical, political, or cultural region. Java uses it for locale-sensitive formatting and parsing—not for translating your UI strings by itself.

Common uses:

API area Locale effect
Dates & times DateTimeFormatter, SimpleDateFormat patterns
Numbers & currency Decimal separators, currency symbols
Collation Sort order for strings (Collator)
Resource bundles Loading messages_fr.properties vs messages_en.properties

Example:

java
Locale us = Locale.US;
Locale france = Locale.FRANCE;

NumberFormat currency = NumberFormat.getCurrencyInstance(us);
// formats as $1,234.56

Interview note: Locale is about formatting conventions, not automatic translation. i18n still requires message bundles and explicit locale selection (user preference, Accept-Language, etc.).

A strong answer is:

Locale tells Java how to format dates, numbers, and currency for a region. It works with formatters and resource bundles—I do not confuse it with automatic translation.


Serialization and externalization

What is the serialization?

Serialization is the process of converting a Java object into a byte stream that captures the object's class, version, and internal state. The JVM (or another process) can deserialize those bytes back into a live object—often across network boundaries or after storage.

What gets captured (by default):

  • Class metadata and field values
  • Object graph reachable from the root (references followed recursively)
java
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
}

Not magic persistence: Both ends need compatible class definitions and compatible serialVersionUID strategy.

A strong answer is:

Serialization converts an object graph to bytes for transmission or storage. I mention serialVersionUID because version mismatches are a common production deserialization failure.

What is the purpose of serialization?

Serialization solves moving object state between JVMs, processes, or storage layers without hand-rolling a custom binary format.

Use case How serialization helps
Network communication RMI, legacy clustering, session replication
Persistence Write object state to file/DB blob; reload later
Caching Serialize expensive-to-build graphs; deserialize on cache hit
Cross-JVM transfer Same bytecode, different machines (with version caveats)

Modern interview framing:

  • Many greenfield APIs prefer JSON/Protobuf over Java serialization (security and compatibility).
  • Java serialization still appears in legacy enterprise, session replication, and some framework internals.

A strong answer is:

Serialization moves object state as bytes—for caching, replication, or legacy integration. In new systems I often prefer JSON or Protobuf, but I still understand Java serialization for maintaining older codebases.

Why do we mark a data member transient?

The transient keyword marks a field that must not be included in the default serialization snapshot.

Why you would mark a field transient:

Reason Example
Derived / recomputable Cache that can be rebuilt after load
Security Passwords, tokens, secrets
Non-serializable dependency JDBC Connection, thread handles
Not part of persistent state Logger references, runtime flags

During serialization, the JVM skips transient fields. On deserialization they get default values (null, 0, false) unless you restore them in readObject() or a constructor.

A strong answer is:

transient excludes a field from the serialized byte stream—typically for secrets, non-serializable resources, or derived data I can rebuild after load.

How does marking a field as transient makes it possible to serialize an object?

If class ABC implements Serializable but contains a field of type XYZ that does not implement Serializable, default serialization fails with NotSerializableException.

Problem:

java
public class ABC implements Serializable {
    private XYZ helper;  // XYZ is not Serializable → serialization fails
}

Fix: Mark the non-serializable field transient and restore it after deserialization:

java
public class ABC implements Serializable {
    private transient XYZ helper;

    private void readObject(ObjectInputStream in)
            throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        this.helper = new XYZ();  // re-create or look up from context
    }
}

Interview point: transient is a workaround for one non-serializable member—the real state you care about must still be serializable or reconstructed explicitly.

A strong answer is:

When a member type is not Serializable, marking it transient lets the enclosing class serialize. I must rebuild that field in readObject or accept it being null after load.

What is Externalizable interface in Java?

Externalizable extends Serializable but gives the class full control over what bytes are written and read—via writeExternal() and readExternal().

java
public class Order implements Externalizable {
    private String id;
    private double total;

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(id);
        out.writeDouble(total);
    }

    @Override
    public void readExternal(ObjectInput in)
            throws IOException, ClassNotFoundException {
        id = in.readUTF();
        total = in.readDouble();
    }

    public Order() { }  // public no-arg constructor required
}

When teams choose it: Tight control over format, smaller payloads, or avoiding default reflection-based serialization.

A strong answer is:

Externalizable lets the class own the byte format through writeExternal and readExternal. I mention the required public no-arg constructor—that is a common follow-up trap.

What is the difference between Serializable and Externalizable interface?

Both move object state to bytes, but the default mechanism and control level differ.

Aspect Serializable Externalizable
Type Marker interface Interface with methods
Default behavior JVM walks fields reflectively You implement read/write
Customization Optional writeObject / readObject Required writeExternal / readExternal
Algorithm Recursive default serialization Whatever you write
No-arg constructor Not required for Serializable itself Public no-arg required
serialVersionUID Strongly recommended Still relevant for class identity

Rule of thumb:

  • Serializable — convenience, legacy, frameworks that expect it
  • Externalizable — explicit format, performance tuning, minimal payload

A strong answer is:

Serializable is marker-based with optional hooks; Externalizable forces me to implement the byte format and requires a public no-arg constructor. I pick Externalizable only when I need explicit control.


Reflection and runtime introspection

What is Reflection in Java?

Reflection is the ability to inspect and manipulate classes, methods, fields, and constructors at runtime—even when you did not know the types at compile time.

What you can do:

Capability Typical API
Load class by name Class.forName("com.example.User")
Inspect methods/fields getDeclaredMethods(), getDeclaredFields()
Invoke methods Method.invoke(target, args)
Create instances getDeclaredConstructor().newInstance()
Change accessibility setAccessible(true) on private members

Where you see it: Spring DI, JUnit, Jackson/Gson, Hibernate, annotation processors, test utilities.

Trade-off: Reflection is slower, bypasses compile-time checks, and can break encapsulation—use deliberately, not for everyday business logic.

A strong answer is:

Reflection inspects and invokes classes at runtime. Frameworks use it heavily; in application code I avoid it unless I am building infrastructure or test utilities.

What are the uses of Reflection in Java?

Reflection powers frameworks and tools that must work with unknown types at compile time.

Use area Example
Dependency injection Spring creates beans from annotations
Testing JUnit discovers @Test methods
Serialization / mapping JSON/XML libraries set fields from data
IDEs & debuggers Inspect object state at runtime
Plugins / extensibility Load implementations by class name
Annotations Read @Entity, @Path, custom markers

Interview framing: Reflection trades compile-time safety for runtime flexibility. Production app code rarely calls it directly; you benefit from frameworks that do.

A strong answer is:

Reflection lets frameworks wire objects, run tests, and map data without hard-coding every class. I name Spring and JUnit as examples rather than suggesting reflection for everyday business logic.

How can we access private method of a class from outside the class?

Use reflection to obtain a Method, call setAccessible(true), then invoke. This bypasses normal access checks—common in tests and some framework code, not recommended for production business logic.

Foo.java:

java
public class Foo {
    private void message() {
        System.out.println("hello java");
    }
}

FooMethodCall.java:

java
import java.lang.reflect.Method;

public class FooMethodCall {
    public static void main(String[] args) throws Exception {
        Class<?> c = Class.forName("Foo");
        Object o = c.getDeclaredConstructor().newInstance();
        Method m = c.getDeclaredMethod("message");
        m.setAccessible(true);
        m.invoke(o);
    }
}

Important corrections for interviews:

  • Use getDeclaredConstructor().newInstance()Class.newInstance() is deprecated
  • Use getDeclaredMethod("message") — no null parameter array needed for zero-arg methods

Java 9+ module note: Strong encapsulation may block deep reflection on JDK internals unless --add-opens is used.

A strong answer is:

I use getDeclaredMethod, setAccessible(true), and invoke. I also mention getDeclaredConstructor().newInstance() instead of the deprecated Class.newInstance—and that this pattern belongs in tests or frameworks, not normal app code.


Garbage collection and Runtime

What is Garbage Collection in Java?

Garbage collection (GC) is the JVM's automatic process for reclaiming heap memory occupied by objects that are no longer reachable. Developers do not free() objects manually as in C/C++.

High-level model:

  1. Your code creates objects on the heap.
  2. GC identifies unreachable object graphs.
  3. Memory is reclaimed and may be compacted (collector-dependent).

GC is also called automatic memory management—the JVM owns allocation and reclamation policy.

Interview follow-ups: Generational collectors (young/old), STW pauses, tuning for latency vs throughput.

A strong answer is:

GC reclaims heap memory from unreachable objects automatically. I connect it to reachability—not reference counting—and mention that pause behavior matters in latency-sensitive services.

Why Java provides Garbage Collector?

Java hides raw pointers and expects the JVM to manage memory. As your application allocates objects, the heap fills; without reclamation, you would hit OutOfMemoryError.

Factor Why GC exists
No manual free Java references are not pointers you arithmetic on
JVM-owned heap Allocation is fast path; bulk reclaim is centralized
Safety Reduces use-after-free and double-free bugs
Developer focus Business logic over per-object lifetime tracking

When memory pressure rises, the GC runs (or is triggered) to free objects that are no longer reachable—see also process memory on Linux for OS-level visibility.

A strong answer is:

Java has no manual memory free—the JVM allocates on the heap and GC reclaims unreachable objects so developers avoid pointer lifetime bugs common in native code.

What is the purpose of gc() in Java?

System.gc() and Runtime.getRuntime().gc() are hints asking the JVM to run garbage collection. They do not guarantee immediate collection.

Method What it does
System.gc() Static convenience wrapper
Runtime.getRuntime().gc() Same underlying suggestion

Interview points:

  • The JVM may ignore or delay the request.
  • Forcing GC in application code is usually a code smell—trust the collector unless you have measured evidence (rare).
  • Modern low-latency apps tune GC flags and heap size, not scatter gc() calls.

A strong answer is:

gc() is only a suggestion to the JVM. I do not call it in production code—the collector knows better when to run based on heap pressure.

When does an object become eligible for Garbage Collection in Java?

An object is eligible for GC when it is unreachable from GC roots—not merely when you null a local variable (that is one way to drop reachability).

Common eligibility scenarios:

Scenario Eligible?
No live reference from any thread stack or static field Yes
Only weak/soft/phantom references remain (type-dependent) Often yes, timing varies
Circular references isolated from outside world Yes
Object still referenced from a running thread's stack No

GC roots include: thread stacks, static fields, JNI references, synchronized monitor objects, and JVM internal tables.

java
Object o = new Object();
o = null;  // previous object may become eligible (if no other refs)

A strong answer is:

An object is collectible when no GC root path reaches it—isolated circular graphs count too. Nulling a reference is one way to drop reachability, not the definition itself.

Why do we use finalize() method in Java?

finalize() in Object was intended as a last-chance cleanup hook before an object is reclaimed. The JVM may call it once after the object becomes unreachable.

Fact Detail
Invocation Not guaranteed—GC may never run it
Frequency At most once per object
Timing Unpredictable delay
Modern status Deprecated since Java 9; removed/discouraged—use try-with-resources, Cleaner, or explicit close()

Interview answer today: Mention finalize() history, then pivot to AutoCloseable and try-with-resources for files, sockets, and pools.

A strong answer is:

finalize was a pre-GC cleanup hook but is deprecated and unreliable. I use try-with-resources and explicit close methods instead of depending on finalize.

What are the different types of References in Java?

Beyond ordinary strong references, java.lang.ref provides reference types that interact differently with GC—useful for caches and lifecycle management.

Reference type GC behavior Typical use
Strong Not collected while reachable Normal fields and locals
Soft Collected only if memory needed Memory-sensitive caches
Weak Collected at next GC cycle (when only weak refs remain) Canonical maps, listeners
Phantom Enqueued after finalization; object already finalized Pre-mortem cleanup, resource tracking
java
WeakReference<User> weak = new WeakReference<>(user);
User u = weak.get();  // may be null if GC cleared the referent

A strong answer is:

Strong refs keep objects alive; soft refs suit caches; weak refs for canonicalizing keys; phantom refs for post-finalization cleanup. I pick the type based on how aggressively GC should reclaim memory.

What is the purpose of the Runtime class?

Runtime exposes a singleton gateway to the JVM process: memory stats, GC hints, shutdown hooks, and exec of external processes.

Method Purpose
freeMemory() Approximate free heap memory
totalMemory() Current heap size
maxMemory() Max heap JVM may use (-Xmx)
gc() Suggest garbage collection
addShutdownHook(Thread) Run code on JVM exit
exec(String) Spawn OS process (use carefully)
java
Runtime rt = Runtime.getRuntime();
long max = rt.maxMemory();
long free = rt.freeMemory();

A strong answer is:

Runtime wraps JVM process services—memory metrics, shutdown hooks, and optional external process execution. I use maxMemory and freeMemory when discussing heap sizing or OOM investigations.

What are the uses of Runtime class?

Practical uses of Runtime in applications and tooling:

Use API / pattern
Memory monitoring freeMemory(), totalMemory(), maxMemory()
Graceful shutdown addShutdownHook to flush buffers, close pools
Environment introspection availableProcessors() for pool sizing
External programs exec() — legacy; prefer ProcessBuilder
System properties Often via System.getProperty (related JVM config)

For environment variables on Linux, deployment scripts and System.getenv() complement runtime introspection.

Security note: exec() with user input is dangerous—always validate and prefer ProcessBuilder with explicit arguments.

A strong answer is:

I use Runtime for memory stats and shutdown hooks. For spawning processes I prefer ProcessBuilder over exec, and I never pass unvalidated user input to either.


Nested and inner classes

How many types of Nested classes are in Java?

Java supports four kinds of nested types:

Type Static? Typical use
Member inner class No Tied to outer instance; access outer fields
Local inner class No Defined inside a method/block
Anonymous inner class No One-off implementation (often replaced by lambdas)
Static nested class Yes Helper class; no outer instance required
java
public class Outer {
    class MemberInner { }
    static class StaticNested { }

    void demo() {
        class LocalInner { }
        Runnable anon = new Runnable() { public void run() { } };
    }
}

A strong answer is:

Four nested types: member inner, local inner, anonymous inner, and static nested. The static vs non-static split is the first follow-up—inner classes need an outer instance; static nested classes do not.

Why do we use Nested Classes?

Nested classes organize code that is logically part of one outer type but should not pollute the package namespace.

Benefit Explanation
Logical grouping Helper used only by one class lives inside it—one file, clearer ownership
Encapsulation Inner class accesses outer private members; outer hides helper from package
Readability Listener/callback implementations sit next to the code that registers them
Event handlers GUI and adapter patterns (often lambdas today)

Example pattern: HashMap uses nested Node internally—you do not expose Node as a public top-level type.

A strong answer is:

Nested classes keep helpers close to the class that uses them and let inner types access private outer state without exposing implementation details to the whole package.

What is the difference between a Nested class and an Inner class in Java?

Terminology trap: "Nested class" is the umbrella; "inner class" means a non-static nested class.

Term Meaning
Nested class Any class declared inside another class
Inner class Non-static nested class (member, local, or anonymous)
Static nested class Nested but not inner—no implicit outer this
Capability Inner (non-static) Static nested
Access outer instance members Yes, even private No (without outer reference)
Implicit outer this Yes No
Can exist without outer instance No Yes
java
Outer outer = new Outer();
Outer.MemberInner inner = outer.new MemberInner();
Outer.StaticNested nested = new Outer.StaticNested();

A strong answer is:

Every inner class is nested, but not every nested class is inner—static nested classes do not hold an implicit reference to the outer instance. That distinction matters for memory leaks in long-lived inner classes.

Why do we use Static Nested interface in Java?

A static nested interface (interface declared inside a class) restricts visibility to code that can already access the enclosing class—useful for callback contracts tied to one API surface.

java
public class Abc {
    public interface Xyz {
        void callback();
    }

    public static void registerCallback(Xyz xyz) { /* ... */ }
}

// Client code
Abc.registerCallback(new Abc.Xyz() {
    public void callback() { /* ... */ }
});
Point Detail
Access control Interface name is Abc.Xyz—scoped to Abc's public API
Coupling Callback type clearly belongs to Abc
Alternative today Top-level functional interface + lambda if package visibility is enough

Code that cannot reference Abc cannot reference Abc.Xyz either—tighter than a public top-level interface in the same package.

A strong answer is:

A static nested interface ties a callback contract to its enclosing class and limits how it is discovered. I use it when the callback type is part of one API, not a general-purpose interface.


Strings and immutability

What is the meaning of Immutable in the context of String class in Java?

Immutable means the object's state cannot change after creation. String in Java is immutable—you cannot modify characters in place.

Operation What actually happens
String s = "hi"; Reference to constant-pool or heap object
s = s + " there"; New String object; old "hi" unchanged
s.toUpperCase() Returns new String; original unchanged
java
String a = "Test";
String b = a;
a = a + "Data";  // b still "Test"

Why interviewers care: Immutability enables safe sharing, string pooling, and predictable behavior as HashMap keys.

A strong answer is:

Immutable means no in-place mutation—every transforming method returns a new String. Assigning a new value to a variable just points the reference elsewhere.

Why a String object is considered immutable in java?

Java made String immutable for security, performance, and correctness across the platform.

Reason Impact
String pool Literals can be shared safely—no caller can mutate shared "password" text
Security Network addresses, class names, file paths passed as String cannot be altered by holders
Thread safety Immutable objects need no locks when shared
Hash keys hashCode never changes after insert into HashMap/HashSet
Class loading Class names as stable String literals

Literal sharing example:

java
String a = "TestData";
String b = "TestData";  // may reference same pool entry
// If String were mutable, changing a would break b

When you "change" a String, Java creates a new object and retargets your variable—other references keep the old value.

A strong answer is:

String is immutable so literals can be pooled and shared safely, hash codes stay stable, and concurrent code does not need locks. Mutation is modeled as creating a new String.

How many objects does following code create?

Code:

java
String s1 = "HelloWorld";
String s2 = " HelloWorld ";
String s3 = " HelloWorld ";

Answer: two distinct string literals in the constant pool (not one).

Variable Literal value Pool entry
s1 "HelloWorld" Pool entry #1 (no leading/trailing spaces)
s2, s3 " HelloWorld " Pool entry #2 (spaces matter—different from s1)

s2 and s3 reference the same pooled literal " HelloWorld "—only one pool object for that exact sequence.

Count summary:

  • 2 unique string literals in the pool: "HelloWorld" and " HelloWorld "
  • 3 reference variables, all pointing at one of those two pool entries
  • 0 extra heap String objects from new (all are literal references)

Common mistake: Treating "HelloWorld" and " HelloWorld " as the same literal—they differ by whitespace.

A strong answer is:

Two pool entries because the literals differ—s1 is HelloWorld without spaces, s2 and s3 share the spaced literal. Whitespace makes them distinct interned strings.

How many objects does following code create?

Code:

java
String s = new String("HelloWorld");

Classic interview answer: two objects (when "HelloWorld" is not already in the pool).

Object Where
Literal "HelloWorld" String constant pool
new String(...) instance Heap (non-pool copy or distinct object)

If "HelloWorld" already existed in the pool from earlier code, you still get the new heap String from new; the literal is not duplicated in the pool.

Contrast:

java
String a = "HelloWorld";           // pool only (typical)
String b = new String("HelloWorld"); // pool literal + heap object

A strong answer is:

new String("HelloWorld") creates a heap String plus the literal in the constant pool—two objects in the usual trace. I contrast it with String s = "HelloWorld" which typically uses only the pooled literal.

What is String interning?

String interning ensures only one copy of each distinct literal sequence lives in the string constant pool (for a given class loader / JVM rules).

Concept Detail
Automatic String literals are interned at compile/load time
Manual s.intern() returns the pooled canonical copy
Benefit == safe for interned equal content; memory deduplication
Cost intern() on dynamic strings can pollute the pool (permanent until class loader collected)
java
String a = new String("hello").intern();
String b = "hello";
System.out.println(a == b);  // true  both canonical pool refs

Caution in modern code: Prefer equals() for business logic; use intern() only with measured need.

A strong answer is:

Interning deduplicates identical character sequences in the pool. Literals are interned automatically; intern() forces dynamic strings into the pool—I warn against over-interning user input.

What is the basic difference between a String and StringBuffer object?
Aspect String StringBuffer
Mutability Immutable Mutable character sequence
Thread safety Inherently safe (immutable) Synchronized methods
Performance Poor for repeated concatenation in loops Better for incremental builds
Since Java 1.0 Java 1.0 (legacy; prefer StringBuilder single-threaded)
java
StringBuffer sb = new StringBuffer("Hello");
sb.append(" World");  // mutates same object
String s = sb.toString();  // new immutable String

A strong answer is:

String is immutable; StringBuffer is mutable and synchronized. For single-threaded concatenation I use StringBuilder instead of StringBuffer unless I need thread safety.

How will you create an immutable class in Java?

Immutability is more than final on the class—you must prevent mutation paths through fields, getters, and subclasses.

Checklist:

Step Why
Declare class final Block subclass tricks
Make fields private (and final where possible) No direct mutation
No setters No post-construction changes
Deep copy mutable fields in constructor Caller cannot retain mutable reference
Defensive copies in getters Caller cannot mutate internal state
clone() returns copy (if Cloneable) Prevent escape of mutable internals
java
public final class ImmutableUser {
    private final String name;
    private final List<String> tags;

    public ImmutableUser(String name, List<String> tags) {
        this.name = name;
        this.tags = List.copyOf(tags);  // defensive copy
    }

    public List<String> tags() {
        return tags;  // unmodifiable list
    }
}

Modern helpers: List.copyOf, record for simple immutable carriers (Java 16+).

A strong answer is:

Final class, private final fields, no setters, and defensive copies for any mutable components—in constructor and getters. I mention records when the type is a simple data carrier.

What is the use of toString() method in java ?

Every class inherits toString() from Object. It returns a string representation for logging, debugging, and UI display.

Behavior Detail
Default ClassName@hashCode hex — rarely useful
Override Return meaningful field summary
Implicit call System.out.println(obj) calls obj.toString()
Contract Should be readable and stable enough for logs (not necessarily for persistence)
java
public class User {
    private final String email;

    @Override
    public String toString() {
        return "User{email='" + email + "'}";
    }
}

Pair with equals/hashCode when objects are used in collections—consistent identity story.

A strong answer is:

toString gives a human-readable summary for logs and debugging. I override it with key fields instead of relying on the default Object identity string.

Arrange the three classes String, StringBuffer and StringBuilder in the order of efficiency for String processing operations?

For repeated mutation (append in a loop), efficiency from fastest to slowest:

Rank Class Why
1 (fastest) StringBuilder Mutable, not synchronized
2 StringBuffer Mutable, synchronized overhead
3 (slowest) String Immutable — each change creates new objects
java
// Slow — many intermediate String objects
String s = "";
for (int i = 0; i < 1000; i++) s += i;

// Fast — single growing buffer
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) sb.append(i);

Order: StringBuilderStringBufferString

A strong answer is:

StringBuilder first, then StringBuffer, then String for heavy mutation work. String concatenation in loops creates many throwaway objects—I use StringBuilder unless I need synchronization.


Exception handling

What is Exception Handling in Java?

Exception handling is Java's structured model for dealing with runtime errors and exceptional conditions without crashing the whole process—using try, catch, finally, and throw.

Construct Role
try Wrap code that may fail
catch Handle specific exception types
finally Cleanup that runs whether or not an exception occurred
throw Signal an exception from your code
java
try (BufferedReader reader = Files.newBufferedReader(path)) {
    return reader.readLine();
} catch (IOException e) {
    log.error("read failed", e);
    throw new ServiceException("unable to read config", e);
}

Interview angle: Distinguish recoverable errors (retry, fallback) from programming bugs (IllegalArgumentException, NPE) that should fail fast.

A strong answer is:

Exception handling separates normal flow from failure paths with try/catch/finally. I use specific catch blocks and preserve the cause when wrapping exceptions for APIs.

In Java, what are the differences between a Checked and Unchecked?

Java splits exceptions into checked (compile-time enforced) and unchecked (runtime hierarchy).

Aspect Checked Unchecked
Extends Exception (not RuntimeException) RuntimeException
Compile-time Must handle or declare throws No declare/handle requirement
Examples IOException, SQLException NullPointerException, IllegalArgumentException
Meaning Recoverable external failures Often programming bugs or bad state
java
void readFile() throws IOException {  // checked — caller must deal
    Files.readAllBytes(Path.of("data.txt"));
}

void divide(int a, int b) {
    if (b == 0) throw new ArithmeticException();  // unchecked
}

Modern style: Prefer unchecked for non-recoverable API misuse; use checked for true I/O boundaries you expect callers to handle.

A strong answer is:

Checked exceptions must be caught or declared; unchecked extend RuntimeException and usually indicate bugs or bad input. I name IOException as checked and NullPointerException as unchecked.

What is the difference between throw and throws in Java?
Keyword Where Purpose
throw Inside method body Creates and throws an exception instance now
throws Method signature Declares checked exceptions the method may propagate
java
public void save(User user) throws SQLException {
    if (user == null) {
        throw new IllegalArgumentException("user required");
    }
    repository.insert(user);  // may throw SQLException
}

Additional contrasts:

throw throws
Count One instance per statement Multiple types allowed: throws A, B
Checked propagation Throwing checked type still needs throws on signature Declares checked types to callers
Followed by new Exception(...) Exception class names

A strong answer is:

throw actually raises an exception; throws documents checked exceptions on the method signature. IllegalArgumentException via throw does not need throws; SQLException does.


Collections framework overview

What is the difference between Collection and Collections Framework in Java?

Naming is confusing—Collection (singular) is an interface; Collections Framework is the whole library ecosystem.

Term What it is
Collection<E> Root interface for single-unit groups: add, remove, size, iteration
Collections Framework List, Set, Queue, Map (map is separate hierarchy), implementations, and algorithms
java
Collection<String> names = new ArrayList<>();
names.add("Ada");

The framework provides interoperable APIs so algorithms work across ArrayList, HashSet, PriorityQueue, etc.

A strong answer is:

Collection is the root interface for groups of elements; the Collections Framework is the full library of interfaces, implementations, and utilities built around it.

What are the main benefits of Collections Framework in Java?

Before generics and the unified framework, Java had Vector, Hashtable, arrays, and ad-hoc helpers—inconsistent APIs.

Benefit Interview talking point
Reusability Write to List/Set interfaces; swap implementations
Quality Battle-tested implementations used worldwide
Productivity Collections.sort, binarySearch, synchronized wrappers
Maintainability Standard patterns new teammates recognize
Generics Compile-time type safety (List<String>)

A strong answer is:

The framework gives consistent interfaces, proven implementations, and utility algorithms so I program to List or Map abstractions instead of reinventing data structures.

What is the root interface of Collection hierarchy in Java?

The Collections Framework root for single-element containers is Collection<E> in java.util.

Hierarchy nuance:

Interface Role
Iterable<E> iterator() — in java.lang
Collection<E> Extends Iterable; framework root for lists/sets/queues
Map<K,V> Separate hierarchy—not a Collection

Some texts call Iterable the "root" because Collection extends it, but Oracle documents Collection as the Collections Framework memberMap is parallel, not a subtype of Collection.

A strong answer is:

Collection is the framework root for lists, sets, and queues. Map is a separate hierarchy—I do not say Map extends Collection.

What are the main differences between Collection and Collections?

Easy to confuse—one is an interface, the other a utility class.

Collection Collections
Kind Interface Final utility class
Package java.util java.util
Purpose Contract for data structures Static helpers: sort, sync, unmodifiable
Examples add, remove, size sort, synchronizedList, emptyList
java
List<Integer> list = new ArrayList<>();
Collections.sort(list);

A strong answer is:

Collection is the interface; Collections is a helper class with static methods like sort and synchronized wrappers. I never try to instantiate either name blindly.

What are the Thread-safe classes in Java Collections framework?

Legacy synchronized collections and modern java.util.concurrent types serve concurrent code.

Category Classes
Legacy synchronized Vector, Stack, Hashtable, Properties
Concurrent (preferred) ConcurrentHashMap, ConcurrentSkipListMap, CopyOnWriteArrayList, CopyOnWriteArraySet
Blocking queues ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue, etc.

Interview guidance:

  • Do not default to Vector/Hashtable in new code—use ConcurrentHashMap and proper queue types.
  • "Thread-safe" ≠ "fast under all contention"—measure for your access pattern.

A strong answer is:

Vector, Hashtable, and Properties are legacy synchronized; for new concurrent code I prefer ConcurrentHashMap, CopyOnWriteArrayList, and blocking queues from java.util.concurrent.


Lists, sets, and maps in practice

How will you convert a List into an array of integers like- int\[\]?

List<Integer> is not assignable to int[]—you must unbox primitives.

Option 1 — Apache Commons Lang:

java
int[] intArray = ArrayUtils.toPrimitive(
    myList.toArray(new Integer[0])
);

Option 2 — Java 8+ streams:

java
int[] intArray = myList.stream().mapToInt(Integer::intValue).toArray();

Option 3 — Simple loop:

java
int[] intArray = new int[myList.size()];
for (int i = 0; i < myList.size(); i++) {
    intArray[i] = myList.get(i);
}

Trap: list.toArray() alone gives Object[] or Integer[], not int[].

A strong answer is:

I unbox via stream().mapToInt, a loop, or ArrayUtils.toPrimitive after toArray(new Integer[0]). List.toArray() alone does not produce int[].

How will you convert an array of primitive integers int\[\] to a List collection?

Primitives cannot live in generic collections directly—you box to Integer.

Option 1 — Streams (Java 8+):

java
List<Integer> intList = Arrays.stream(intArray).boxed().collect(Collectors.toList());

Option 2 — Apache Commons:

java
List<Integer> intList = Arrays.asList(ArrayUtils.toObject(intArray));

Option 3 — Loop:

java
int[] intArray = {10, 20, 30};
List<Integer> intList = new ArrayList<>();
for (int i : intArray) {
    intList.add(i);
}

Note: Arrays.asList on Integer[] returns a fixed-size list backed by the array—not the same as mutable ArrayList.

A strong answer is:

I box primitives with streams boxed(), ArrayUtils.toObject, or a simple loop into ArrayList. I distinguish Arrays.asList from a mutable ArrayList when follow-ups ask about fixed-size lists.

How will you convert a List to a Set?

Converting ListSet deduplicates (Set contract)—pick implementation based on ordering needs.

Option 1 — HashSet (unordered, O(1) average):

java
Set<MyType> mySet = new HashSet<>(myList);

Uses hashCode() / equals() for uniqueness.

Option 2 — LinkedHashSet (insertion order preserved):

java
Set<MyType> mySet = new LinkedHashSet<>(myList);

Option 3 — TreeSet (sorted, custom order):

java
Set<MyType> mySet = new TreeSet<>(myComparator);
mySet.addAll(myList);

A strong answer is:

new HashSet(list) for fast dedupe; LinkedHashSet if order matters; TreeSet with a Comparator for sorted or custom equality logic.

How will you remove duplicate elements from an ArrayList?

Use a Set to filter duplicates, then rebuild the list—order depends on set type.

Option 1 — HashSet (order not preserved):

java
ArrayList<MyType> myList = /* list with duplicates */;
Set<MyType> mySet = new HashSet<>(myList);
myList.clear();
myList.addAll(mySet);

Option 2 — LinkedHashSet (insertion order preserved):

java
Set<MyType> mySet = new LinkedHashSet<>(myList);
myList.clear();
myList.addAll(mySet);
Approach Order Complexity
HashSet Not guaranteed O(n) average
LinkedHashSet First-seen order kept O(n) average
Stream.distinct() Encounter order O(n) with hash set backing
java
List<MyType> unique = myList.stream().distinct().collect(Collectors.toList());

A strong answer is:

Put elements in a Set to drop duplicates, then addAll back—LinkedHashSet when insertion order matters, HashSet when it does not. Streams distinct() is a concise alternative.

What are the differences between the two data structures: a Vector and an ArrayList?

Both are resizable arrays implementing List, but Vector is legacy synchronized; ArrayList is the default choice.

Aspect Vector ArrayList
Thread safety Synchronized methods Not synchronized
Performance Slower under single-threaded use Faster typical access
Growth Doubles capacity Grows by ~50%
Legacy Pre-generics era survivor Modern standard
Modern alternative Collections.synchronizedList or concurrent structures if needed Use as-is single-threaded
java
List<String> list = new ArrayList<>();  // preferred

A strong answer is:

Vector is synchronized and legacy; ArrayList is faster for normal single-threaded use. I only reach for Vector in maintaining old APIs—not new code.

What are the differences between Collection and Collections in Java?

(See also Q42—interviewers repeat this naming trap.)

Collection<E> Collections
Type Interface Utility class (static methods only)
Role Defines group operations Algorithms and wrappers on collections
Instantiation Via ArrayList, HashSet, etc. Cannot instantiate—Collections.sort(...)
Examples add, iterator, size sort, reverse, unmodifiableList

A strong answer is:

Collection is the interface implemented by List and Set; Collections is a helper class with static algorithms. The names sound alike but roles are completely different.

What are the differences between a List and Set collection in Java?
Aspect List Set
Ordering Ordered sequence (by index) Generally unordered (LinkedHashSet / TreeSet exceptions)
Duplicates Allowed Not allowed (by equals/hashCode or comparator)
Access Positional: get(i), add(i, e) No index API—contains, iteration
Implementations ArrayList, LinkedList HashSet, TreeSet, LinkedHashSet
java
List<String> list = List.of("a", "b", "a");   // [a, b, a]
Set<String> set = Set.of("a", "b", "a");       // compile-time or duplicate rejection

A strong answer is:

List is ordered and allows duplicates with index access; Set enforces uniqueness and usually has no positional index. I pick List for sequences and Set for unique membership.

What are the differences between a HashSet and TreeSet collection in Java?

Both implement Set, but hash table vs red-black tree drives behavior.

Aspect HashSet TreeSet
Ordering No guaranteed order Sorted (natural or Comparator)
null Allows one null (typical) Does not allow null
Performance O(1) average add/contains/remove O(log n)
Backing HashMap internally NavigableMap (tree)
Extra API Basic set first(), last(), ceiling(), pollFirst(), etc.
Equality hashCode + equals compareTo or comparator
java
Set<Integer> fast = new HashSet<>();
Set<Integer> sorted = new TreeSet<>();

A strong answer is:

HashSet for fast uniqueness checks without order; TreeSet when I need sorted iteration or range views. I remember TreeSet rejects null and uses comparison, not just equals.

In Java, how will you decide when to use a List, Set or a Map collection?

Choose by access pattern, not habit.

Need Choose
Ordered sequence, duplicates OK, index access List (ArrayList, LinkedList)
Unique elements only Set (HashSet, LinkedHashSet, TreeSet)
Key → value lookup Map (HashMap, LinkedHashMap, TreeMap)
Preserve insertion order LinkedHashSet / LinkedHashMap
Sorted keys or elements TreeSet / TreeMap
High-concurrency shared map ConcurrentHashMap
java
Map<String, User> byId = new HashMap<>();
List<Order> chronological = new ArrayList<>();
Set<String> uniqueTags = new HashSet<>();

A strong answer is:

List for sequences, Set for uniqueness, Map for key-based lookup. I add LinkedHash for order or Tree for sorting when requirements demand it.


Comparable, Comparator, and iteration

What are the differences between Comparable and Comparator?

Both order objects, but where the logic lives differs.

Aspect Comparable<T> Comparator<T>
Package java.lang java.util
Method compareTo(T other) on self compare(T a, T b)
Implementation Inside the class (implements Comparable) Separate class or lambda
Sort sequences One natural order Multiple orders via different comparators
Typical use Default ordering (Collections.sort(list)) list.sort(comparator), TreeSet(comparator)
java
public class User implements Comparable<User> {
    public int compareTo(User other) {
        return this.name.compareTo(other.name);
    }
}

Comparator<User> byAge = Comparator.comparingInt(User::getAge);
users.sort(byAge);

A strong answer is:

Comparable defines natural order inside the class via compareTo; Comparator is external and supports multiple sort strategies. I use Comparator when the class should not own every possible order.

What are the Java Collection classes that implement List interface?

Core implementations interviewers expect:

Class Notes
ArrayList Default resizable array
LinkedList Doubly linked list; also Deque
Vector Legacy synchronized
Stack Extends Vector (legacy)
CopyOnWriteArrayList Concurrent, snapshot iteration

Abstract bases: AbstractList, AbstractSequentialList

Specialized / JMX (rare in app interviews): AttributeList, RoleList, RoleUnresolvedList

java
List<String> items = new ArrayList<>();

A strong answer is:

ArrayList and LinkedList are the everyday answers; I mention CopyOnWriteArrayList for concurrent read-heavy lists and Vector/Stack only as legacy.

What are the Java Collection classes that implement Set interface?
Class Notes
HashSet Hash table, fastest general use
LinkedHashSet Hash + insertion order
TreeSet Red-black tree, sorted
EnumSet Bit vector over enum constants—very efficient
CopyOnWriteArraySet Concurrent, copy-on-write
ConcurrentSkipListSet Concurrent sorted set

Abstract base: AbstractSet

A strong answer is:

HashSet, LinkedHashSet, and TreeSet cover most cases; EnumSet for enum domains; concurrent variants when threads share the set.

What is the difference between Iterator and Enumeration?

Legacy Enumeration (JDK 1.0) vs modern Iterator (Collections Framework).

Feature Enumeration Iterator
Age Legacy (Vector, Hashtable) Collections Framework
Methods hasMoreElements, nextElement hasNext, next
Remove No remove remove() supported (optional)
Fail-fast No Yes—ConcurrentModificationException
Generics Raw types historically Works with generics
java
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
}

Modern code: Enhanced for-loop uses Iterator under the hood; use ListIterator for bidirectional traversal.

A strong answer is:

Iterator is the modern choice with optional remove and fail-fast behavior; Enumeration is legacy for old Vector/Hashtable APIs. I use Iterator or enhanced for-loops in new code.


Multithreading fundamentals

How Multi-threading works in Java?

Java threads let one JVM process run multiple threads of execution concurrently—sharing heap memory but with private stacks.

Concept Detail
Thread Unit of execution; implements Runnable or extends Thread
Parallelism Multiple threads on multiple cores (CPU-bound work)
Concurrency Overlapping progress (especially I/O-bound)
Shared state Heap objects shared—requires synchronization
Coordination synchronized, Lock, wait/notify, java.util.concurrent
java
Thread t = Thread.ofVirtual().start(() -> fetchFromService());
t.join();

Key APIs: start() (not run() directly for new thread), sleep, interrupt, executors, virtual threads (Java 21+).

Link: Deeper JVM threading builds on part one fundamentals; modern I/O concurrency continues in Q62–63 below.

A strong answer is:

Threads share the heap but have their own stacks. I start work with start() or executors, protect shared mutable state with locks or concurrent collections, and use virtual threads for I/O-heavy workloads in modern Java.


Modern Java (Java 8 and later)

What is a lambda expression in Java?

A lambda is a concise way to implement a functional interface—an interface with exactly one abstract method (SAM).

java
list.sort((a, b) -> Integer.compare(a.length(), b.length()));

Runnable task = () -> System.out.println("running");
Predicate<String> nonEmpty = s -> !s.isBlank();
Piece Meaning
(a, b) -> ... Parameters
-> Separates params from body
Target type Inferred from context (e.g., Comparator)

Common functional interfaces: Runnable, Callable, Comparator, Predicate, Function, Consumer

A strong answer is:

Lambdas implement functional interfaces—one abstract method. I use them with streams, comparators, and callbacks instead of anonymous inner classes for cleaner code.

What is the difference between intermediate and terminal stream operations?

Stream pipelines are lazy until a terminal operation runs.

Kind Examples Behavior
Intermediate filter, map, flatMap, sorted, distinct Return new stream; lazy
Terminal collect, forEach, reduce, count, findFirst Triggers evaluation; produces result
java
List<String> result = names.stream()
    .filter(n -> !n.isBlank())   // intermediate
    .map(String::trim)           // intermediate
    .sorted()                    // intermediate
    .collect(Collectors.toList()); // terminal  runs pipeline

Interview notes:

  • Streams do not mutate the source collection.
  • Parallel streams help CPU-bound bulk in-memory work—not default for I/O or small lists.

A strong answer is:

Intermediate operations chain lazily; a terminal operation like collect or forEach executes the pipeline. I do not assume parallel streams help I/O-bound code.

What is Optional and how should you use it?

Optional<T> models a value that may be absent—primarily as a return type to avoid returning null without documentation.

java
return findUser(id)
    .map(User::getEmail)
    .orElse("unknown@example.com");
Good use Poor use
Method return when absent is valid Fields on entities
Chaining transforms (map, flatMap) Method parameters
Explicit "not found" APIs Collections of Optional
java
Optional<User> found = repository.findById(id);
found.ifPresent(user -> audit.log("loaded " + user.id()));

A strong answer is:

Optional is for return types where a value might be missing—I chain map and orElse instead of null checks. I avoid Optional fields and parameters; they add noise.

What are Java records?

Records (Java 16+, standardized) are compact immutable data carriers—the compiler generates constructor, accessors, equals, hashCode, and toString.

java
public record Point(int x, int y) { }

public record User(String email, Instant createdAt) {
    public User {
        Objects.requireNonNull(email);
    }
}
Fit Not a fit
DTOs, value objects, API responses JPA entities with lazy relations
Immutable tuples Rich domain models with behavior

Related: See sealed classes in part one trends for controlled hierarchies.

A strong answer is:

Records are immutable data classes with generated boilerplate. I use them for DTOs and value types—not as drop-in replacements for entities with lifecycle and lazy loading.

What are virtual threads and when should you use them?

Virtual threads (Java 21, Project Loom) are lightweight threads scheduled by the JVM on platform-thread carriers—ideal for massive I/O concurrency with blocking code style.

java
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<String>> futures = urls.stream()
        .map(url -> executor.submit(() -> fetch(url)))
        .toList();
}
Use virtual threads Prefer platform threads / structured tasks
Many blocking HTTP/DB calls CPU-bound computation
Request-per-thread servers Heavy synchronized blocks (pinning risk)

Pinning caveat: Blocking inside synchronized may pin a carrier—ReentrantLock is often safer in hot paths.

A strong answer is:

Virtual threads give cheap concurrency for I/O-heavy blocking code. I do not use them for CPU-bound work, and I watch for synchronized pinning in tight loops.

What is the difference between CompletableFuture and virtual threads?

They solve different shapes of async problems—often complementary.

CompletableFuture Virtual threads
Style Callback / stage composition Blocking-style sequential code
Strength Combine async steps, handle errors, allOf/anyOf Millions of concurrent blocking tasks
Mental model Explicit async pipeline Thread-per-task without OS thread cost
Typical use Orchestrate multiple async services in one method Servlet-style request handlers, blocking I/O
java
CompletableFuture<User> user = fetchUserAsync(id)
    .thenCompose(u -> fetchOrdersAsync(u.id()))
    .exceptionally(ex -> User.guest());

Combined pattern: Virtual threads for request scope; CompletableFuture when one method fans out to several async dependencies and merges results.

A strong answer is:

CompletableFuture composes async stages and callbacks; virtual threads let me write blocking code at huge concurrency. Many services use virtual threads per request and CompletableFuture inside a method to join parallel calls.

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …