Skip to the questions

Java Developer · Fresher to 3 years

Java interview questions

The 40 Java and OOP questions that come up most in interviews. Each one has a short answer you can say out loud in about 30 seconds, and the full explanation, with Java code, for when you need it.

Read the quick answers

5 Basics · 3 Strings · 4 OOP · 4 Keywords · 2 Interfaces · 2 equals & immutability · 4 Exceptions · 6 Collections · 3 Java 8 · 5 Multithreading · 2 JVM & memoryUpdated 27 Oct 2026

When is your interview?

01Java basics

0/5 got it

Q01Not marked yetFresherAsked very oftenConcept

How do the JDK, JRE and JVM differ, and how does that split make Java platform independent?

Say this first~30 sec to say

The JVM (Java Virtual Machine) runs bytecode: it loads .class files, interprets them and compiles hot code to machine code with its JIT compiler. The JRE is the JVM plus the standard class libraries: everything needed to run Java programs. The JDK is the JRE plus development tools such as javac, jar and javadoc: everything needed to build them. Java is platform independent because javac produces the same bytecode everywhere, and each operating system has its own JVM to run it.

Could you say it?
Full answer2 min read
JDK  = JRE + javac, jar, javadoc, jdb, jshell…      (develop)
JRE  = JVM + standard libraries (java.lang, java.util…)   (run)
JVM  = class loader + bytecode verifier + interpreter + JIT compiler + garbage collector

Write once, run anywhere

Hello.java ──javac──▶ Hello.class (bytecode, the same on every OS)
                             │
         ┌───────────────────┼───────────────────┐
         ▼                   ▼                   ▼
   JVM for Windows     JVM for Linux       JVM for macOS     ← each is platform-specific

The bytecode is portable; the JVM is not. That's the whole trick: Java moves the platform-specific part into the JVM, so your compiled program doesn't change.

Terminal
javac Hello.java        # compile: produces Hello.class
java Hello              # run: the JVM loads and executes the bytecode
java Hello.java         # Java 11+: compile in memory and run a single-file program
JDKJREJVM
ContainsJRE + toolsJVM + librariesThe runtime engine
Needed toWrite and compileRunExecute bytecode
Includes javacYesNoNo

A change worth mentioning: since Java 11, Oracle no longer ships a separate JRE download. You install a JDK, or build a slimmed-down runtime for your app with jlink.

Follow-ups

  • Is Java compiled or interpreted? Both: compiled to bytecode ahead of time, then interpreted and JIT-compiled to machine code at runtime.
  • What does the JIT compiler do? It watches which methods run often ("hot" code) and compiles them to optimised native code, which is why long-running Java programs speed up after warming up.
Q02Not marked yetFresherAsked very oftenConcept

What does each part of public static void main(String[] args) do, and what happens if you remove static?

Say this first~30 sec to say

It's the exact signature the JVM looks for to start a program. public: the JVM, outside your class, must be able to call it. static: it's called on the class, without creating an object first. void: it returns nothing to the JVM (use System.exit(code) for an exit status). main: the name the launcher looks for. String[] args: the command-line arguments. In Java 17, removing static still compiles, but launching fails with "Main method is not static".

Could you say it?
Full answer2 min read
Java
public class Greeter {
    public static void main(String[] args) {
        String name = args.length > 0 ? args[0] : "world";
        System.out.println("Hello, " + name);
    }
}
$ java Greeter.java Asha
Hello, Asha

Without static, in Java 17

$ java Greeter
Error: Main method is not static in class Greeter, please define the main method as:
   public static void main(String[] args)

The class compiles because an instance method named main is perfectly legal; only the launcher rejects it.

Java 25 relaxed the rules. Instance main methods are now standard (JEP 512), so a launcher can run void main() with no static, no public and no args, creating the object itself. Interviews still expect the classic form and the Java 17 behaviour.

PartWhy it's there
publicAccessible to the launcher from outside the class
staticNo object exists yet when the program starts
voidThe exit status is set with System.exit, not a return value
mainThe method name the launcher searches for
String[] argsCommand-line arguments; String... args also works

Follow-ups

  • Can main be overloaded? Yes, but the launcher only calls the String[] version.
  • Can main be final or synchronized? Yes; neither changes how it's launched.
  • What's in args if you pass nothing? An empty array, not null, so args.length is 0.
Q03Not marked yetFresherAsked oftenConcept

Why isn't Java purely object-oriented, and how do wrapper classes and autoboxing connect primitives to objects?

Say this first~25 sec to say

Java has eight primitive types, byte, short, int, long, float, double, char and boolean, which are plain values, not objects: they have no methods and can't be null. That's why Java isn't purely object-oriented. Each primitive has a wrapper class (Integer, Double, Character…) for when an object is required, such as in collections. Autoboxing converts a primitive to its wrapper automatically, and unboxing converts back.

Could you say it?
Full answer2 min read
Java
import java.util.ArrayList;
import java.util.List;

public class Boxing {
    public static void main(String[] args) {
        List<Integer> marks = new ArrayList<>();
        marks.add(85);                      // autoboxing: int 85 -> Integer.valueOf(85)
        int first = marks.get(0);           // unboxing: Integer -> int
        System.out.println(first + 5);      // 90

        Integer missing = null;
        int value = missing;                // unboxing null: NullPointerException
    }
}
PrimitiveSizeWrapper
byte8 bitsByte
short16 bitsShort
int32 bitsInteger
long64 bitsLong
float32 bitsFloat
double64 bitsDouble
char16 bitsCharacter
booleannot precisely definedBoolean

Why primitives exist at all: speed and memory. An int is 4 bytes on the stack or inside an array; an Integer is a separate heap object with a header, roughly four times bigger, and needs a pointer to reach.

Watch out for

  • Unboxing null throws NullPointerException, as above; it often hides in map.get(key) returning null.
  • Comparing wrappers with == compares references, not values (question 5).
  • Boxing in hot loops creates garbage: Long sum = 0L; for (...) sum += i; boxes on every iteration. Use long.

Follow-ups

  • Why can't generics use primitives? Generic type parameters must be reference types, so List<int> isn't allowed; use List<Integer>, or a primitive stream such as IntStream.
  • What does Integer.parseInt("42") return vs Integer.valueOf("42")? parseInt returns an int; valueOf returns an Integer, from the cache when possible.
Q04Not marked yetFresherAsked oftenPredict the output

A method appends "!" to its StringBuilder parameter, then reassigns the parameter to new StringBuilder("Bye"). If the caller passed a builder holding "Hi", what does it print afterwards?

Say this first~25 sec to say

It prints Hi!. Java is always pass-by-value; for objects, the value passed is a copy of the reference. The method's parameter and the caller's variable point to the same StringBuilder, so append("!") changes the object both can see. Reassigning the parameter only points the method's local copy at a new object; the caller's variable still points to the original, which now holds "Hi!".

Could you say it?
Full answer2 min read
Java
public class PassByValue {
    static void change(StringBuilder sb) {
        sb.append("!");                    // changes the shared object
        sb = new StringBuilder("Bye");     // re-points the local copy only
        sb.append("?");                    // affects the new object, which nobody else sees
    }

    public static void main(String[] args) {
        StringBuilder greeting = new StringBuilder("Hi");
        change(greeting);
        System.out.println(greeting);      // Hi!
    }
}
Before the call:      greeting ──▶ [ "Hi" ]
Inside, after append: greeting ──▶ [ "Hi!" ] ◀── sb
After reassignment:   greeting ──▶ [ "Hi!" ]      sb ──▶ [ "Bye?" ]

The same rule with primitives: a method receives a copy of the int itself, so nothing it does can change the caller's variable.

PassedThe method canThe method can't
A primitiveUse the valueChange the caller's variable
An object referenceChange the object's state (if it's mutable)Make the caller's variable point to a different object

Follow-ups

  • So is Java pass-by-reference for objects? No. In true pass-by-reference (C++'s &), reassigning the parameter would change the caller's variable. Java passes references by value.
  • What if the parameter were a String? Strings are immutable, so there's no append that changes the object; s = s + "!" creates a new String and only re-points the local copy.
Q05Not marked yetFresherAsked sometimesPredict the output

What does this print? Integer a = 127, b = 127; Integer c = 128, d = 128; System.out.println(a == b); System.out.println(c == d);

Say this first~25 sec to say

It prints true then false. Autoboxing calls Integer.valueOf(), which returns cached objects for values from -128 to 127, so a and b are the same object and ==, which compares references, is true. 128 is outside the cache, so c and d are two separate objects and == is false. Always compare wrapper values with equals(), or unbox them first.

Could you say it?
Full answer2 min read
Java
public class IntegerCache {
    public static void main(String[] args) {
        Integer a = 127, b = 127;
        Integer c = 128, d = 128;
        System.out.println(a == b);          // true: the same cached object
        System.out.println(c == d);          // false: two different objects
        System.out.println(c.equals(d));     // true: compares the values
        System.out.println(c.intValue() == d.intValue());   // true: compares ints
    }
}

Where the cache comes from: Integer.valueOf(int) keeps a ready-made array of Integer objects for -128 to 127, because small numbers are used constantly. new Integer(127), deprecated since Java 9, always created a new object.

TypeCached range for valueOf
Integer, Short, Long, Byte-128 to 127
Character0 to 127
BooleanTRUE and FALSE
Float, DoubleNo cache

The Integer cache's upper limit can be raised with a JVM option (-XX:AutoBoxCacheMax), which is one more reason never to rely on == for wrappers.

Follow-ups

  • What does a == 127 compare? A wrapper against a primitive: a is unboxed, so it compares values and is true.
  • How would this bug show up in real code? Comparing two IDs with == passes every test with small IDs, then fails in production once IDs exceed 127.

02Strings

0/3 got it

Q06Not marked yetFresherAsked very oftenConcept

Why are Strings immutable in Java, and how does the String pool depend on that? How many objects can new String("java") create?

Say this first~30 sec to say

A String's characters can never change; every "change" returns a new String. That makes the String pool safe: identical literals share one object because nobody can modify it. Immutability also gives security (a checked path can't change afterwards), thread safety without locks, and a cached hash code for fast HashMap keys. new String("java") creates up to two objects: the pooled literal, if it isn't there yet, plus a new heap String.

Could you say it?
Full answer2 min read
Java
public class StringPool {
    public static void main(String[] args) {
        String a = "java";                 // the literal lives in the String pool
        String b = "java";                 // reuses the same pooled object
        String c = new String("java");     // a new object on the heap, outside the pool
        String d = c.intern();             // fetches the pooled copy

        System.out.println(a == b);        // true
        System.out.println(a == c);        // false
        System.out.println(a == d);        // true

        String upper = a.toUpperCase();    // a new String; a is unchanged
        System.out.println(a + " " + upper);   // java JAVA
    }
}

Why immutability matters

BenefitExample
Safe sharing (the pool)Thousands of "OK" literals share one object
SecurityA path passed to a permission check can't be changed before it's used
Thread safetyStrings can be shared between threads with no synchronisation
HashinghashCode() is computed once and cached, so String keys are fast in a HashMap

Counting objects for new String("java")

  • If "java" isn't in the pool yet: two, the pooled literal and the heap copy.
  • If an earlier "java" literal already put it in the pool: one, just the heap copy.

Follow-ups

  • Where is the String pool? In the heap, since Java 7; before that, in PermGen.
  • Is String final? Yes. Otherwise a subclass could add mutable behaviour and break every guarantee above.
Q07Not marked yetFresherAsked very oftenConcept

String vs StringBuilder vs StringBuffer: how do they differ in mutability, thread safety and speed, and when would you use each?

Say this first~30 sec to say

String is immutable: every concatenation creates a new object. StringBuilder is mutable: append and insert change one internal buffer, so building text in a loop is fast; it's not synchronized. StringBuffer is the same as StringBuilder but synchronized, so it's thread-safe and slower. Use String for values that don't change, StringBuilder for building strings, especially in loops, and StringBuffer only when several threads really append to the same buffer.

Could you say it?
Full answer2 min read
Java
public class Builders {
    public static void main(String[] args) {
        String csv = "";
        for (int i = 1; i <= 5; i++) {
            csv += i + ",";                  // a new String every iteration
        }
        System.out.println(csv);             // 1,2,3,4,5,

        StringBuilder sb = new StringBuilder();
        for (int i = 1; i <= 5; i++) {
            sb.append(i).append(',');        // one buffer, grown as needed
        }
        sb.setLength(sb.length() - 1);       // drop the trailing comma
        System.out.println(sb);              // 1,2,3,4,5
        System.out.println(sb.reverse());    // 5,4,3,2,1: mutating methods change sb itself
    }
}
StringStringBuilderStringBuffer
MutableNoYesYes
Thread-safeYes, by immutabilityNoYes (synchronized methods)
Speed of repeated appendsSlow: copies every timeFastestSlower than StringBuilder
SinceJava 1.0Java 5Java 1.0
Use forFixed text, keys, constantsBuilding text in one threadShared buffers across threads (rare)

Why += in a loop is slow: each pass copies all the characters so far into a new String, so building n pieces costs O(n²) character copies. StringBuilder appends into spare capacity, O(n) overall.

Is + always bad? No. In a single expression such as "Hello, " + name + "!", the compiler generates efficient code (since Java 9 through invokedynamic). The problem is concatenation across loop iterations.

Follow-ups

  • Does StringBuilder have equals()? It inherits Object.equals, so it compares references. Convert with toString() first, or use compareTo (Java 11+).
  • Why is StringBuffer rarely used now? Most string building happens within one thread, where its locking is pure overhead.
Q08Not marked yetFresherAsked very oftenPredict the output

What does this print? String a = "java", b = "java"; String c = new String("java"); then a == b, a == c and a.equals(c)

Say this first~25 sec to say

true, false, true. == compares references: whether two variables point to the same object. a and b are the same literal, so they share one pooled object: true. new String always creates a fresh object outside the pool, so a == c is false. equals() compares content, character by character, so a.equals(c) is true. Use equals() to compare strings; == only answers "same object?".

Could you say it?
Full answer2 min read
Java
public class StringEquality {
    public static void main(String[] args) {
        String a = "java", b = "java";
        String c = new String("java");

        System.out.println(a == b);              // true: one pooled object
        System.out.println(a == c);              // false: c is a separate object
        System.out.println(a.equals(c));         // true: same characters

        String d = "ja" + "va";                  // compile-time constant: folded into "java"
        String part = "ja";
        String e = part + "va";                  // built at runtime: a new object
        System.out.println(a == d);              // true
        System.out.println(a == e);              // false
        System.out.println(a == e.intern());     // true
    }
}

Why "ja" + "va" is pooled but part + "va" isn't: the compiler evaluates concatenations of constants at compile time, so d is just the literal "java". When a variable is involved, the String is built at runtime as a new object.

ComparisonChecksUse for
==Same object?Identity checks, enums, null checks
equals()Same characters?Comparing string values
equalsIgnoreCase()Same characters, ignoring caseUser input such as emails
"java".equals(input)Same, and safe when input is nullAvoiding NullPointerException

Follow-ups

  • What does equals() do in Object? It's the same as ==. String overrides it to compare content, which is exactly what you must do in your own classes (question 19).
  • Why put the literal first, "java".equals(s)? Because s.equals("java") throws NullPointerException when s is null. Objects.equals(a, b) is another null-safe option.

03OOP pillars

0/4 got it

Q09Not marked yetFresherAsked very oftenConcept

What are the four pillars of OOP? Give a Java example of each and explain how abstraction differs from encapsulation.

Say this first~30 sec to say

Encapsulation bundles data with the methods that use it and hides the data behind access control: private fields, public methods. Inheritance lets a class reuse and extend another with extends. Polymorphism lets one call behave differently depending on the object's actual type: overriding at runtime, overloading at compile time. Abstraction exposes what an object does and hides how, through abstract classes and interfaces. Abstraction is about design: what to show; encapsulation is about protection: what to hide and guard.

Could you say it?
Full answer3 min read
Java
import java.util.List;

abstract class Payment {                                  // abstraction: what a payment does
    private final double amount;                          // encapsulation: hidden state

    Payment(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("amount must be positive");
        this.amount = amount;                             // validated before it's stored
    }

    double getAmount() { return amount; }                 // controlled read access

    abstract String pay();                                // how: left to subclasses
}

class UpiPayment extends Payment {                        // inheritance
    private final String upiId;
    UpiPayment(double amount, String upiId) { super(amount); this.upiId = upiId; }
    @Override String pay() { return "Paid ₹" + getAmount() + " via UPI to " + upiId; }
}

class CardPayment extends Payment {
    CardPayment(double amount) { super(amount); }
    @Override String pay() { return "Paid ₹" + getAmount() + " by card"; }
}

public class Pillars {
    public static void main(String[] args) {
        List<Payment> payments = List.of(new UpiPayment(499, "asha@okbank"), new CardPayment(1299));
        for (Payment p : payments) {
            System.out.println(p.pay());                  // polymorphism: each type's own pay()
        }
    }
}
Paid ₹499.0 via UPI to asha@okbank
Paid ₹1299.0 by card
PillarIdeaJava tools
EncapsulationHide data; expose behaviourprivate fields, getters and methods that validate
InheritanceReuse and extend a parentextends, super
PolymorphismOne interface, many behavioursOverriding (runtime), overloading (compile time)
AbstractionShow what, hide howAbstract classes, interfaces

Abstraction vs encapsulation, side by side: a car's steering wheel and pedals are abstraction: you use them without knowing about the engine. The locked bonnet is encapsulation: you can't reach in and change the fuel injection directly. One decides the interface; the other protects the internals.

Follow-ups

  • Is inheritance always good? No: deep hierarchies are rigid. "Favour composition over inheritance": hold another object as a field when "has-a" fits better than "is-a".
  • What is the difference between a class and an object? A class is the blueprint; an object is an instance built from it, with its own state.
Q10Not marked yetFresherAsked very oftenConcept

Method overloading vs overriding: what rules apply to each, and which gives compile-time and which runtime polymorphism?

Say this first~30 sec to say

Overloading is several methods with the same name but different parameter lists in one class; the compiler picks one from the argument types, so it's compile-time polymorphism. Overriding is a subclass redefining an inherited method with the same signature; the JVM picks the version from the object's actual type at runtime, so it's runtime polymorphism. An override can't reduce visibility or throw broader checked exceptions, and static, private and final methods can't be overridden.

Could you say it?
Full answer2 min read
Java
class Notifier {
    void send(String to) { System.out.println("email to " + to); }
    void send(String to, String subject) { System.out.println("email to " + to + ": " + subject); }   // overload
    void send(long phone) { System.out.println("SMS to " + phone); }                                 // overload
}

class SlackNotifier extends Notifier {
    @Override
    void send(String to) { System.out.println("Slack message to " + to); }    // override
}

public class Polymorphism {
    public static void main(String[] args) {
        Notifier n = new SlackNotifier();      // declared as Notifier, really a SlackNotifier
        n.send("asha");                        // Slack message to asha: chosen at runtime
        n.send("asha", "Offer letter");        // email to asha: Offer letter: inherited overload
        n.send(9876543210L);                   // SMS to 9876543210: chosen at compile time
    }
}
OverloadingOverriding
WhereSame class (or inherited)Subclass
SignatureSame name, different parametersSame name and parameters
Return typeCan differ (but can't be the only difference)Same, or a subtype (covariant)
AccessAnySame or wider, never narrower
Checked exceptionsAnySame, narrower or none
ResolvedCompile time, by argument typesRuntime, by the object's type
static methodsCan be overloadedHidden, not overridden (question 14)

@Override is worth always writing: if the signature doesn't actually match the parent's, say through a typo, the compiler reports an error instead of quietly creating a new method.

Follow-ups

  • Can you override a private method? No: it isn't inherited. A method with the same signature in the subclass is simply a new method.
  • Does Java support operator overloading? No, apart from the built-in + for String concatenation.
Q11Not marked yetFresherAsked very oftenConcept

Why can a Java class extend only one class? How does Java handle two interfaces that supply the same default method?

Say this first~30 sec to say

To avoid the diamond problem: if a class could extend two parents that both define move() and hold their own state, which version and whose fields would it inherit? Java allows one parent class but many interfaces, which mostly carry contracts, not state. Since Java 8, interfaces can have default methods, so the diamond can reappear; when two interfaces supply the same default, the class must override the method, and can call either version with InterfaceName.super.method().

Could you say it?
Full answer2 min read
Java
interface Walker {
    default String move() { return "walk"; }
}

interface Swimmer {
    default String move() { return "swim"; }
}

class Duck implements Walker, Swimmer {
    @Override
    public String move() {                                   // required: the compiler won't choose
        return Walker.super.move() + " and " + Swimmer.super.move();
    }
}

public class Diamond {
    public static void main(String[] args) {
        System.out.println(new Duck().move());               // walk and swim
    }
}

Without the override, Duck doesn't compile: javac reports that it "inherits unrelated defaults for move() from types Walker and Swimmer".

The resolution rules, in order

  1. A class method wins over an interface default: if Duck extended a class with move(), that one is used.
  2. The more specific interface wins: if Swimmer extended Walker and overrode move(), Swimmer's version is used.
  3. Otherwise it's ambiguous, and the class must override it.

Follow-ups

  • How does Java get the benefits of multiple inheritance, then? Through multiple interfaces for types and contracts, and composition for reusing implementation.
  • Can an interface extend several interfaces? Yes: interface Amphibian extends Walker, Swimmer {}, which then has to resolve the same conflict itself.
Q12Not marked yet1–3 yrsAsked sometimesPredict the output

Overloads print(long), print(Integer) and print(int...) all exist. Which one does print(5) call?

Say this first~25 sec to say

print(long). The compiler resolves overloads in three phases and stops at the first that finds a match. Phase one allows only widening of primitives, so int to long matches. Phase two adds boxing, which would choose print(Integer). Phase three adds varargs, print(int...). Widening wins because it's tried first; remove print(long) and you get print(Integer), and remove that too and you get the varargs version.

Could you say it?
Full answer2 min read
Java
public class Overloads {
    static void print(long x) { System.out.println("long"); }
    static void print(Integer x) { System.out.println("Integer"); }
    static void print(int... x) { System.out.println("varargs"); }

    public static void main(String[] args) {
        print(5);             // long
        print(5L);            // long: an exact match
        print(Integer.valueOf(5));   // Integer: an exact match
        print();              // varargs: nothing else takes zero arguments
        print(1, 2);          // varargs
    }
}
PhaseAllowsPicks here
1Exact matches and widening (int → long, float, double)print(long)
2+ boxing and unboxing (int → Integer)print(Integer)
3+ varargsprint(int...)

Why this order? Backward compatibility: boxing and varargs arrived in Java 5. Code written before them had to keep calling the same methods once they existed.

A related trap: int can't box to Long. With only print(Long) available, print(5) doesn't compile, because widening and boxing can't be combined in one step.

Follow-ups

  • When is a call ambiguous? When two overloads match in the same phase and neither is more specific, for example print(Integer, long) and print(long, Integer) called with (5, 5).
  • Is overload resolution affected by the object's runtime type? No. It uses the declared (compile-time) types of the arguments; only overriding looks at runtime types.

04Classes and keywords

0/4 got it

Q13Not marked yetFresherAsked very oftenConcept

What is a constructor? Can one be private, static or inherited, and how do this() and super() chain constructors?

Say this first~30 sec to say

A constructor initialises a new object: it has the class's name and no return type, and runs when you call new. It can be private (singletons, factories, utility classes), but can't be static, final or abstract, and isn't inherited. Every constructor first calls either this(...), another constructor of the same class, or super(...), a parent constructor; if you write neither, the compiler inserts super(). In Java 17 that call must be the first statement.

Could you say it?
Full answer3 min read
Java
class Account {
    protected final String owner;
    protected double balance;

    Account(String owner) {
        this(owner, 0.0);                            // this(): reuse the other constructor
    }

    Account(String owner, double opening) {
        // super() is inserted here automatically: Object's constructor
        this.owner = owner;                          // this.field vs the parameter of the same name
        this.balance = opening;
        System.out.println("Account for " + owner);
    }
}

class SavingsAccount extends Account {
    private final double rate;

    SavingsAccount(String owner, double rate) {
        super(owner, 1000.0);                         // super(): the parent part is built first
        this.rate = rate;
        System.out.println("Savings at " + rate + "%");
    }
}

public class Constructors {
    public static void main(String[] args) {
        new SavingsAccount("Asha", 7.0);
    }
}
Account for Asha
Savings at 7.0%

The parent's constructor always runs before the child's body, so the parent's fields are ready when the child starts.

The usual follow-up: this vs super

thissuper
Refers toThe current objectThe parent part of the current object
As a callthis(...): another constructor in this classsuper(...): a parent constructor
As a referencethis.field, this.method()super.method(): the parent's version of an overridden method

A common compile error: if the parent has only constructors that take arguments, a child constructor that doesn't call super(...) explicitly fails, because the implicit super() has nothing to call.

Java 25 changed one rule. Flexible constructor bodies (JEP 513) allow statements, such as validating arguments, before this(...) or super(...), as long as they don't use the object being built.

Follow-ups

  • What is a default constructor? The no-argument constructor the compiler adds when a class declares none. Declaring any constructor stops it being added.
  • Why make a constructor private? To control creation: a singleton (question 36), static factory methods such as List.of, or a class of static utilities that should never be instantiated.
Q14Not marked yetFresherAsked very oftenConcept

What does static mean for variables, methods, blocks and nested classes? Can a static method be overridden?

Say this first~30 sec to say

static means belongs to the class, not to an object. A static variable has one copy shared by all instances; a static method is called on the class and has no this; a static block runs once, when the class loads; a static nested class needs no outer instance. Static methods can't be overridden: a subclass's static method with the same signature hides the parent's, so the declared type decides which runs.

Could you say it?
Full answer3 min read
Java
class Employee {
    static int count = 0;                      // one copy, shared by all employees
    static final String COMPANY;               // a constant
    final String name;

    static {                                   // runs once, when the class loads
        COMPANY = "CareersAt Corp";
    }

    Employee(String name) {
        this.name = name;
        count++;
    }

    static String describe() { return "Employee"; }
}

class Manager extends Employee {
    Manager(String name) { super(name); }
    static String describe() { return "Manager"; }   // hides Employee.describe()
}

public class StaticDemo {
    public static void main(String[] args) {
        new Employee("Asha");
        new Manager("Ravi");
        System.out.println(Employee.count);           // 2: shared counter
        System.out.println(Employee.COMPANY);         // CareersAt Corp

        Employee e = new Manager("Priya");
        System.out.println(e.describe());             // Employee: chosen by the declared type
        System.out.println(Manager.describe());       // Manager
    }
}

Calling a static method through a variable, e.describe(), compiles (IDEs and javac -Xlint:static flag it), but the object is ignored and the declared type decides. That's method hiding, not overriding.

static onMeans
A fieldOne value per class, shared by all instances
A methodCalled on the class; no this; can't be overridden, only hidden
A blockRuns once, when the class is initialised
A nested classDoesn't hold a reference to an outer instance
An importimport static java.lang.Math.max; lets you write max(a, b)

Why can't a static method use instance fields? It may be called before, or without, any object existing, so there's no this to read them from.

Follow-ups

  • When should a method be static? When it doesn't depend on instance state: utilities such as Math.max, and factory methods.
  • Static nested class vs inner class? An inner (non-static) class holds a hidden reference to its outer object, which can keep that object alive longer than expected.
Q15Not marked yetFresherAsked very oftenConcept

final vs finally vs finalize(): what does each one do?

Say this first~30 sec to say

Three unrelated things with similar names. final is a modifier that prevents change: a final variable can't be reassigned, a final method can't be overridden, and a final class can't be extended. finally is a block after try that always runs, used for cleanup. finalize() was a method the garbage collector might call before reclaiming an object; it's unreliable, deprecated since Java 9 and marked for removal since Java 18.

Could you say it?
Full answer2 min read
Java
final class TaxRates {                              // final class: no subclasses
    static final double GST = 0.18;                 // final variable: a constant
}

class Report {
    final String title;                             // must be assigned once, in the constructor
    Report(String title) { this.title = title; }

    final String header() { return "== " + title + " =="; }   // final method: can't be overridden
}

public class FinalDemo {
    public static void main(String[] args) {
        final int limit = 3;
        // limit = 4;                               // compile error: cannot assign a value to final variable limit

        try {
            System.out.println(new Report("Sales").header());
        } finally {
            System.out.println("finally always runs");   // cleanup goes here
        }
    }
}
== Sales ==
finally always runs

final on a reference doesn't freeze the object. final List<String> names = new ArrayList<>(); stops names pointing elsewhere, but names.add("Asha") still works. For an unchangeable list, use List.of(...) or an immutable class (question 20).

finalfinallyfinalize()
What it isA modifierA blockA method of Object
PurposePrevent reassignment, overriding or extensionRun cleanup whether or not an exception occurredA last-chance hook before garbage collection
StatusUse freelyUse, or prefer try-with-resources for resourcesDeprecated for removal: don't use

What replaced finalize()? Try-with-resources (question 24) for closing resources deterministically, and java.lang.ref.Cleaner for the rare cases that need a safety net.

Follow-ups

  • Why was finalize() a bad idea? There was no guarantee when, or whether, it ran; it slowed garbage collection; and it could even resurrect objects.
  • Does finally run if try returns? Yes, just before the method actually returns (question 22).
Q16Not marked yetFresherAsked oftenConcept

What are Java's four access levels, and how does protected differ from default (package-private) access?

Say this first~20 sec to say

From most to least restrictive: private, visible only inside the class; default (no keyword, "package-private"), visible to every class in the same package; protected, the same package plus subclasses in other packages; and public, visible everywhere. So protected is default access plus one addition: subclasses elsewhere can use the member through inheritance.

Could you say it?
Full answer2 min read
ModifierSame classSame packageSubclass, other packageEverywhere else
privateYesNoNoNo
(default)YesYesNoNo
protectedYesYesYes, through inheritanceNo
publicYesYesYesYes
Java
// file: com/shop/billing/Invoice.java
package com.shop.billing;

public class Invoice {
    private double discount;           // only Invoice itself
    double subtotal;                   // default: anything in com.shop.billing
    protected double tax;              // com.shop.billing, plus subclasses anywhere
    public double total() { return subtotal + tax - discount; }
}

// file: com/shop/reports/GstInvoice.java
package com.shop.reports;

import com.shop.billing.Invoice;

public class GstInvoice extends Invoice {
    void applyGst() {
        tax = 18.0;                    // OK: protected, inherited by this subclass
        // subtotal = 100;             // compile error: default access, different package
        // discount = 5;               // compile error: private
    }
}

A subtle protected rule: in another package, a subclass can use the protected member on itself (or its own subclasses), not on an arbitrary parent object: inside GstInvoice, new Invoice().tax = 5; doesn't compile.

Choosing a level: start with private and widen only when needed. Fields are almost always private; the public methods form the class's API.

Follow-ups

  • Can a top-level class be private or protected? No: a top-level class is either public or package-private. Nested classes can use all four.
  • What did Java 9 modules add? Another layer: a public class is visible outside its module only if its package is exported in module-info.java.

05Abstract classes and interfaces

0/2 got it

Q17Not marked yetFresherAsked very oftenConcept

Abstract class vs interface: how do they differ as of Java 17, and when would you pick each?

Say this first~30 sec to say

An abstract class is a partly built class: it can have constructors and instance state, plus abstract and concrete methods, but a class extends only one. An interface is a contract: abstract methods plus default and static (Java 8) and private (Java 9) methods, but no instance state and no constructors; a class can implement many. Use an abstract class to share state and code within a family, an interface for a capability unrelated classes share.

Could you say it?
Full answer3 min read
Java
interface Discountable {                          // a capability: anything that can be discounted
    double discountPercent();                     // abstract by default

    default double priceAfterDiscount(double price) {      // Java 8: default method
        return round(price * (1 - discountPercent() / 100));
    }

    static double gst(double price) { return price * 0.18; }   // Java 8: static method

    private static double round(double v) { return Math.round(v * 100) / 100.0; }   // Java 9: private
}

abstract class Product {                          // a family with shared state
    protected final String name;
    protected final double price;

    protected Product(String name, double price) {          // abstract classes have constructors
        this.name = name;
        this.price = price;
    }

    abstract String category();                    // each kind of product must say what it is

    String label() { return name + " (" + category() + "): ₹" + price; }   // shared code
}

class Book extends Product implements Discountable {
    Book(String name, double price) { super(name, price); }
    @Override String category() { return "Books"; }
    @Override public double discountPercent() { return 10; }
}

public class AbstractVsInterface {
    public static void main(String[] args) {
        Book book = new Book("Clean Code", 650);
        System.out.println(book.label());                       // Clean Code (Books): ₹650.0
        System.out.println(book.priceAfterDiscount(book.price)); // 585.0
        System.out.println(Discountable.gst(1000));              // 180.0
    }
}
Abstract classInterface (Java 17)
Instance fields (state)YesNo: only public static final constants
ConstructorsYesNo
Concrete methodsYesdefault, static and private methods
Method access levelsAnyPublic, or private helpers
A class can extend or implementOneMany
Models"is-a" with shared state"can-do" capabilities and contracts
ExamplesAbstractList, HttpServletComparable, Runnable, List

Many prep answers predate Java 8 and say interfaces can't have method bodies. That stopped being true in 2014, so mention default, static and private methods.

Sealed types, since Java 17: sealed interface Payment permits Upi, Card {} restricts which classes may implement it, which pairs well with pattern-matching switch.

Follow-ups

  • Can an abstract class have no abstract methods? Yes. Marking it abstract simply stops anyone instantiating it.
  • Why were default methods added? So interfaces like List could gain new methods, such as forEach and stream, without breaking every existing implementation.
Q18Not marked yetFresherAsked oftenCoding

Write your own functional interface and implement it with a lambda and with a method reference. What makes an interface "functional"?

Say this first~25 sec to say

A functional interface has exactly one abstract method; default and static methods don't count. Because there's only one method to implement, a lambda or a method reference can implement it directly, without writing a class. Annotate it with @FunctionalInterface so the compiler rejects a second abstract method. Java ships many ready-made ones in java.util.function, such as Function, Predicate, Supplier and Consumer.

Could you say it?
Full answer3 min read
Java
@FunctionalInterface
interface PriceRule {
    double apply(double price);                       // the single abstract method

    default PriceRule then(PriceRule next) {          // defaults are allowed
        return price -> next.apply(apply(price));
    }
}

public class FunctionalDemo {
    static double addGst(double price) {
        return price * 1.18;
    }

    public static void main(String[] args) {
        PriceRule festiveSale = price -> price * 0.9;        // a lambda
        PriceRule gst = FunctionalDemo::addGst;              // a method reference

        System.out.println(festiveSale.apply(1000));         // 900.0
        System.out.println(gst.apply(1000));                 // 1180.0
        System.out.println(festiveSale.then(gst).apply(1000));   // 1062.0: sale, then GST
    }
}

Adding a second abstract method to PriceRule would make the @FunctionalInterface annotation fail to compile, and the lambdas with it.

Kinds of method reference

KindExampleLambda equivalent
Static methodFunctionalDemo::addGstp -> FunctionalDemo.addGst(p)
Method of a particular objectSystem.out::printlnx -> System.out.println(x)
Method of an arbitrary object of a typeString::toUpperCases -> s.toUpperCase()
ConstructorArrayList::new() -> new ArrayList<>()

The built-in functional interfaces you'll use most

InterfaceMethodShape
Function<T, R>applyT → R
Predicate<T>testT → boolean
Supplier<T>get() → T
Consumer<T>acceptT → nothing
BiFunction<T, U, R>apply(T, U) → R
UnaryOperator<T>applyT → T

PriceRule is really a UnaryOperator<Double> (or DoubleUnaryOperator); a custom interface earns its place when the name explains the domain.

Follow-ups

  • Can a lambda use local variables from its surrounding method? Only if they're effectively final: never reassigned after they're set.
  • Is Runnable a functional interface? Yes: one abstract method, run(), which is why new Thread(() -> ...) works.

06Object class and immutability

0/2 got it

Q19Not marked yetFresherAsked very oftenCoding

Override equals() and hashCode() in an Employee class so it works as a HashMap key. What goes wrong if you override only equals()?

Say this first~25 sec to say

equals() should compare the fields that define identity, here id, and hashCode() must be computed from the same fields, because the contract says equal objects must have equal hash codes. HashMap uses hashCode() to choose a bucket, then equals() inside it. Override only equals(), and two equal employees usually get different hash codes from Object, land in different buckets, and a lookup with an equal key returns null.

Could you say it?
Full answer3 min read
Java
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

final class Employee {
    private final int id;
    private final String name;

    Employee(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                       // same object
        if (!(o instanceof Employee other)) return false; // null or a different type (Java 16 pattern)
        return id == other.id;                            // identity is the employee id
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);                          // the same field as equals()
    }

    @Override
    public String toString() { return "Employee(" + id + ", " + name + ")"; }
}

public class EqualsHashCode {
    public static void main(String[] args) {
        Map<Employee, String> desks = new HashMap<>();
        desks.put(new Employee(42, "Asha"), "Desk 7B");

        Employee lookup = new Employee(42, "Asha");           // a different object, equal by id
        System.out.println(desks.get(lookup));                // Desk 7B
        System.out.println(lookup.equals(new Employee(42, "A. Rao")));   // true: same id
    }
}

With only equals() overridden, desks.get(lookup) almost always prints null: Object.hashCode() gives the two objects unrelated numbers, so HashMap searches the wrong bucket and never even calls equals().

The contract, in brief

RuleMeaning
Reflexive, symmetric, transitivea.equals(a); a.equals(b) ⇔ b.equals(a); and so on along chains
ConsistentRepeated calls give the same answer while the fields don't change
x.equals(null)Always false, never an exception
Equal objects → equal hash codesRequired
Equal hash codes → equal objectsNot required: collisions are allowed

Records do this for you. record Employee(int id, String name) {} generates equals, hashCode and toString from all components, so two records with the same id but different names are not equal. If identity should be the id alone, write the methods yourself as above.

Follow-ups

  • What if a key's fields change after it goes into a HashMap? Its hash changes, so the map can't find it any more. Keys should be immutable.
  • Why instanceof rather than getClass()? instanceof lets subclasses compare equal to their parent, getClass() doesn't; each choice has trade-offs, so state which you picked and why.
Q20Not marked yetFresherAsked very oftenCoding

Write an immutable class that holds a List. How do you stop callers changing it, and when is a shallow copy not enough?

Say this first~30 sec to say

Make the class final, its fields private final, provide no setters, and set everything in the constructor. For the List, make a defensive copy on the way in, List.copyOf(skills), so the caller's later changes can't leak in, and return an unmodifiable list on the way out. That copy is shallow: it copies references to the elements. If the elements are themselves mutable, such as Address objects, they can still change, so copy them deeply or make them immutable too.

Could you say it?
Full answer3 min read
Java
import java.util.ArrayList;
import java.util.List;

public final class Candidate {                           // final: no mutable subclasses
    private final String name;
    private final List<String> skills;

    public Candidate(String name, List<String> skills) {
        this.name = name;
        this.skills = List.copyOf(skills);               // defensive copy, and unmodifiable
    }

    public String getName() { return name; }
    public List<String> getSkills() { return skills; }   // safe to return: nobody can change it

    public Candidate withSkill(String skill) {           // "changes" return a new object
        List<String> more = new ArrayList<>(skills);
        more.add(skill);
        return new Candidate(name, more);
    }

    public static void main(String[] args) {
        List<String> input = new ArrayList<>(List.of("Java", "SQL"));
        Candidate asha = new Candidate("Asha", input);

        input.add("Hacking");                            // the caller changes its own list...
        System.out.println(asha.getSkills());            // [Java, SQL]: ...without affecting asha

        Candidate updated = asha.withSkill("Spring");
        System.out.println(updated.getSkills());         // [Java, SQL, Spring]
        System.out.println(asha.getSkills());            // [Java, SQL]: the original is unchanged

        asha.getSkills().add("Docker");                  // UnsupportedOperationException
    }
}

The checklist

  1. final class, so a subclass can't add mutable state or override getters.
  2. private final fields, set once in the constructor.
  3. No setters; "modifier" methods return a new object, as withSkill does.
  4. Defensive copies of mutable inputs (List.copyOf, new Date(d.getTime())).
  5. Never hand out a mutable internal object; return copies or unmodifiable views.

Shallow vs deep copy

Java
final class Address {
    String city;                                   // mutable
    Address(String city) { this.city = city; }
}

// List.copyOf(addresses) creates a new list, but its elements are the SAME Address objects:
// address.city = "Delhi" through the caller's reference changes what the "immutable" object holds.

A shallow copy duplicates the container; a deep copy also duplicates the objects inside it. For true immutability, the elements must be immutable (Strings, records of immutable fields) or deep-copied.

Records are a quick way to build immutable data carriers, record Candidate(String name, List<String> skills) {}, but they're only shallowly immutable: still copy the list in a compact constructor, skills = List.copyOf(skills);.

Follow-ups

  • Why does List.copyOf reject null elements? It throws NullPointerException for them: immutable collections made by List.of and List.copyOf don't allow nulls.
  • What's the benefit of immutable objects? They're thread-safe without locks, safe as HashMap keys, and easy to reason about and cache.

07Exception handling

0/4 got it

Q21Not marked yetFresherAsked very oftenConcept

Checked vs unchecked exceptions: how does the compiler treat each, and how is throw different from throws?

Say this first~30 sec to say

Checked exceptions, subclasses of Exception but not of RuntimeException, such as IOException and SQLException, must be caught or declared, or the code doesn't compile: they're recoverable conditions the caller should plan for. Unchecked exceptions, RuntimeException and its subclasses such as NullPointerException and IllegalArgumentException, aren't checked by the compiler: they usually mean bugs. throw actually raises an exception; throws, in a method signature, declares which checked exceptions it may raise.

Could you say it?
Full answer3 min read
Throwable
├── Error                         serious JVM problems: OutOfMemoryError, StackOverflowError (don't catch)
└── Exception                     checked, unless it's a RuntimeException
    ├── IOException, SQLException, InterruptedException…     checked
    └── RuntimeException          unchecked
        ├── NullPointerException
        ├── IllegalArgumentException → NumberFormatException
        ├── IndexOutOfBoundsException
        └── ArithmeticException, ClassCastException…
Java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class CheckedDemo {
    static String readConfig(Path path) throws IOException {      // throws: declares it
        return Files.readString(path);                             // may throw IOException (checked)
    }

    static int parseAge(String text) {
        int age = Integer.parseInt(text);                          // may throw NumberFormatException (unchecked)
        if (age < 0) throw new IllegalArgumentException("age < 0"); // throw: raises it
        return age;
    }

    public static void main(String[] args) {
        try {
            readConfig(Path.of("missing.properties"));
        } catch (IOException e) {                                   // the compiler insists on this
            System.out.println("config not found: " + e.getClass().getSimpleName());
        }
        System.out.println(parseAge("21"));                         // 21: no try needed to compile
    }
}
config not found: NoSuchFileException
21
CheckedUnchecked
ExtendsException (not RuntimeException)RuntimeException, or Error
CompilerMust catch or declare with throwsNo requirement
Typical causeExpected external problems: files, network, databasesProgramming errors: nulls, bad arguments, bad indexes
ExamplesIOException, SQLExceptionNullPointerException, ArithmeticException
throwthrows
WhereInside a method bodyIn the method signature
DoesRaises one exception objectDeclares exceptions the method may raise
Followed byAn instance: throw new X(...)Class names: throws IOException, SQLException

Error vs Exception: errors such as OutOfMemoryError signal problems the application usually can't recover from, so they aren't meant to be caught.

Follow-ups

  • Can you catch several exceptions in one block? Yes, with multi-catch: catch (IOException | SQLException e).
  • Why do some frameworks wrap checked exceptions in unchecked ones? To avoid throws clauses spreading through every layer; Spring, for example, turns SQLException into its own unchecked DataAccessException.
Q22Not marked yetFresherAsked oftenPredict the output

What does a method return if its try block runs return 1 and its finally block runs return 2? When would finally not run at all?

Say this first~30 sec to say

It returns 2. When try executes return 1, the value is computed but the method doesn't exit yet: finally runs first, and a return inside it replaces the pending value, so 1 is discarded. A return in finally also silently swallows exceptions thrown in try, so it's considered a bug. finally doesn't run if the JVM stops first: System.exit(), the process being killed or crashing, or the try block never finishing.

Could you say it?
Full answer3 min read
Java
public class FinallyReturn {
    static int value() {
        try {
            return 1;
        } finally {
            return 2;                    // replaces the 1; IDEs and javac -Xlint:finally warn about this
        }
    }

    static int swallow() {
        try {
            throw new IllegalStateException("lost");
        } finally {
            return -1;                   // the exception disappears
        }
    }

    static int noReturnInFinally() {
        int x = 1;
        try {
            return x;                    // the value 1 is saved here
        } finally {
            x = 99;                      // too late to change the saved return value
        }
    }

    public static void main(String[] args) {
        System.out.println(value());             // 2
        System.out.println(swallow());           // -1: no exception reaches main
        System.out.println(noReturnInFinally()); // 1
    }
}

When finally doesn't run

Situationfinally runs?
try completes normally, returns, or throwsYes
break or continue out of tryYes
System.exit() in tryNo: the JVM halts
The process is killed, or the JVM crashesNo
try loops forever or deadlocksNever reached
A daemon thread when the JVM exitsNo: daemon threads are abandoned

The rule: use finally, or better try-with-resources, for cleanup only, and never return, break or continue from it.

Follow-ups

  • What if finally itself throws? That exception replaces any exception from try, which is lost unless you catch it. Try-with-resources handles this better, with suppressed exceptions (question 24).
  • Same behaviour in Python? Yes: a return in finally overrides too, and recent Python versions warn about it (Python kit, question 32).
Q23Not marked yetFresherAsked oftenCoding

Create a custom checked exception such as InsufficientBalanceException, throw it from withdraw() and handle it where withdraw() is called. When would you extend RuntimeException instead?

Say this first~30 sec to say

Extend Exception to make it checked, give it constructors that pass a clear message to super, and add fields the caller may need, such as the shortfall. Declare it with throws on withdraw() and throw it when the balance is too low. The caller must then catch it and react, say by showing "insufficient funds". Extend RuntimeException instead when the problem is a programming error the caller can't reasonably recover from, such as a negative amount passed by mistake.

Could you say it?
Full answer3 min read
Java
class InsufficientBalanceException extends Exception {          // checked
    private final double shortfall;

    InsufficientBalanceException(double balance, double requested) {
        super(String.format("Balance ₹%.2f is less than ₹%.2f", balance, requested));
        this.shortfall = requested - balance;
    }

    double getShortfall() { return shortfall; }
}

class BankAccount {
    private double balance;

    BankAccount(double opening) { this.balance = opening; }

    void withdraw(double amount) throws InsufficientBalanceException {
        if (amount <= 0) {
            throw new IllegalArgumentException("amount must be positive");   // unchecked: a bug in the caller
        }
        if (amount > balance) {
            throw new InsufficientBalanceException(balance, amount);         // checked: an expected situation
        }
        balance -= amount;
    }

    double getBalance() { return balance; }
}

public class CustomException {
    public static void main(String[] args) {
        BankAccount account = new BankAccount(5000);
        try {
            account.withdraw(2000);
            account.withdraw(4000);
        } catch (InsufficientBalanceException e) {
            System.out.println(e.getMessage());
            System.out.println("Short by ₹" + e.getShortfall());
        }
        System.out.println("Balance: ₹" + account.getBalance());
    }
}
Balance ₹3000.00 is less than ₹4000.00
Short by ₹1000.0
Balance: ₹3000.0
Extend Exception (checked) whenExtend RuntimeException (unchecked) when
The caller can and should handle itIt signals a bug: invalid arguments, broken state
It's part of normal business flow: insufficient balance, seat already bookedHandling it at every call site would add noise
You want the compiler to force a decisionIt should travel up to a central handler

Good practice: keep a cause when wrapping another exception, super(message, cause), so the original stack trace survives; and name exceptions after the problem (SeatAlreadyBookedException), not the place they're thrown.

Follow-ups

  • Should you catch Exception everywhere? No: catch the specific types you can handle, and let others travel to a central handler that logs them.
  • What's exception chaining? Passing the original exception as the cause, new ServiceException("payment failed", e), so getCause() and the stack trace show both.
Q24Not marked yet1–3 yrsAsked sometimesCoding

Rewrite a try/catch/finally that closes a file reader using try-with-resources. What must the resource implement, and what happens to an exception thrown while closing?

Say this first~30 sec to say

Declare the resources in parentheses after try: try (BufferedReader reader = Files.newBufferedReader(path)) { ... }. Java closes them automatically when the block ends, normally or with an exception, in reverse order of opening. The resource must implement AutoCloseable (or its subinterface Closeable). If the body throws and close() throws too, the body's exception is the one that propagates, and the closing exception is attached to it as a suppressed exception, readable with getSuppressed().

Could you say it?
Full answer4 min read

Before: manual cleanup

Java
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class ManualClose {
    static String firstLine(Path path) throws IOException {
        BufferedReader reader = null;
        try {
            reader = Files.newBufferedReader(path);
            return reader.readLine();
        } finally {
            if (reader != null) {
                reader.close();                       // can throw, hiding the original exception
            }
        }
    }
}

After: try-with-resources

Java
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class AutoClose {
    static String firstLine(Path path) throws IOException {
        try (BufferedReader reader = Files.newBufferedReader(path)) {
            return reader.readLine();                 // reader.close() runs automatically
        }
    }
}

Closing order and suppressed exceptions

Java
public class Suppressed {
    static class Resource implements AutoCloseable {
        private final String name;

        Resource(String name) {
            this.name = name;
            System.out.println("open " + name);
        }

        @Override
        public void close() throws Exception {
            System.out.println("close " + name);
            if (name.equals("B")) throw new Exception("close failed: B");
        }
    }

    public static void main(String[] args) {
        try (Resource a = new Resource("A"); Resource b = new Resource("B")) {
            throw new IllegalStateException("work failed");
        } catch (Exception e) {
            System.out.println("caught: " + e.getMessage());
            for (Throwable s : e.getSuppressed()) {
                System.out.println("suppressed: " + s.getMessage());
            }
        }
    }
}
open A
open B
close B
close A
caught: work failed
suppressed: close failed: B

B closes before A (reverse order), every resource is closed even though the body threw, and the real error, "work failed", isn't hidden by the closing failure.

try/finallytry-with-resources
BoilerplateNull checks, nested try blocksNone
Several resourcesEasy to get the order or the null checks wrongClosed in reverse order automatically
Exception from close()Can replace the original exceptionAdded as suppressed; the original wins
SinceAlwaysJava 7; Java 9 allows effectively final variables: try (reader) {...}

Follow-ups

  • AutoCloseable vs Closeable? Closeable (from java.io) extends AutoCloseable, narrows the exception to IOException, and requires close() to be safe to call twice.
  • Can you still have catch and finally? Yes; they run after the resources have been closed.

08Collections

0/6 got it

Q25Not marked yetFresherAsked very oftenConcept

How does HashMap work internally? Cover hashing, buckets, collisions, load factor, resizing and Java 8's tree bins.

Say this first~30 sec to say

A HashMap is an array of buckets. put takes the key's hashCode(), mixes in its high bits, and picks a bucket with (capacity − 1) & hash. Keys in the same bucket collide and are chained; equals() finds the exact key. When entries exceed capacity × load factor (16 × 0.75 = 12 by default), the table doubles and entries are redistributed. Since Java 8, a chain that grows past 8 nodes becomes a red-black tree.

Could you say it?
Full answer3 min read

What put("Asha", 42) does

  1. h = key.hashCode(), then hash = h ^ (h >>> 16): spreads the high bits, so tables with few buckets still use them.
  2. index = (table.length - 1) & hash: the same as hash % length, because the length is always a power of two.
  3. Empty bucket: store a new node there.
  4. Occupied: walk the bucket; if a node's key equals the new key, replace its value; otherwise append a node.
  5. If the size now exceeds the threshold, resize: double the array and redistribute the entries.
Java
import java.util.HashMap;
import java.util.Map;

public class HashMapDemo {
    public static void main(String[] args) {
        Map<String, Integer> ages = new HashMap<>();
        ages.put("Asha", 24);
        ages.put("Ravi", 27);
        ages.put("Asha", 25);                       // same key: the value is replaced
        ages.put(null, 0);                          // one null key is allowed, in bucket 0

        System.out.println(ages.get("Asha"));       // 25
        System.out.println(ages.size());            // 3
        System.out.println(ages.containsKey(null)); // true

        System.out.println("Aa".hashCode() + " " + "BB".hashCode());   // 2112 2112: a real collision
    }
}

"Aa" and "BB" have the same hash code, so they share a bucket; the map still keeps them apart with equals().

SettingDefaultMeaning
Initial capacity16 bucketsAlways rounded up to a power of two
Load factor0.75Resize when size > capacity × 0.75
Treeify threshold8Adding to a bucket that already holds 8 nodes turns it into a red-black tree…
Minimum table size to treeify64…but below 64 buckets, the table resizes instead
Untreeify threshold6When a resize splits a tree bucket, a part with 6 or fewer nodes goes back to a list

Complexity: get and put are O(1) on average. With every key colliding, a chained bucket is O(n); since Java 8, tree bins cap that at O(log n) for comparable keys.

Follow-ups

  • Why must keys be immutable, or at least keep their hash code? If a key's hash changes after insertion, the map looks in the wrong bucket and can't find it (question 19).
  • How do you avoid resizing? If you know roughly how many entries there will be, size the map up front: new HashMap<>(expected * 4 / 3 + 1), or HashMap.newHashMap(expected) from Java 19.
Q26Not marked yetFresherAsked very oftenConcept

ArrayList vs LinkedList: how is each built internally, and when would you choose one over the other?

Say this first~30 sec to say

ArrayList wraps a resizable array: get(i) is O(1), adding at the end is O(1) amortised, and inserting or removing in the middle shifts elements, O(n). When full, it grows by about 50% and copies everything. LinkedList is a doubly linked list: adding or removing at either end is O(1), but get(i) walks node by node, O(n), and every element carries two extra pointers. In practice, choose ArrayList almost always; for a queue or stack, ArrayDeque beats LinkedList too.

Could you say it?
Full answer3 min read
OperationArrayListLinkedList
get(i), set(i)O(1)O(n)
add(x) at the endO(1) amortisedO(1)
add(0, x) at the frontO(n): shifts everythingO(1)
Insert or remove in the middleO(n) shiftingO(n) to find the spot, then O(1) to relink
Memory per elementOne reference in an arrayA node object: element + next + prev
CPU cacheElements' references are contiguous: fast to scanNodes scattered across the heap: slow to scan
Also implementsRandomAccessDeque: addFirst, pollLast …
Java
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class Lists {
    public static void main(String[] args) {
        List<String> array = new ArrayList<>(List.of("a", "b", "c"));
        array.add(1, "x");                        // shifts "b" and "c" one place right
        System.out.println(array);                // [a, x, b, c]
        System.out.println(array.get(2));         // b: direct index

        LinkedList<String> linked = new LinkedList<>(List.of("a", "b", "c"));
        linked.addFirst("start");                 // O(1): relink the head
        linked.removeLast();                      // O(1): relink the tail
        System.out.println(linked);               // [start, a, b]
    }
}

Why ArrayList usually wins even for inserts: shifting a block of references is a fast memory copy, while LinkedList has to walk to the insertion point node by node, chasing pointers through memory. LinkedList only wins when you already hold an iterator at the position and do many inserts or removals there.

ArrayList's growth: when the backing array is full, it allocates a new one about 1.5 times larger (newCapacity = old + old / 2) and copies the elements. Pass an initial capacity, new ArrayList<>(10_000), when you know the size.

Follow-ups

  • What does the DSA kit's array vs linked list answer add? The general trade-offs; this question is about Java's two classes (DSA kit, question 28).
  • Which is better for a queue? ArrayDeque: a resizable circular array, faster than LinkedList for both stack and queue use.
Q27Not marked yetFresherAsked oftenCoding

Make Employee implement Comparable by id, then sort the same list by salary and then name with a Comparator. When do you use each?

Say this first~25 sec to say

Comparable defines a class's natural order inside the class itself, with one method, compareTo; here, by id. Collections.sort(list) and TreeSet use it automatically. Comparator defines any number of external orders without touching the class, built neatly with Comparator.comparing(...) and thenComparing(...). Use Comparable for the one obvious order, and Comparators for every other way a screen or report needs to sort.

Could you say it?
Full answer3 min read
Java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

record Employee(int id, String name, double salary) implements Comparable<Employee> {
    @Override
    public int compareTo(Employee other) {
        return Integer.compare(id, other.id);           // natural order: by id
    }
}

public class Sorting {
    public static void main(String[] args) {
        List<Employee> staff = new ArrayList<>(List.of(
                new Employee(3, "Priya", 120000),
                new Employee(1, "Asha", 250000),
                new Employee(4, "Arjun", 120000),
                new Employee(2, "Ravi", 180000)));

        staff.sort(null);                                // null = use the natural order (Comparable)
        System.out.println(staff.stream().map(Employee::name).toList());
        // [Asha, Ravi, Priya, Arjun]

        staff.sort(Comparator.comparingDouble(Employee::salary)
                .thenComparing(Employee::name));         // salary, then name for ties
        System.out.println(staff.stream().map(Employee::name).toList());
        // [Arjun, Priya, Ravi, Asha]

        staff.sort(Comparator.comparingDouble(Employee::salary).reversed());
        System.out.println(staff.get(0).name());         // Asha: highest salary first
    }
}

Priya and Arjun earn the same, so thenComparing(Employee::name) breaks the tie alphabetically: Arjun first.

ComparableComparator
Packagejava.langjava.util
MethodcompareTo(T other)compare(T a, T b)
Where it livesInside the classOutside, as a separate object or lambda
How many ordersOne: the natural orderAs many as you like
Used by default byCollections.sort, TreeSet, TreeMapPassed explicitly: list.sort(cmp)
Can sort classes you can't editNoYes

A classic bug: return this.id - other.id; looks fine but overflows for large or negative values. Integer.compare (or Comparator.comparingInt) is always safe.

Follow-ups

  • What should compareTo be consistent with? Ideally equals: if compareTo returns 0, equals should be true, or sorted sets and maps behave oddly.
  • How do you sort nulls? Wrap the comparator: Comparator.nullsLast(Comparator.naturalOrder()).
Q28Not marked yetFresherAsked oftenConcept

Walk through the Collections hierarchy: how do List, Set, Queue and Map differ, and why doesn't Map extend Collection?

Say this first~25 sec to say

Collection is the root interface for groups of single elements, with three main branches. A List is ordered and allows duplicates, with index access. A Set holds unique elements. A Queue orders elements for processing, usually first in, first out, or by priority. A Map is separate: it holds key–value pairs, so methods like add(element) don't fit it. It offers collection views instead: keySet(), values() and entrySet().

Could you say it?
Full answer2 min read
Iterable
└── Collection
    ├── List          ArrayList, LinkedList, CopyOnWriteArrayList
    ├── Set           HashSet, LinkedHashSet, TreeSet (SortedSet)
    └── Queue         PriorityQueue, ArrayDeque, LinkedList
        └── Deque     ArrayDeque, LinkedList

Map (not a Collection)   HashMap, LinkedHashMap, TreeMap (SortedMap), ConcurrentHashMap
InterfaceDuplicatesOrderMain implementations
ListYesInsertion order, by indexArrayList (default), LinkedList
SetNoHashSet: none; LinkedHashSet: insertion; TreeSet: sortedHashSet (default)
Queue / DequeYesFIFO, LIFO or priorityArrayDeque, PriorityQueue
MapKeys no, values yesHashMap: none; LinkedHashMap: insertion; TreeMap: sorted keysHashMap (default)
Java
import java.util.*;

public class Hierarchy {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(List.of("Pune", "Delhi", "Pune"));
        Set<String> set = new TreeSet<>(list);                // unique and sorted
        Queue<Integer> queue = new PriorityQueue<>(List.of(5, 1, 3));
        Map<String, Integer> map = new LinkedHashMap<>();
        map.put("Pune", 2);
        map.put("Delhi", 1);

        System.out.println(list);              // [Pune, Delhi, Pune]
        System.out.println(set);               // [Delhi, Pune]
        System.out.println(queue.poll());      // 1: smallest first
        System.out.println(map.keySet());      // [Pune, Delhi]: a Set view of the keys
        System.out.println(map.entrySet());    // [Pune=2, Delhi=1]
    }
}

Why Map isn't a Collection: a Collection's contract is about single elements: add(E), contains(Object), iterator() over elements. A map's unit is a key–value pair, and its core operations are put(K, V) and get(K). Forcing one into the other would make half the methods meaningless, so Java offers views that are collections.

Follow-ups

  • Collection vs Collections? Collection is the root interface; Collections is a utility class of static helpers such as sort, unmodifiableList and synchronizedMap.
  • Which collections keep insertion order? ArrayList, LinkedList, LinkedHashSet and LinkedHashMap. Java 21 added SequencedCollection for exactly those with a defined order.
Q29Not marked yet1–3 yrsAsked oftenConcept

HashMap vs Hashtable vs ConcurrentHashMap: how do they differ in thread safety, null handling and performance?

Say this first~30 sec to say

HashMap has no locking: fastest, not thread-safe, and allows one null key and null values. Hashtable is a legacy class where every method is synchronized on the whole table, so it's thread-safe but threads queue up even to read, and it allows no nulls. ConcurrentHashMap is thread-safe with fine-grained locking: reads don't lock, and writes lock only one bucket (using CAS where possible), so it scales across threads. It allows no nulls either. Use HashMap in one thread, ConcurrentHashMap when shared.

Could you say it?
Full answer2 min read
HashMapHashtableConcurrentHashMap
Thread-safeNoYesYes
LockingNoneThe whole map, every methodPer bucket for writes; reads don't lock
Null keys and valuesOne null key; any number of null valuesNeitherNeither
IteratorsFail-fastFail-fast (its old Enumeration isn't)Weakly consistent: never throw ConcurrentModificationException
Performance with many threadsUnsafePoor: one lockGood
SinceJava 1.2Java 1.0 (legacy)Java 5
Java
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ConcurrentCounts {
    public static void main(String[] args) throws InterruptedException {
        Map<String, Integer> hits = new ConcurrentHashMap<>();
        ExecutorService pool = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 1000; i++) {
            pool.submit(() -> hits.merge("home", 1, Integer::sum));   // atomic read-modify-write
        }
        pool.shutdown();
        pool.awaitTermination(5, TimeUnit.SECONDS);
        System.out.println(hits.get("home"));                         // 1000, every time
    }
}

Why no nulls in ConcurrentHashMap? In a concurrent map, get(key) returning null must unambiguously mean "absent". If null values were allowed, you couldn't tell "absent" from "present with null" without a second call, and another thread could change the answer in between.

A trap, even with ConcurrentHashMap: if (!map.containsKey(k)) map.put(k, v); is two separate operations, so two threads can both pass the check. Use the atomic methods: putIfAbsent, computeIfAbsent, merge.

Follow-ups

  • What about Collections.synchronizedMap(new HashMap<>())? Thread-safe like Hashtable, with one lock for everything, and you must still lock manually while iterating.
  • How did ConcurrentHashMap work before Java 8? It split the map into 16 segments, each with its own lock. Java 8 replaced that with locking on individual bucket heads, plus CAS operations.
Q30Not marked yet1–3 yrsAsked oftenPredict the output

What happens when a for-each loop over an ArrayList holding "a", "b", "c" and "d" calls list.remove("a") on reaching "a"? How do fail-fast and fail-safe iterators differ?

Say this first~30 sec to say

It throws ConcurrentModificationException on the next iteration. A for-each loop uses an Iterator, which remembers the list's modification count; removing through the list itself, not the iterator, changes that count, and the iterator's next next() call detects the mismatch and fails fast. Remove safely with Iterator.remove() or list.removeIf(...). Fail-fast iterators throw on concurrent modification; fail-safe ones, from concurrent collections, iterate over a snapshot or a weakly consistent view instead.

Could you say it?
Full answer3 min read
Java
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class RemoveWhileIterating {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(List.of("a", "b", "c", "d"));
        try {
            for (String s : list) {
                if (s.equals("a")) list.remove(s);       // modifies the list behind the iterator's back
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("ConcurrentModificationException");
        }

        Iterator<String> it = list.iterator();           // list is now [b, c, d]
        while (it.hasNext()) {
            if (it.next().equals("b")) it.remove();      // safe: the iterator does the removal
        }
        list.removeIf(s -> s.equals("d"));               // safe and shortest
        System.out.println(list);                        // [c]

        List<String> cow = new CopyOnWriteArrayList<>(List.of("a", "b"));
        for (String s : cow) cow.add(s + "!");           // fail-safe: loops over a snapshot
        System.out.println(cow);                         // [a, b, a!, b!]
    }
}
ConcurrentModificationException
[c]
[a, b, a!, b!]

The famous exception to the rule: removing the second-to-last element, "c", doesn't throw. After the removal, the list's size equals the iterator's position, so hasNext() returns false, the loop ends, and next(), which does the check, is never called. The last element is silently skipped. That's why the original snippet removes "a": the behaviour must never be relied on.

Fail-fastFail-safe (weakly consistent or snapshot)
CollectionsArrayList, HashMap, HashSetCopyOnWriteArrayList, ConcurrentHashMap
On concurrent changeThrows ConcurrentModificationException (best effort)Doesn't throw
Sees changes made during iterationn/aSnapshot: no; weakly consistent: maybe
CostNoneCopy on every write (CopyOnWrite…) or more complex internals

Follow-ups

  • Is the exception only about multiple threads? No: as here, a single thread modifying a collection while iterating it is the most common cause.
  • How do you remove from a map while iterating? map.entrySet().removeIf(e -> e.getValue() == 0), or the iterator's remove().

09Java 8 features

0/3 got it

Q31Not marked yet1–3 yrsAsked oftenConcept

Streams: what are intermediate and terminal operations, what does lazy evaluation mean, and how does map() differ from flatMap()?

Say this first~30 sec to say

A stream is a pipeline over a data source. Intermediate operations, such as filter, map, sorted and distinct, return a new stream and are lazy: nothing runs until a terminal operation, such as collect, toList, forEach, count or reduce, asks for a result. Elements then flow through the pipeline one at a time, and short-circuiting operations like findFirst can stop early. map turns each element into exactly one element; flatMap turns each element into a stream and flattens them into one.

Could you say it?
Full answer3 min read
Java
import java.util.List;
import java.util.stream.Stream;

public class Streams {
    public static void main(String[] args) {
        List<String> names = List.of("Asha", "Ravi", "Priya", "Arjun");

        Stream<String> pipeline = names.stream()
                .filter(n -> {
                    System.out.println("filter " + n);
                    return n.startsWith("A");
                })
                .map(String::toUpperCase);
        System.out.println("nothing has run yet");
        System.out.println(pipeline.toList());               // the terminal operation starts it

        List<List<String>> teams = List.of(List.of("Asha", "Ravi"), List.of("Priya"));
        System.out.println(teams.stream().map(List::size).toList());              // [2, 1]
        System.out.println(teams.stream().flatMap(List::stream).toList());        // [Asha, Ravi, Priya]
    }
}
nothing has run yet
filter Asha
filter Ravi
filter Priya
filter Arjun
[ASHA, ARJUN]
[2, 1]
[Asha, Ravi, Priya]
Intermediate (lazy)Terminal (triggers the work)
filter, map, flatMapcollect, toList (Java 16)
sorted, distinctforEach, count, reduce
limit, skip, peekfindFirst, anyMatch, allMatch
mapToInt, boxedmin, max, sum (on primitive streams)

Rules worth saying out loud

  • A stream can be consumed only once; a second terminal operation throws IllegalStateException.
  • Streams don't modify their source; they produce new results.
  • Avoid side effects in map and filter (the prints above are for demonstration only).

What's new in Java 8, briefly: lambdas and method references (question 18), functional interfaces, streams, Optional (question 33), default and static interface methods (question 17), and the java.time date API.

Follow-ups

  • What does peek do? It runs an action on each element as it passes, without changing it; it's meant for debugging.
  • Are parallel streams faster? Sometimes, for large CPU-bound workloads. For small lists or I/O they're usually slower, and they share the common fork-join pool with the whole application.
Q32Not marked yet1–3 yrsAsked oftenCoding

Using streams, group a list of employees by department and find the average salary in each

Say this first~20 sec to say

collect(Collectors.groupingBy(Employee::dept, Collectors.averagingDouble(Employee::salary))). groupingBy splits the stream into groups by the key function, and the downstream collector, averagingDouble, reduces each group to its average. The result is a Map<String, Double>; pass TreeMap::new as the map factory if you want the departments sorted. Swapping the downstream collector answers the common variants: counting(), summingDouble, maxBy.

Could you say it?
Full answer3 min read
Java
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.stream.Collectors;

record Employee(String name, String dept, double salary) {}

public class GroupBy {
    public static void main(String[] args) {
        List<Employee> staff = List.of(
                new Employee("Asha", "Engineering", 250000),
                new Employee("Ravi", "Engineering", 180000),
                new Employee("Priya", "Engineering", 120000),
                new Employee("Sneha", "Sales", 150000),
                new Employee("Sana", "Sales", 160000),
                new Employee("Meera", "HR", 85000));

        Map<String, Double> avg = staff.stream()
                .collect(Collectors.groupingBy(Employee::dept, TreeMap::new,
                        Collectors.averagingDouble(Employee::salary)));
        System.out.println(avg);
        // {Engineering=183333.33333333334, HR=85000.0, Sales=155000.0}

        Map<String, Optional<Employee>> top = staff.stream()
                .collect(Collectors.groupingBy(Employee::dept, TreeMap::new,
                        Collectors.maxBy(Comparator.comparingDouble(Employee::salary))));
        top.forEach((dept, e) -> System.out.println(dept + ": " + e.get().name()));
        // Engineering: Asha
        // HR: Meera
        // Sales: Sana

        Optional<Double> secondHighest = staff.stream()
                .map(Employee::salary).distinct()
                .sorted(Comparator.reverseOrder())
                .skip(1).findFirst();
        System.out.println(secondHighest.orElseThrow());     // 180000.0

        Map<Character, Long> letters = "banana".chars()
                .mapToObj(c -> (char) c)
                .collect(Collectors.groupingBy(Function.identity(), TreeMap::new, Collectors.counting()));
        System.out.println(letters);                          // {a=3, b=1, n=2}
    }
}

The pieces

CollectorGives per group
groupingBy(key) aloneA List of the group's elements
averagingDouble(f)The average, as a Double
counting()The count, as a Long
summingDouble(f)The total
maxBy(cmp), minBy(cmp)An Optional of the largest or smallest
mapping(f, toList())The elements transformed, e.g. just the names

Follow-ups

  • How would you round the averages? Round when displaying (String.format("%.2f", v)), or use collectingAndThen(averagingDouble(...), v -> Math.round(v * 100) / 100.0).
  • How is this different from SQL? It's the same idea as GROUP BY dept with AVG(salary) (SQL & DBMS kit, question 18), done in memory.
Q33Not marked yet1–3 yrsAsked sometimesConcept

What problem does Optional solve? How do orElse() and orElseGet() differ, and where shouldn't you use Optional?

Say this first~30 sec to say

Optional<T> makes "there may be no value" explicit in a method's return type, so callers must deal with absence instead of tripping over a null they didn't expect. orElse(x) returns the value or x, but always evaluates x, even when a value is present; orElseGet(supplier) calls the supplier only if the value is missing, which matters when the fallback is expensive. Use Optional for return values; don't use it for fields, method parameters, or collections.

Could you say it?
Full answer3 min read
Java
import java.util.Map;
import java.util.Optional;

public class OptionalDemo {
    static final Map<Integer, String> USERS = Map.of(1, "Asha", 2, "Ravi");

    static Optional<String> findName(int id) {
        return Optional.ofNullable(USERS.get(id));        // empty if there's no such user
    }

    static String expensiveDefault() {
        System.out.println("  (computing the default)");
        return "Guest";
    }

    public static void main(String[] args) {
        System.out.println(findName(1).orElse(expensiveDefault()));        // computes it, then prints Asha
        System.out.println(findName(1).orElseGet(() -> expensiveDefault())); // Asha: no computing
        System.out.println(findName(9).orElseGet(() -> expensiveDefault())); // computes it, prints Guest

        String upper = findName(2).map(String::toUpperCase).orElse("UNKNOWN");
        System.out.println(upper);                                          // RAVI
        findName(9).ifPresentOrElse(
                n -> System.out.println("found " + n),
                () -> System.out.println("no user 9"));                    // no user 9
    }
}
  (computing the default)
Asha
Asha
  (computing the default)
Guest
RAVI
no user 9
MethodUse
orElse(value)A cheap, constant fallback
orElseGet(supplier)A costly or side-effecting fallback
orElseThrow()Absence is an error
map, filter, flatMapTransform without unwrapping
ifPresent, ifPresentOrElseRun code only when there's a value
get()Avoid: it throws if empty, which defeats the purpose

Where not to use it: as a field (it isn't serializable and adds an allocation), as a method parameter (callers then pass Optional.empty() or even null; use overloading instead), or to wrap collections (return an empty list rather than an empty Optional of a list).

Follow-ups

  • Optional.of vs ofNullable? of(null) throws NullPointerException; ofNullable(null) gives an empty Optional.
  • Does Optional remove NullPointerExceptions? No, but it makes absence visible in the API, so it's handled deliberately.

10Multithreading basics

0/5 got it

Q34Not marked yetFresherAsked very oftenConcept

How can you create a thread? Why is implementing Runnable usually preferred over extending Thread, and what happens if you call run() instead of start()?

Say this first~30 sec to say

Either extend Thread and override run(), or, better, implement Runnable (usually as a lambda) and pass it to a Thread, or submit it to an ExecutorService. Runnable is preferred because Java allows only one superclass, so extending Thread uses it up, and it separates the task from the thread that runs it, letting thread pools reuse threads. start() creates a new thread that then calls run(); calling run() directly just runs it on the current thread, like any method call.

Could you say it?
Full answer2 min read
Java
public class CreatingThreads {
    static class Worker extends Thread {                    // option 1: extend Thread
        @Override
        public void run() {
            System.out.println("Worker on " + Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () ->                               // option 2: a Runnable (preferred)
                System.out.println("task on " + Thread.currentThread().getName());

        Thread t1 = new Thread(task, "report-thread");
        t1.start();                                         // a new thread runs task
        t1.join();                                          // wait for it to finish

        Thread t2 = new Thread(task, "never-started");
        t2.run();                                           // no new thread: runs on main

        Worker w = new Worker();
        w.start();
        w.join();
    }
}
task on report-thread
task on main
Worker on Thread-0

The second line shows the run() trap: the code ran, but on main, so nothing happened in parallel.

Extend ThreadImplement Runnable
SuperclassUses up your one extendsFree to extend something else
Task vs threadMergedSeparate: the same task can run on any thread or pool
Works with thread poolsAwkwardlyDirectly: executor.submit(task)
As a lambdaNoYes

Other things to know

  • start() can be called only once per Thread object; a second call throws IllegalThreadStateException.
  • Thread states: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED.
  • Java 21 virtual threads: Thread.ofVirtual().start(task) creates very lightweight threads, which suit large numbers of I/O-bound tasks.

Follow-ups

  • What does join() do? Makes the calling thread wait until the other thread finishes.
  • What is a daemon thread? A background thread that doesn't keep the JVM alive: when only daemon threads remain, the JVM exits.
Q35Not marked yetFresherAsked oftenConcept

What does synchronized do? Compare synchronized methods and blocks, and explain how wait() differs from sleep().

Say this first~30 sec to say

Every object has a lock (monitor). synchronized makes a thread acquire it, so only one thread at a time runs the guarded code, and its changes are visible to the next thread that takes the lock. A synchronized method locks the whole method on this (the class, if static); a synchronized block locks only the lines that need it, on an object you choose. wait() releases the lock until notified; sleep() pauses but keeps its locks.

Could you say it?
Full answer3 min read
Java
public class Counter {
    private int count = 0;
    private final Object lock = new Object();

    public synchronized void incrementMethod() {     // locks "this" for the whole method
        count++;
    }

    public void incrementBlock() {
        // ...work that doesn't need the lock...
        synchronized (lock) {                        // locks only the critical section
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter c = new Counter();
        Runnable work = () -> {
            for (int i = 0; i < 100_000; i++) c.incrementBlock();
        };
        Thread a = new Thread(work), b = new Thread(work);
        a.start(); b.start();
        a.join(); b.join();
        System.out.println(c.count);                 // 200000; without synchronized, usually less
    }
}

count++ is really three steps (read, add, write). Without the lock, two threads can read the same value and one increment is lost.

wait() and notify(): a tiny hand-off

Java
public class OrderDesk {
    private String order;

    public synchronized void place(String o) {
        order = o;
        notifyAll();                                 // wake threads waiting on this object
    }

    public synchronized String take() throws InterruptedException {
        while (order == null) {
            wait();                                  // releases the lock while waiting
        }
        String o = order;
        order = null;
        return o;
    }
}

Waiting goes in a while loop, not an if, because a thread can wake up spuriously, or find that another thread got there first.

wait()sleep()
Belongs toObjectThread (static)
Must hold the lockYes, or IllegalMonitorStateExceptionNo
Releases the lockYesNo
Wakes up whenNotified, timed out, or interruptedThe time passes, or interrupted
Used forCoordinating threadsPausing

Follow-ups

  • Synchronized method or block? Prefer blocks: lock the smallest section, and lock on a private object so outside code can't interfere with your lock.
  • What does java.util.concurrent offer instead? ReentrantLock with timeouts and fairness, AtomicInteger for counters, and BlockingQueue for producer–consumer hand-offs.
Q36Not marked yet1–3 yrsAsked oftenCoding

Implement a thread-safe singleton. Why does double-checked locking need volatile, and what simpler options exist?

Say this first~30 sec to say

With double-checked locking, check the instance without locking, and only if it's null, lock and check again before creating it, so the lock is taken just once. The field must be volatile: object construction can be reordered, so another thread could otherwise see a non-null reference to a half-built object. Simpler options: an eager static final instance, the holder-class idiom, which is lazy and thread-safe through class loading, or an enum with one value.

Could you say it?
Full answer4 min read
Java
public final class AppConfig {
    private static volatile AppConfig instance;      // volatile: no half-built objects leak out

    private final String env;

    private AppConfig() {                             // private: nobody else can construct one
        env = System.getProperty("env", "dev");
    }

    public static AppConfig getInstance() {
        AppConfig result = instance;                  // one volatile read on the common path
        if (result == null) {                         // first check, without locking
            synchronized (AppConfig.class) {
                result = instance;
                if (result == null) {                 // second check, inside the lock
                    instance = result = new AppConfig();
                }
            }
        }
        return result;
    }

    public String env() { return env; }

    public static void main(String[] args) {
        System.out.println(AppConfig.getInstance() == AppConfig.getInstance());   // true
        System.out.println(AppConfig.getInstance().env());                       // dev
    }
}

Why volatile matters: instance = new AppConfig() is roughly three steps: allocate memory, run the constructor, assign the reference. Without volatile, the compiler or CPU may assign the reference before the constructor finishes, so a second thread passing the first, unlocked check could use an object whose fields aren't set yet. volatile forbids that reordering and guarantees visibility.

Simpler, and usually better

Java
// EagerConfig.java
public final class EagerConfig {                       // created when the class loads
    private static final EagerConfig INSTANCE = new EagerConfig();
    private EagerConfig() {}
    public static EagerConfig getInstance() { return INSTANCE; }
}

// LazyConfig.java
public final class LazyConfig {                        // the holder idiom: lazy, no locks
    private LazyConfig() {}
    private static class Holder {
        static final LazyConfig INSTANCE = new LazyConfig();   // runs on first getInstance()
    }
    public static LazyConfig getInstance() { return Holder.INSTANCE; }
}

// EnumConfig.java
public enum EnumConfig {                               // the enum singleton
    INSTANCE;
    public String env() { return "dev"; }
}
ApproachLazyThread-safeNotes
Eager static finalNoYes, by class initialisationThe simplest; fine for cheap objects
synchronized getterYesYesLocks on every call
Double-checked locking with volatileYesYesFast, but easy to get wrong
Holder classYesYes, by class loadingClean and lazy
EnumNoYesAlso safe against serialisation and reflection attacks

Follow-ups

  • How can a singleton be broken? Reflection (calling the private constructor), serialisation (creating a copy on deserialisation), and cloning. An enum singleton resists all three.
  • Why are singletons often discouraged? They're global state: hard to test and hidden dependencies. Frameworks such as Spring manage single instances through dependency injection instead.
Q37Not marked yet1–3 yrsAsked oftenConcept

Why use an ExecutorService instead of creating threads yourself? How do Runnable and Callable differ, and what does a Future give you?

Say this first~30 sec to say

An ExecutorService manages a pool of reusable threads and a queue of tasks: creating a thread per task is expensive and, under load, can exhaust memory, while a pool caps concurrency and reuses threads. A Runnable returns nothing and can't throw checked exceptions; a Callable<V> returns a value and can throw. Submitting a Callable gives you a Future<V>: a handle to the pending result, with get() to wait for it, isDone() and cancel().

Could you say it?
Full answer3 min read
Java
import java.util.List;
import java.util.concurrent.*;

public class Executors101 {
    static int fetchPrice(String item) throws InterruptedException {
        Thread.sleep(200);                                // stands in for a slow API call
        return switch (item) {
            case "kettle" -> 1299;
            case "mixer" -> 2499;
            default -> 399;
        };
    }

    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(3);
        try {
            List<Callable<Integer>> tasks = List.of(
                    () -> fetchPrice("kettle"),
                    () -> fetchPrice("mixer"),
                    () -> fetchPrice("bottle"));

            long start = System.currentTimeMillis();
            int total = 0;
            for (Future<Integer> f : pool.invokeAll(tasks)) {   // run all three at once
                total += f.get();                              // get() waits for each result
            }
            System.out.println("total ₹" + total);              // total ₹4197
            System.out.println("took about " + (System.currentTimeMillis() - start) / 100 * 100 + " ms");   // about 200, not 600

            Future<Integer> slow = pool.submit(() -> fetchPrice("kettle"));
            System.out.println(slow.get(1, TimeUnit.SECONDS));  // 1299, or TimeoutException after 1 s
        } finally {
            pool.shutdown();                                    // stop accepting work; let queued tasks finish
        }
    }
}
RunnableCallable<V>
Methodvoid run()V call() throws Exception
Returns a resultNoYes
Can throw checked exceptionsNoYes
Submitted withexecute() or submit()submit(), invokeAll(), invokeAny()

Common pools

FactoryUse
newFixedThreadPool(n)A bounded number of workers for CPU or mixed work
newCachedThreadPool()Many short tasks; grows as needed (can grow too far under load)
newSingleThreadExecutor()Tasks that must run one at a time, in order
newScheduledThreadPool(n)Delayed or periodic tasks
newVirtualThreadPerTaskExecutor() (Java 21)Huge numbers of I/O-bound tasks

Always shut the pool down. A running pool's threads keep the JVM alive; shutdown(), ideally in a finally (or try-with-resources from Java 19, since ExecutorService became AutoCloseable), lets it end cleanly.

Follow-ups

  • What does Future.get() do if the task threw? It throws an ExecutionException wrapping the task's exception.
  • What's CompletableFuture? A Future you can chain without blocking, supplyAsync(...).thenApply(...).thenAccept(...), and combine with others.
Q38Not marked yet1–3 yrsAsked sometimesCoding

Write code in which two threads deadlock over two locks, then fix it. What conditions cause a deadlock?

Say this first~30 sec to say

Thread 1 locks A then waits for B, while thread 2 locks B then waits for A: each holds what the other needs, so both wait forever. A deadlock needs four conditions at once: mutual exclusion, hold and wait, no preemption and circular wait. The simplest fix breaks the circular wait by making every thread take the locks in the same order. tryLock with a timeout is the other common fix: back off and retry instead of waiting forever.

Could you say it?
Full answer4 min read
Java
public class Deadlock {
    static final Object accountA = new Object();
    static final Object accountB = new Object();

    static void pause() {
        try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (accountA) {
                System.out.println("t1 locked A");
                pause();                                   // give t2 time to lock B
                synchronized (accountB) {                  // waits for t2: forever
                    System.out.println("t1 locked B");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (accountB) {
                System.out.println("t2 locked B");
                pause();
                synchronized (accountA) {                  // waits for t1: forever
                    System.out.println("t2 locked A");
                }
            }
        });
        t1.start();
        t2.start();
    }
}
t1 locked A
t2 locked B
(the program hangs here)

The first two lines can appear in either order, but the program hangs either way.

The fix: one global lock order

Java
public class NoDeadlock {
    static final Object accountA = new Object();
    static final Object accountB = new Object();

    static void transfer(String who) {
        synchronized (accountA) {                          // everyone locks A first...
            synchronized (accountB) {                      // ...then B: no cycle is possible
                System.out.println(who + " moved money");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> transfer("t1"));
        Thread t2 = new Thread(() -> transfer("t2"));
        t1.start(); t2.start();
        t1.join(); t2.join();
        System.out.println("done");                        // always reaches here
    }
}

With real accounts, order the locks by something stable, such as the account number: always lock the lower number first.

ConditionMeaningBreak it by
Mutual exclusionA lock is held by one thread at a time(Usually unavoidable)
Hold and waitHolding one lock while waiting for anotherAcquiring everything at once, or nothing
No preemptionLocks can't be taken awaytryLock with a timeout, then release and retry
Circular waitA cycle of threads, each waiting on the nextA global lock order

Finding one in a running JVM: jstack <pid> (or a thread dump from your monitoring tool) prints "Found one Java-level deadlock" with the threads and locks involved.

Follow-ups

  • Deadlock vs livelock vs starvation? Deadlock: blocked forever. Livelock: threads keep reacting to each other without progress. Starvation: a thread never gets the resource because others always win.
  • Do databases deadlock too? Yes; they detect it and roll one transaction back (SQL & DBMS kit, question 35).

11JVM and memory

0/2 got it

Q39Not marked yetFresherAsked very oftenConcept

What is garbage collection? When does an object become eligible, and can you force a collection?

Say this first~30 sec to say

Garbage collection automatically frees heap memory used by objects the program can no longer reach, so Java has no free or delete. An object becomes eligible when no chain of references leads to it from a GC root: local variables on thread stacks, static fields, and a few JVM internals. That happens when references are set to null, reassigned, or go out of scope. You can't force a collection: System.gc() is only a request the JVM may ignore.

Could you say it?
Full answer2 min read
Java
public class Eligibility {
    static class Order {
        final int id;
        Order(int id) { this.id = id; }
    }

    public static void main(String[] args) {
        Order a = new Order(1);
        Order b = new Order(2);

        a = null;                     // Order 1: no references left, so eligible
        b = new Order(3);             // Order 2: its only reference was reassigned, so eligible

        Order c = new Order(4);
        Order d = c;
        c = null;                     // Order 4 is still reachable through d: not eligible

        System.gc();                  // a request; the JVM decides whether and when to collect
        System.out.println(d.id);     // 4
    }
}

Cycles aren't a problem: two objects that point only to each other, with nothing reachable pointing to them, are both eligible. Java's collectors trace reachability from the roots rather than counting references, unlike CPython's reference counting (Python kit, question 35).

The generational idea, briefly: most objects die young, so the heap is split into a young generation, collected often and cheaply, and an old generation for long-lived objects, collected less often. Modern collectors such as G1 (the default), ZGC and Shenandoah focus on short pauses.

Memory leaks still happen in Java: if a reachable structure, such as a static Map used as a cache, keeps references to objects you no longer need, they can never be collected.

Follow-ups

  • Why not call System.gc() to free memory? The JVM tunes collection itself; forcing full collections usually just adds pauses. It can even be disabled with -XX:+DisableExplicitGC.
  • What replaced finalize() for cleanup? Try-with-resources, and Cleaner as a safety net (question 15).
Q40Not marked yetFresherAsked oftenConcept

Heap vs stack: what lives in each, and what typically causes StackOverflowError vs OutOfMemoryError?

Say this first~30 sec to say

Each thread has its own stack, holding a frame per method call: local variables, parameters, and references to objects. The heap is shared by all threads and holds every object and array, including the objects those references point to. StackOverflowError means one thread's stack ran out of room, almost always through too-deep or infinite recursion. OutOfMemoryError: Java heap space means the heap is full of reachable objects, through a leak or just too much data.

Could you say it?
Full answer3 min read
Java
import java.util.ArrayList;
import java.util.List;

public class MemoryErrors {
    static int depth = 0;

    static void recurse() {
        depth++;
        recurse();                               // no base case: a new frame for every call
    }

    public static void main(String[] args) {
        try {
            recurse();
        } catch (StackOverflowError e) {
            System.out.println("StackOverflowError after about " + depth / 1000 * 1000 + " calls");
        }

        List<long[]> hoard = new ArrayList<>();
        try {
            while (true) hoard.add(new long[1_000_000]);   // 8 MB each, all kept reachable
        } catch (OutOfMemoryError e) {
            int blocks = hoard.size();
            hoard.clear();                                  // release them so the program can continue
            System.out.println("OutOfMemoryError after " + blocks + " blocks of 8 MB");
        }
    }
}

The numbers depend on the stack size (-Xss) and heap size (-Xmx). Catching these errors is only for demonstration; real code shouldn't.

StackHeap
HoldsMethod frames: locals, parameters, return addresses, referencesObjects and arrays
Shared?One per threadShared by all threads
LifetimeA frame disappears when its method returnsUntil the object becomes unreachable and is collected
Size setting-Xss, per thread-Xms (initial), -Xmx (maximum)
Error when fullStackOverflowErrorOutOfMemoryError: Java heap space
Typical causeInfinite or very deep recursionLeaks (growing caches, collections), or loading too much data

Where each variable lives in Order o = new Order(1); inside a method: the reference o is in the method's stack frame; the Order object is on the heap.

Other OutOfMemoryErrors: Metaspace (too many loaded classes, often from a class-loader leak) and unable to create native thread (too many threads), which is why the message after the colon matters.

Follow-ups

  • How do you investigate an OutOfMemoryError? Capture a heap dump (-XX:+HeapDumpOnOutOfMemoryError) and open it in a tool such as Eclipse MAT to see which objects hold the memory.
  • How do you fix deep recursion? Rewrite it as a loop with an explicit stack, or increase -Xss if the depth is legitimately large.

Last step

Write these from memory

Before a Java interview, make sure you can write each of these on a blank page without help:

  • Q4
  • Q8
  • Q18
  • Q19
  • Q20
  • Q23
  • Q24
  • Q27
  • Q30
  • Q32
  • Q36
  • Q37
  • Q38

0 of 13 done