Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Top 50 - Part 3
JAVA

Top 50 - Part 3

Practice the final 20 questions from a comprehensive set of 50 important Java programming interview questions.

1. Which classloader in the JVM hierarchy is responsible for loading standard runtime classes like java.lang.Object?

Answer: The Bootstrap ClassLoader.

The JVM loads classes through a hierarchy of classloaders, each with a defined responsibility:

  • Bootstrap ClassLoader — the root of the hierarchy. It’s not a Java class at all; it’s implemented natively (in C/C++) and is built into the JVM. It loads the core JDK classes — everything in java.base, which includes java.lang.Object, String, the collections, and the rest of the fundamental runtime.
  • Platform (Extension) ClassLoader — loads JDK modules beyond the core, historically the extension and module-path classes.
  • Application (System) ClassLoader — loads classes from the application’s own classpath.
  • Custom ClassLoaders — user-defined loaders for specialized needs like hot-reloading or plugin systems.

The point to remember: the most fundamental classes, the ones the JVM needs before anything else works, come from the Bootstrap ClassLoader. It sits at the base of the hierarchy and delegates nothing upward because it’s already the top.

2. What will be printed by this snippet?

public class ThreadTest {
    public static void main(String[] args) throws Exception {
        Thread t = new Thread(() -> System.out.print("Run "));
        t.run();
        System.out.print("Main ");
    }
}

Output: Run Main

The trap is mistaking .run() for .start(). They are completely different.

  • t.start() creates a new thread and executes run() on it, asynchronously. The order of output would then be nondeterministic.
  • t.run() is just a normal method call. It invokes the run() body directly on the current thread — the main thread. No new thread is spawned.

So the code runs purely sequentially: run() executes its lambda, printing Run , then control returns to main, which prints Main . The output is deterministically Run Main .

The interview lesson: calling run() directly does not start a thread. It’s a plain synchronous call that runs the task on the calling thread. Only start() launches real concurrency.

3. What will be the output of this code?

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
for (int i = 0; i < list.size(); i++) {
    list.remove(i);
}
System.out.println(list);

Output: [B]

The bug hides in the fact that remove(i) shifts elements left, and the loop counter is still climbing.

Walk through it. The list starts as [A, B].

  • i = 0: condition 0 < 2 is true. list.remove(0) removes "A". The array shifts left: "B" moves to index 0. The list is now [B], size 1.
  • i = 1: condition 1 < 1 is false. The loop ends immediately.

So "B" survives at index 0, never examined. The list prints [B].

The general pattern this illustrates: removing elements from a collection while iterating with an index loop skips elements, because each removal shifts the remaining ones into positions the loop has already passed. The correct approaches are iterating from the end (i = size-1; i >= 0; i--), using an Iterator with remove(), or list.removeIf(...).

4. What is the output of this exception handling block?

public class ThrowTest {
    public static void main(String[] args) {
        try {
            throw null;
        } catch (Exception e) {
            System.out.println(e.getClass().getSimpleName());
        }
    }
}

Output: NullPointerException

You can write throw null — it compiles. The question is what happens when it executes.

In Java, the throw statement requires a Throwable. When you throw null, there is no actual exception object to throw. The JVM resolves this at runtime by replacing the null with a freshly created NullPointerException.

That exception propagates up and is caught by catch (Exception e). NullPointerException is a subclass of Exception, so the catch matches. e.getClass().getSimpleName() prints NullPointerException.

The interview point: throw null compiles and, when executed, throws a NullPointerException. It’s a deliberately confusing question that tests whether you know the JVM’s behavior rather than just the grammar.

5. What is the outcome when applying final to a class declaration?

Answer: The class cannot be extended (subclassed).

final is a modifier with different meanings depending on what it’s attached to:

  • final class — cannot be subclassed. String is the canonical example; so are the boxed primitives and Math. You cannot extend String.
  • final method — cannot be overridden by a subclass.
  • final field/variable — cannot be reassigned after initialization.

The purpose of a final class is to lock down its behavior and identity. Since no subclass can exist, its methods can never be overridden, which makes the class safe to cache, safely comparable by reference, and immune to extension-based attacks.

Note what final does not do: it doesn’t prevent instantiation (that’s an abstract class’s job), and it doesn’t make fields static. A final class is perfectly instantiable — String s = "hi" works fine.

6. What will be printed?

public class IntTest {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        System.out.println("Result: " + a + b);
    }
}

Output: Result: 1020

String concatenation with + is evaluated left to right, and the moment a String is in the chain, everything that follows is converted to a string too.

The expression is "Result: " + a + b. The + operator associates left to right:

  1. "Result: " + a — one operand is a String, so a (int 10) is converted to the string "10". The result is "Result: 10".
  2. "Result: 10" + b — the left operand is again a String, so b (int 20) becomes "20". The result is "Result: 1020".

There is no arithmetic addition at all, because by the time b is reached, the left operand is already a string.

The contrast: System.out.println(a + b) would print 30 — no string present, so plain integer addition. But the prefix "Result: " changes everything. If the intent were arithmetic, the parenthesization "Result: " + (a + b) would be required.

7. Which set operation guarantees maintaining insertion order?

Answer: LinkedHashSet.

The three common Set implementations differ in one key dimension: ordering.

  • HashSet — backed by a hash table. Fast O(1) lookups, but the iteration order is essentially arbitrary; it depends on hash codes and can change when the table resizes.
  • TreeSet — backed by a Red-Black tree. Keeps elements in sorted order, whether by natural ordering or a supplied comparator. Not insertion order.
  • LinkedHashSet — a HashSet augmented with a doubly linked list running through the entries. This records the order in which elements were inserted, so iteration yields them in insertion order — with the same hash-table performance.
  • ConcurrentSkipListSet — a concurrent sorted set, also ordered, not insertion-ordered.

The answer is LinkedHashSet, the hybrid: hash-table speed plus deterministic, insertion-ordered iteration.

8. What will be the printed output?

public class TernaryTest {
    public static void main(String[] args) {
        System.out.println(true ? 1 : 2.0);
    }
}

Output: 1.0

The ternary operator a ? b : c produces a single result whose type must accommodate both branches. When the branches have different types, Java performs numeric promotion to find a common type.

Here the branches are 1 (an int) and 2.0 (a double). The common type is double — every int can be widened to a double. So the ternary’s type is double, the chosen branch value 1 is promoted to 1.0, and println prints 1.0.

The output is 1.0, not 1 — a subtle promotion that changes the printed form. The lesson: the ternary’s type is the common type of both branches, not the type of the branch actually selected at runtime.

9. What is the purpose of the Java 10 local variable type inference keyword var?

Answer: var lets the compiler infer a local variable’s type from its initializer — at compile time, preserving static type safety. Java does not become dynamically typed.

var is pure syntax sugar for local variable declarations. Consider:

var list = new ArrayList<String>();

The compiler sees the initializer new ArrayList<String>() and infers that list is of type ArrayList<String>. The compiled bytecode is identical to writing the type out by hand.

Crucially, var does not introduce dynamic typing the way JavaScript or Python have it. The type is fully known and fixed at compile time — list is statically an ArrayList<String>. You can’t reassign it to an unrelated type, and the IDE and compiler apply full type checking.

var is restricted to local variables (and a few places like enhanced-for loop variables). It’s not allowed for fields, method parameters, or return types.

The interview answer: var is compile-time local type inference — static typing, just with the type name written by the compiler instead of you.

10. What will be the output of this snippet?

public class EqualsTest {
    public static void main(String[] args) {
        int[] arr1 = {1, 2, 3};
        int[] arr2 = {1, 2, 3};
        System.out.println(arr1.equals(arr2));
    }
}

Output: false

The trap is assuming arrays behave like other objects. They don’t override equals().

arr1.equals(arr2) calls the inherited Object.equals(), whose default implementation is simply reference equality: arr1 == arr2. The two arrays are distinct objects with different identities, even though their contents match. So the result is false.

If the code had wanted to compare contents, the correct call is Arrays.equals(arr1, arr2), which compares element by element and returns true for these two arrays. (For nested arrays, Arrays.deepEquals is needed.)

The interview point: arrays inherit Object.equals() and are compared by reference, not by content. Content comparison requires Arrays.equals.

11. Which state is a thread in when waiting to acquire a Java synchronized block lock?

Answer: BLOCKED.

The Thread.State enum has six values, and they map to specific situations:

  • NEW — created, not yet started.
  • RUNNABLE — executing or ready to run.
  • BLOCKED — waiting to acquire a monitor lock (entering or re-entering a synchronized block or method held by another thread).
  • WAITING — waiting indefinitely for another thread to act (wait(), join() without timeout).
  • TIMED_WAITING — waiting with a timeout (sleep, wait(timeout), join(timeout)).
  • TERMINATED — finished.

The distinguishing detail: BLOCKED specifically means blocked on a lock, whereas WAITING/TIMED_WAITING mean waiting for another thread’s notification or a timeout. When a thread can’t get past a synchronized statement, it’s BLOCKED. That’s the answer.

12. What will be the printed result?

public class MathTest {
    public static void main(String[] args) {
        System.out.println(10 / 4);
    }
}

Output: 2

Both 10 and 4 are int literals, and the / operator on two integers performs integer division — the fractional part is truncated (not rounded). 10 / 4 is 2, with the remainder 2 discarded.

The output prints 2, not 2.5 and not 2.0.

To get a fractional result, at least one operand must be a floating-point type: 10 / 4.0 or 10.0 / 4 would produce 2.5. The interview point is the classic one — know when integer division applies, because silent truncation is a common source of off-by-one bugs in real code.

13. What will be printed by this code?

public class BoolAssign {
    public static void main(String[] args) {
        boolean b = false;
        if (b = true) {
            System.out.println("TRUE");
        } else {
            System.out.println("FALSE");
        }
    }
}

Output: TRUE

The condition uses the assignment operator = instead of the equality operator ==. That’s not a typo — it’s the whole question.

b = true is an assignment expression. It assigns true to b and evaluates to the assigned value, which is true. Since the condition of the if is true, the then-branch executes and prints TRUE.

This compiles and runs normally because b is a boolean and the assignment’s value is a boolean — legal as a condition. (The infamous bug version is if (x = 1) with an int, which would not compile in Java — another reason Java is stricter than C here.)

The lesson: = assigns and evaluates to the assigned value; == compares. In conditions, = is almost always a mistake — one reason many style guides mandate if (true == b) or the Yoda style, so a missing = produces a compile error instead of silent wrong behavior.

14. What is the output of the following stream pipeline?

List<Integer> list = List.of(1, 2, 3);
list.stream()
    .map(x -> x * 2)
    .forEach(System.out::print);

Output: 246

Two operations: an intermediate map and a terminal forEach.

map(x -> x * 2) transforms each element: 12, 24, 36. The stream now holds [2, 4, 6].

forEach(System.out::print) applies print to each element. Note the method reference is print — which writes without spaces or newlines. So the output is the digits run together: 246.

If it had been println, each value would be on its own line. The answer is the unspaced concatenation 246.

15. What is the outcome of attempting to instantiate an abstract class directly?

Answer: Compilation error — an abstract class cannot be instantiated.

An abstract class is an incomplete template. It may declare abstract methods — signatures with no body — that subclasses are responsible for implementing. Instantiating such a class directly would produce an object missing those implementations, which makes no sense.

So new AbstractClass() is a compile-time error. The language refuses to let you create an instance of a type that isn’t fully defined.

The correct usage: a concrete subclass extends the abstract class, implements all its abstract methods, and then that subclass is instantiated. (The subclass’s constructor implicitly invokes the abstract class’s constructor to initialize the inherited state — but you can never new the abstract class itself.)

The interview answer: abstract classes cannot be instantiated with new; they exist to be extended by concrete subclasses.

16. What happens when a method is declared final in Java?

Answer: It cannot be overridden by subclasses.

final on a method is a contract: the implementation is final, and no subclass may replace it with its own version.

When a subclass tries to override a final method, the compiler rejects it — this is a compile-time error, caught before the program even runs.

Why is this useful? It locks down critical behavior. A base class author may have a method whose semantics must not change — an initialization sequence, a security check, a hot path that must not be reimplemented incorrectly. Marking it final guarantees every subclass inherits exactly that behavior. Object.getClass() is effectively like this — not final itself, but the same idea of protected invariants.

Note the distinction from final classes: a final method still allows subclassing — subclasses just can’t change that one method. A final class forbids subclassing entirely.

17. What will be printed by this code?

public class StrNull {
    public static void main(String[] args) {
        String str = null;
        System.out.println(str + " Java");
    }
}

Output: null Java

This seems like it should throw NullPointerException — calling a method on null. But string concatenation has special handling.

str + " Java" is string concatenation. The JVM compiles it (loosely) into something like new StringBuilder().append(str).append(" Java").toString(). The key is StringBuilder.append(Object): when the argument is null, it appends the literal string "null" rather than throwing.

So the concatenation produces the string "null Java", and println prints null Java.

The lesson: concatenating a null reference doesn’t throw — it renders as the text "null". This is a frequent source of confusion (and of “null” appearing in logs you expected to be empty).

18. What is the function of the super keyword inside a subclass constructor?

Answer: super(...) invokes the superclass constructor, ensuring inherited state is initialized before the subclass constructor body runs.

Object construction is a chain. When you create an instance of a subclass, the parent’s state must be set up first — a subclass builds on top of a parent, and the parent has no valid instance until its constructor has run.

super(args) is the explicit way to trigger that. Java also has an implicit rule: if a subclass constructor doesn’t call super(...), the compiler inserts super() — the no-arg parent constructor — as the first statement. Either way, the parent constructor runs before any subclass-specific code.

The rules are strict:

  • The super(...) call must be the first statement in the constructor.
  • If the parent has no accessible no-arg constructor, the subclass must call super(...) explicitly with matching arguments — otherwise the code won’t compile.

The interview point: super(...) (and its cousin this(...) for the same class) chain constructors upward so that parent state is initialized before child state.

19. Which functional interface signature matches the Java 8 lambda s -> s.length()?

Answer: Function<String, Integer>.

A lambda matches a functional interface based on its shape — the parameter types and return type.

s -> s.length() takes one argument (s) and produces an integer result (s.length() returns int). So it’s a function that maps a String to an int. Among the standard functional interfaces:

  • Function<T, R> — takes one T, returns one R. Function<String, Integer> takes a String and returns an Integer (the int is boxed). This matches.
  • Consumer<T> — takes one T, returns void. The lambda returns a value, so no match.
  • Supplier<T> — takes nothing, returns a T. The lambda takes an argument, so no match.
  • Predicate<T> — takes one T, returns boolean. The lambda returns an int, so no match.

The answer is Function<String, Integer> — the mapping shape: one input, one output.

20. What will be printed by this code?

public class ListTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add(0, "B");
        System.out.println(list);
    }
}

Output: [B, A]

List.add(index, element) is an insertion, not an overwrite. It puts the new element at the given index and shifts every existing element from that index onward one position to the right.

Walk through it. The list starts [A]. Then list.add(0, "B") inserts "B" at index 0. The existing "A" shifts from index 0 to index 1.

The final list is [B, A], and that’s what prints.

The lesson: add(0, x) inserts at the front and shifts everything else right; it does not replace the element at index 0. Contrast with set(index, element), which overwrites in place.

My Private Notes

Notes are auto-saved locally to this device.