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 10 - Part 1
JAVA

Top 10 - Part 1

Practice 10 of the most important and frequently asked Java programming interview questions.

1. What will be the output of the following code?

String s1 = "Java";
String s2 = new String("Java");
String s3 = s2.intern();
System.out.println((s1 == s2) + " " + (s1 == s3));

Output: false true

The trick here is understanding where string objects actually live in memory, because == compares references, not contents.

When you write String s1 = "Java" with a literal, the JVM looks in a special area called the String Constant Pool. If the literal already exists there, s1 points to that pool object. Literals are reused — two literals with the same value always share the same pool object.

When you write new String("Java"), that is different. The new keyword forces the creation of a brand-new object on the heap, even though the value is the same as the pool object. So s1 and s2 are two separate objects that happen to hold the same characters. That is why s1 == s2 is false — their references point to different places.

The intern() method is the bridge between the two worlds. It looks in the String Pool for an object with the same content. If it finds one, it returns that pool reference. Since s1 already lives in the pool, s2.intern() hands back the very same object s1 points to. So s1 == s3 is true.

The lesson for interviews: == on strings checks whether two references point to the same object. To compare actual content, always use .equals(). String literals get pooled automatically, but strings created with new do not — unless you call intern().

2. What is the value of result after executing this code snippet?

int x = 5;
int result = x++ + ++x * x--;

Output: 54

This question tests three things at once: operator precedence, post-increment, and pre-increment. The key is to keep two separate things straight — what the operator evaluates to in the expression, and what the variable becomes afterward.

Let’s work through it one piece at a time. In x++, the post-increment evaluates to the current value of x, which is 5, and then increments x to 6. So the first operand contributes 5.

Next comes ++x. The pre-increment increments first, making x equal to 7, and then evaluates to that new value. So the second operand contributes 7.

Then x--. The post-decrement evaluates to the current value of x, which is 7, and then decrements x back to 6. So the third operand contributes 7.

Now apply precedence. Multiplication binds tighter than addition, so the * happens first: 7 * 7 = 49. Then the addition: 5 + 49 = 54.

A common mistake is to evaluate the increments out of order or to forget that the value a post-increment produces is the old value. If you track each operand’s contribution and the variable’s changes separately, the arithmetic falls out cleanly.

3. What will happen when compiling and executing the following program?

public class Test {
    public static void main(String[] args) {
        try {
            return;
        } finally {
            System.out.println("Finally Block");
        }
    }
}

Output: Finally Block is printed, then the method returns.

The finally block has one ironclad guarantee in Java: it always runs, no matter how the try block exits. Whether the code returns normally, throws an exception, or even calls System.exit() from a nested location that doesn’t kill the JVM, the finally block gets its turn.

In this program, the return statement inside try signals that the method wants to end. But before the method can actually hand control back to the caller, the JVM checks for a finally block. It finds one, executes it — printing Finally Block — and only then completes the return.

So the flow is: the try block hits return, the JVM pauses that return, runs the finally block, and then the return happens. The println executes first.

This guarantee is why finally is the natural home for cleanup work like closing files, releasing database connections, or releasing locks. You can rely on it running regardless of how the surrounding block exits. Interviewers also like to point out that this means code in finally can override an earlier return, which is the trap in the next question.

4. How does HashMap handle bucket collisions starting in Java 8?

Answer: Starting in Java 8, when a bucket’s chain of colliding entries gets long enough, HashMap converts that linked list into a balanced Red-Black tree.

To understand why, you have to know how a HashMap stores data. Every key is hashed, and the hash decides which bucket (array slot) the entry lands in. If two different keys hash to the same bucket, they collide and get chained together in a linked list. A short list is fine, but a very long list turns what should be O(1) lookup into O(n) — the HashMap degrades to a linear scan.

Before Java 8, that was the end of the story. A badly distributed hash could make a HashMap as slow as a list. Java 8 fixed the worst case with a simple threshold rule:

  • When the number of entries in a single bucket reaches 8 (the TREEIFY_THRESHOLD), and
  • the total table capacity is at least 64,

the linked list in that bucket is converted into a Red-Black tree. Searching a balanced tree is O(log n), so even a pathological bucket stays fast. If the bucket shrinks back below 6 entries (the UNTREEIFY_THRESHOLD), it reverts to a plain linked list, because for small sizes a list is cheaper.

The two conditions matter — the tree only forms when both the bucket is long and the table is big enough. The capacity check keeps tiny maps from wasting effort on tree construction.

For interviews, the takeaway is the number to remember: 8. That is the threshold at which a HashMap bucket escalates from a linked list to a Red-Black tree, protecting against worst-case O(n) behavior.

5. What is the output of the following method invocation?

public class Overload {
    static void print(Object o) { System.out.print("Object "); }
    static void print(String s) { System.out.print("String ");
    }
    public static void main(String[] args) {
        print(null);
    }
}

Output: String

When you call an overloaded method, Java has to decide which version to invoke. The rule it follows is called most specific method selection.

Here there are two candidates: print(Object) and print(String). The argument is null, and here is the subtlety — null is compatible with both. It can be treated as an Object reference, and it can also be treated as a String reference, because String is a subclass of Object.

When multiple overloads are applicable, Java picks the one whose parameter type is the most specific — the type that is the closest in the inheritance hierarchy. Since String is a subclass of Object, String is more specific than Object. So print(String) wins, and the output is String .

If you want to force the Object version, you would have to cast: print((Object) null). That explicit cast removes the ambiguity and tells the compiler exactly which overload you mean.

This question is a favorite because it combines two classic Java topics — overloading resolution and the fact that null fits any reference type. The rule to remember: Java prefers the most specific applicable method.

6. What will be printed by the following stream pipeline?

List<String> list = List.of("apple", "banana", "cherry");
list.stream()
    .filter(s -> s.startsWith("a"))
    .peek(System.out::print);

Output: nothing is printed.

The whole point of this question is that Java streams are lazy. Intermediate operations — filter, peek, map, distinct — do not actually do any work when you chain them. They just build a description of what should happen. The pipeline only starts running when a terminal operation is reached.

Here, the pipeline has filter and peek, both intermediate operations, and then… nothing. There is no collect(), no forEach(), no count(), no terminal operation at all. So the JVM never executes anything. peek, despite its name, never runs, and System.out::print is never called.

This is by design. Laziness lets Java build efficient pipelines — it can skip work, short-circuit, and process only the elements that are actually needed, rather than eagerly running every stage on every element.

The fix would be to add a terminal operation, for example:

list.stream()
    .filter(s -> s.startsWith("a"))
    .peek(System.out::print)
    .count();

The interview lesson is simple: intermediate operations alone produce no output. A stream pipeline does nothing until a terminal operation kicks it into gear.

7. Which output is produced by the following code?

public class ExceptionTest {
    static int getValue() {
        try {
            return 10;
        } finally {
            return 20;
        }
    }
    public static void main(String[] args) {
        System.out.println(getValue());
    }
}

Output: 20

This is the trap promised in question 3. A return inside a finally block doesn’t just run before the try’s return — it replaces it.

Here’s the sequence. The try block executes return 10, which says “the value of this method is 10.” But before that value is handed back, the JVM must run the finally block. Inside finally, there is another return 20. That new return discards the pending 10 and replaces it with 20. The method returns 20.

The same rule applies to exceptions. If a try block throws an exception but the finally block contains a return, the return swallows the exception entirely — the caller never sees it. That silent swallowing is exactly why writing return inside finally is considered a bad practice.

The Java Language Specification is explicit: a finally block can abort whatever the try or catch was about to do, whether that was a return value or a thrown exception.

For interviews, remember the asymmetry: code in finally always runs, and a return in finally takes over completely. To return a meaningful value safely, compute it in the try and let the finally block only do cleanup — never return from it.

8. What is the printed result of comparing these wrapper objects?

Integer a = 127;
Integer b = 127;
Integer c = 128;
Integer d = 128;
System.out.println((a == b) + " " + (c == d));

Output: true false

Two things collide here: autoboxing and reference comparison.

When you assign an int literal to an Integer, Java silently boxes it — it creates an Integer object. The question is whether a and b end up referencing the same object or two different ones, because == on objects compares references.

The answer is that Java caches Integer objects for values in a specific range: -128 to 127. This is the IntegerCache. When autoboxing a value inside that range, the JVM reuses a cached object instead of creating a new one. So 127 autoboxes to the same cached Integer for both a and b, making a == b true.

But 128 is outside the cache range. Autoboxing 128 creates a fresh Integer object each time, so c and d are two distinct objects. Comparing their references gives false, even though their values are equal.

The rule generalizes: == on wrapper objects compares references, not values, and only the cached range (-128 to 127) is guaranteed to share objects. Outside that range, two Integer values may or may not be the same object depending on the JVM. To compare values reliably, use .equals() — or unbox with .intValue().

9. What happens when you attempt to run a class where main is NOT declared as static?

Answer: The code compiles fine, but running it throws a runtime error: Main method is not static in class Test, please define the main method as: public static void main(String[] args).

The surprising part is that this is not a compile-time error. The Java compiler happily compiles a class whose main method is an instance method. It is only the JVM at launch time that strictly requires the exact signature public static void main(String[] args).

So the failure happens at runtime, not at compile time. When you run java Test, the JVM looks for a method matching that exact signature. If it finds main but it isn’t static, it reports an error telling you the main method must be static. If it finds no usable main at all, you get Error: Main method not found.

Why does the JVM require static? Because main is the entry point — the JVM starts the program before any objects exist. There is no instance to call a method on, so main must be static, callable directly from the class itself.

The signature has to be exact, too: public (so the JVM can access it), static (no instance needed), void (the JVM doesn’t expect a return value), and String[] args (to receive command-line arguments). Miss any piece, and the launch fails at runtime.

10. What is the output of the following inheritance snippet?

class Parent {
    static void show() { System.out.print("Parent "); }
}
class Child extends Parent {
    static void show() { System.out.print("Child "); }
}
public class Test {
    public static void main(String[] args) {
        Parent p = new Child();
        p.show();
    }
}

Output: Parent

The key fact is that static methods cannot be overridden — they can only be hidden. That distinction changes everything about how this call is resolved.

For instance methods, the JVM decides which version to call at runtime, based on the actual type of the object. If show were an instance method, p.show() would call Child’s version, because p actually holds a Child object.

Static methods work differently. They are resolved at compile time, based purely on the reference type of the variable — not the object it happens to hold. The variable p is declared as type Parent, so the compiler binds p.show() to Parent.show(). The fact that p points to a Child object is irrelevant for a static call.

When Child declares a static method with the same signature, it doesn’t override Parent’s — it hides it. From the child class, Child.show() refers to the child version, but a call made through a Parent reference always reaches the parent version.

That is why the output is Parent . To get the child’s version, you would have to call Child.show() explicitly.

The interview takeaway: static methods are resolved at compile time by the reference type; instance methods are resolved at runtime by the object type. Never use a reference to call static methods if you expect dynamic behavior — call them through the class directly.

My Private Notes

Notes are auto-saved locally to this device.