Menu

Earn Premium with Referrals

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

See how it works and start inviting friends.

OOP & Language Features
JAVA

OOP & Language Features

Practice 12 questions covering classes, inheritance, polymorphism, interfaces, encapsulation, and important Java language features.

1. 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.

Answer:

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.

2. Which statement is TRUE regarding interface default methods in Java 8+?

Answer: Default methods let you add new functionality to interfaces while preserving backward compatibility for existing implementing classes.

Before Java 8, interfaces could only declare method signatures. Adding a new method to an interface broke every class that implemented it, because those classes would no longer implement the full contract. For a widely used interface like Collection, that made evolution nearly impossible.

Default methods solved this. A default method has a body written right in the interface:

interface Greeter {
    default String greet() {
        return "Hello";
    }
}

Existing implementing classes instantly inherit this implementation — they keep compiling without any changes. New behavior is added without breaking old code. This is exactly how Java 8 added .stream() to Collection without forcing every existing collection class to be rewritten.

There are important limits. A default method cannot override equals, hashCode, or toString from java.lang.Object — those are reserved for the classes themselves. Interfaces also cannot hold instance state fields; the default method body has no fields to work with beyond constants. And a default method is not mandatory — an implementing class can override it, or ignore it and use the provided body.

The interview takeaway: default methods are the mechanism for interface evolution — add methods to a published interface without breaking implementers.

Answer:

Default methods let you add new functionality to interfaces while preserving backward compatibility for existing implementing classes.

Before Java 8, interfaces could only declare method signatures. Adding a new method to an interface broke every class that implemented it, because those classes would no longer implement the full contract. For a widely used interface like Collection, that made evolution nearly impossible.

Default methods solved this. A default method has a body written right in the interface:

interface Greeter {
    default String greet() {
        return "Hello";
    }
}

Existing implementing classes instantly inherit this implementation — they keep compiling without any changes. New behavior is added without breaking old code. This is exactly how Java 8 added .stream() to Collection without forcing every existing collection class to be rewritten.

There are important limits. A default method cannot override equals, hashCode, or toString from java.lang.Object — those are reserved for the classes themselves. Interfaces also cannot hold instance state fields; the default method body has no fields to work with beyond constants. And a default method is not mandatory — an implementing class can override it, or ignore it and use the provided body.

The interview takeaway: default methods are the mechanism for interface evolution — add methods to a published interface without breaking implementers.

3. Which method in Object must be overridden whenever equals() is overridden?

Answer: hashCode().

The rule is really a contract, and it lives in the documentation of Object.hashCode(): if two objects are equal according to equals(), then calling hashCode() on both must produce the same integer.

Why does this matter? Hash-based collections — HashMap, HashSet, HashTable — do not work by calling equals() on everything. They work in two steps. First they use hashCode() to decide which bucket to look in. Only then do they use equals() to compare the objects actually in that bucket.

Now consider what happens if you override equals() to treat two objects as equal but leave hashCode() inherited. Two objects that are equal land in different buckets (because their hash codes differ). A HashSet then sees them as distinct — both get stored, the set silently loses its uniqueness guarantee. Or a HashMap fails to find a key that is actually equal to a stored key. The whole collection misbehaves, and the bug is maddening to track down because nothing throws.

The contract also runs the other way, but with a softer rule: two unequal objects may share a hash code (that is just a collision, handled by equals() inside the bucket). Equal must imply equal hash; unequal may share.

So the interview answer is crisp: whenever you override equals(), you must override hashCode() too, so that equal objects always produce equal hash codes. Ignore either half and hash collections stop working correctly.

Answer:

hashCode().

The rule is really a contract, and it lives in the documentation of Object.hashCode(): if two objects are equal according to equals(), then calling hashCode() on both must produce the same integer.

Why does this matter? Hash-based collections — HashMap, HashSet, HashTable — do not work by calling equals() on everything. They work in two steps. First they use hashCode() to decide which bucket to look in. Only then do they use equals() to compare the objects actually in that bucket.

Now consider what happens if you override equals() to treat two objects as equal but leave hashCode() inherited. Two objects that are equal land in different buckets (because their hash codes differ). A HashSet then sees them as distinct — both get stored, the set silently loses its uniqueness guarantee. Or a HashMap fails to find a key that is actually equal to a stored key. The whole collection misbehaves, and the bug is maddening to track down because nothing throws.

The contract also runs the other way, but with a softer rule: two unequal objects may share a hash code (that is just a collision, handled by equals() inside the bucket). Equal must imply equal hash; unequal may share.

So the interview answer is crisp: whenever you override equals(), you must override hashCode() too, so that equal objects always produce equal hash codes. Ignore either half and hash collections stop working correctly.

4. What is the result of compiling and running this Record class snippet (Java 14+)?

public record User(String name, int age) {}

Answer: The compiler automatically generates the final fields, a canonical constructor, accessor methods (name(), age()), and implementations of equals(), hashCode(), and toString().

A record is a transparent, immutable carrier for data. The line of code you write replaces what would otherwise be a small wall of boilerplate.

What you get for free:

  • Final fields for each component (name, age), set in the constructor.
  • A canonical constructor taking all components, performing the assignments.
  • Accessor methods named after the components — name() and age(). Notice the naming: no getName(), no get prefix at all.
  • equals(), hashCode(), and toString() derived from all components. Two records are equal when all their components are equal; the string form shows the components.

There is no such thing as a setter — records are immutable by design, so the fields are final and the state is set once at construction. And records are implicitly final, so you cannot extend one. A subclass of a record is a compile error.

The interview answer: a record is a compact, immutable data holder that auto-generates the fields, constructor, accessors, and the standard Object methods — with no mutators and no inheritance allowed.

Answer:

The compiler automatically generates the final fields, a canonical constructor, accessor methods (name(), age()), and implementations of equals(), hashCode(), and toString().

A record is a transparent, immutable carrier for data. The line of code you write replaces what would otherwise be a small wall of boilerplate.

What you get for free:

  • Final fields for each component (name, age), set in the constructor.
  • A canonical constructor taking all components, performing the assignments.
  • Accessor methods named after the components — name() and age(). Notice the naming: no getName(), no get prefix at all.
  • equals(), hashCode(), and toString() derived from all components. Two records are equal when all their components are equal; the string form shows the components.

There is no such thing as a setter — records are immutable by design, so the fields are final and the state is set once at construction. And records are implicitly final, so you cannot extend one. A subclass of a record is a compile error.

The interview answer: a record is a compact, immutable data holder that auto-generates the fields, constructor, accessors, and the standard Object methods — with no mutators and no inheritance allowed.

5. What is the effect of declaring a parameter final in a method signature?

Answer: It prevents reassigning the parameter variable to another object inside the method body. It does not protect the object’s internals.

final on a parameter is a restriction on the variable, not on the object it references.

The parameter is a local variable initialized with the argument. Marking it final means that variable cannot be reassigned: param = newValue; inside the method body is now a compile-time error. The reference stays pinned to the original argument.

What final does not do is freeze the object. If the argument is mutable, you can still call methods that change its internal state — param.setName(...) is perfectly fine. final only blocks rebinding the reference.

The interview answer: final parameters are non-reassignable references. The referenced object remains fully mutable.

Answer:

It prevents reassigning the parameter variable to another object inside the method body. It does not protect the object’s internals.

final on a parameter is a restriction on the variable, not on the object it references.

The parameter is a local variable initialized with the argument. Marking it final means that variable cannot be reassigned: param = newValue; inside the method body is now a compile-time error. The reference stays pinned to the original argument.

What final does not do is freeze the object. If the argument is mutable, you can still call methods that change its internal state — param.setName(...) is perfectly fine. final only blocks rebinding the reference.

The interview answer: final parameters are non-reassignable references. The referenced object remains fully mutable.

6. Which keyword is used in Java 17+ to declare a sealed class with restricted inheritance?

Answer: sealed.

A sealed class controls who can extend it — the opposite of an open hierarchy. You declare it with sealed, and you list the allowed subclasses with permits:

public sealed class Shape permits Circle, Square, Triangle { ... }

Only Circle, Square, and Triangle may extend Shape — and each of those must itself be sealed, non-sealed, or final. A sealed class cannot be extended by anything not on the permits list.

Why does this matter? It gives you a closed, exhaustive set of types. Pattern matching (especially with switch expressions) can then reason that all cases are covered, which lets the compiler verify exhaustiveness and removes the need for a default branch.

The interview answer: sealed restricts inheritance; paired with permits to name the allowed subclasses. It’s the tool for closed hierarchies and exhaustive pattern matching.

Answer:

sealed.

A sealed class controls who can extend it — the opposite of an open hierarchy. You declare it with sealed, and you list the allowed subclasses with permits:

public sealed class Shape permits Circle, Square, Triangle { ... }

Only Circle, Square, and Triangle may extend Shape — and each of those must itself be sealed, non-sealed, or final. A sealed class cannot be extended by anything not on the permits list.

Why does this matter? It gives you a closed, exhaustive set of types. Pattern matching (especially with switch expressions) can then reason that all cases are covered, which lets the compiler verify exhaustiveness and removes the need for a default branch.

The interview answer: sealed restricts inheritance; paired with permits to name the allowed subclasses. It’s the tool for closed hierarchies and exhaustive pattern matching.

7. What will be the output of this pattern matching code (Java 17+)?

Object obj = "Hello";
if (obj instanceof String s) {
    System.out.println(s.toUpperCase());
}

Output: HELLO

Before Java 16, instanceof only answered yes or no. If you wanted to use the object as the matched type, you had to cast it yourself:

if (obj instanceof String) {
    String s = (String) obj;
    ...
}

Pattern matching for instanceof removes that boilerplate. The pattern String s does two things at once: it tests whether obj is a String, and — if so — binds obj to the new variable s, which is already typed as String. No explicit cast needed.

Here obj is "Hello", so the test passes, s is bound to the string, and s.toUpperCase() produces "HELLO", which is printed.

The scope of the pattern variable is important: s is in scope only where the pattern is guaranteed to have matched — inside the if block. The code compiles and runs without exception. Output: HELLO.

Answer:

HELLO

Before Java 16, instanceof only answered yes or no. If you wanted to use the object as the matched type, you had to cast it yourself:

if (obj instanceof String) {
    String s = (String) obj;
    ...
}

Pattern matching for instanceof removes that boilerplate. The pattern String s does two things at once: it tests whether obj is a String, and — if so — binds obj to the new variable s, which is already typed as String. No explicit cast needed.

Here obj is "Hello", so the test passes, s is bound to the string, and s.toUpperCase() produces "HELLO", which is printed.

The scope of the pattern variable is important: s is in scope only where the pattern is guaranteed to have matched — inside the if block. The code compiles and runs without exception. Output: HELLO.

8. 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.

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.

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.

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 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.

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.

11. 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.

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.

12. 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.

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.

My Private Notes

Notes are auto-saved locally to this device.