OOPs Interview Questions · 2026

OOPs Interview Questions (2026): 25 Most Asked, With Examples

Java still shows up in the day-to-day work of 29.6 percent of professional developers, according to the 2025 Stack Overflow survey (Stack Overflow, 2025), and every one of those developers learned the same four words before they wrote their first real class: encapsulation, inheritance, polymorphism, abstraction. Walk into a campus placement round at TCS, Infosys, Capgemini, or a mid-size product company hiring freshers in 2026, and OOP fundamentals aren't one topic among many, they're often the whole first technical round. Not the coding round, the concepts round.

Here's an opinion that might get pushback: I think most fresher prep spends too long memorizing the textbook one-liners ("abstraction hides complexity, encapsulation hides data") and not nearly enough time actually building a three-class hierarchy and watching where it breaks. Reciting a definition and defending a design decision under a follow-up question are different skills, and interviewers, especially ones two years into their own careers, can tell within thirty seconds which one they're getting.

This page covers OOP interview questions across nine areas: the four pillars, class versus object, method overloading versus overriding, abstract classes versus interfaces, composition versus inheritance, static versus instance members, constructors, the five SOLID principles, and coupling versus cohesion. Every code example here is Java, because that's still the default teaching and interview language for OOP in India, but none of this is Java-specific. The same four pillars show up in C++, C#, Kotlin, and Python, with different keywords and the occasional missing guardrail (Python doesn't really enforce access modifiers, it just asks nicely with an underscore).

52Questions
4 Pillars & SOLIDCore Topic
Concept + Java CodeFormat
Nearly UniversalFresher Rounds

The four pillars: encapsulation, inheritance, polymorphism, abstraction

Worth saying up front: "four pillars" isn't an official language spec term, it's a teaching shorthand, and different textbooks group these ideas slightly differently (Grady Booch's original OOAD framing leans on abstraction, encapsulation, modularity, and hierarchy instead). None of that matters in an interview. This is the vocabulary every interviewer uses, so it's the vocabulary to answer in.

Easy questions

15

Encapsulation bundles data and the methods that operate on it into one unit, restricting direct access to that data from outside. Inheritance lets one class acquire the fields and methods of another, modeling an "is-a" relationship. Polymorphism lets one method call behave differently depending on the actual object it's operating on. Abstraction hides implementation detail behind a simpler interface, exposing what an object does without forcing the caller to know how.

Interviewers rarely stop at the definitions. The follow-up is almost always "give me an example that isn't from a textbook," so have one ready from an actual project, even a small college one, rather than the usual Animal-Dog-Bark example everyone reaches for.

Abstraction is a design-level idea: it hides complexity by exposing only relevant behavior, usually through an interface or abstract class. Encapsulation is an implementation-level mechanism: it hides data by bundling fields with methods and restricting access with modifiers like private.

A shorter way to say it, and the phrasing that tends to land well: abstraction hides the what, encapsulation hides the how. A List interface abstracts away whether you're using an ArrayList or a LinkedList underneath. A private field with no public setter encapsulates that field's value so nothing outside the class can corrupt it directly. They usually show up together, but they're solving two different problems.

Inheritance lets a subclass reuse the fields and methods of a superclass and add or override its own. The IS-A test: only reach for inheritance when the subclass genuinely is a more specific version of the superclass. A Manager is-a Employee, so Manager extends Employee reads fine. A Car is not an Engine, it merely uses one, so Car extends Engine is the wrong tool even though it would compile.

Getting this test wrong is the single most common reason inheritance hierarchies rot two years into a codebase. Someone extends a class purely to reuse a method, the two classes drift apart conceptually, and every future change has to route around a relationship that never should have existed.

A class is a template that defines what fields and methods instances of it will have. An object is an actual instance of that class, allocated in memory, with its own copy of the instance fields defined by the class. One class, many objects, each with independent state.

In Java specifically, a class also gets loaded once by the classloader when it's first referenced, before any object of it exists. Objects come and go on the heap; the class definition itself sticks around in the JVM's metaspace for the life of the application.

Overloading is defining multiple methods with the same name in the same class, differing in the number, type, or order of parameters. Overriding is a subclass providing its own implementation of a method that's already defined in its superclass, with the exact same signature.

Overloading is about giving one name multiple entry points. Overriding is about one name meaning something different depending on which class's object is behind it.

A static variable belongs to the class itself, one copy shared across every object of that class. An instance variable belongs to each object separately, a fresh copy created every time you call new. Change a static variable through one object, and every other object sees the new value, because there's really only one variable to see.

A constructor is a special block with the same name as its class and no return type at all, not even void, that runs automatically when an object is created with new. Unlike a regular method, a constructor can't be called directly on an existing object, can't be inherited, and can't be overridden, though it can be overloaded with different parameter lists.

this refers to the current object instance the method was called on. It's implicit, every instance method effectively gets the calling object handed to it internally, that's literally spelled out in Python's self parameter, and it's a hidden reference in Java and C++ too.

You need it explicitly in a few common situations: when a constructor or setter parameter shares a name with a field (this.name = name to disambiguate), when you want to pass the current object as an argument to another method or constructor, and when you're writing a fluent API where each method returns this so calls can be chained. Static methods never have a this, because they aren't bound to any particular instance in the first place.

public means callable from anywhere. private means visible only inside the declaring class itself, not even subclasses. protected means visible within the same package plus any subclass, even one in a different package. Package-private, meaning no modifier at all, means visible only to other classes in the same package, regardless of whether they're related by inheritance.

Four levels exist because real codebases need an in-between option, not just fully open or fully closed. protected lets a base class expose extension points for subclasses to override or call, without making that method callable by an arbitrary unrelated class. Package-private supports encapsulating implementation details at the package boundary, which is exactly how a lot of internal library plumbing stays hidden from consumers while still being shared between cooperating classes in the same package.

Plain int constants give you no type safety. You can pass any int where a "status code" parameter is expected and it compiles fine even if the number is garbage, there's nothing stopping someone from passing 47 where only 0, 1, or 2 are meaningful. An enum defines a fixed, closed set of typed instances, the compiler rejects anything that isn't one of those instances, and each constant can carry its own fields and even override a method, so different enum values can behave differently without a switch statement scattered across the codebase.

At the implementation level, a Java enum is a real class, each constant is a singleton instance of it, and the compiler generates values() and valueOf() for free along with a private constructor that stops anyone from creating additional instances. Reach for plain int constants only when you're interoperating with something external, like a database column or a wire protocol that already defines numeric codes. Use enums for anything that lives entirely inside your own code.

== compares object references, meaning it checks whether two variables point at the exact same object in memory, or it compares raw values directly when used on primitives..equals() is a method call, and what it actually checks depends entirely on whether the class overrode it. Object's default equals() just falls back to ==, so a class that never overrides it will report two separately-built, identical-looking instances as not equal.

This trips people up most often with strings: two string variables built at runtime through concatenation can come back false with ==, while two compile-time string literals with the same text come back true, purely because of how the JVM interns literals. String, the wrapper classes, and record types override equals() to compare content instead of identity. As a rule, default to.equals() for content comparison and save == for identity checks or primitive comparisons.

A nested class is a class declared inside another class's body. The main reason to do this is to signal tight coupling, the inner class only makes sense in the context of its enclosing class and has no business being usable anywhere else, like a linked list's Node class or a custom iterator built specifically for one collection type.

Non-static inner classes hold an implicit reference back to the enclosing instance, so they can read its private fields directly without anything being passed in, that's how anonymous inner classes and event listener implementations get at the surrounding object's state. Static nested classes don't hold that back-reference, they're really just a namespacing tool, useful for something like a Builder class that logically belongs to the class it constructs without cluttering the top-level namespace with a separate file.

On a variable, final means the reference can only be assigned once, the variable can never be pointed at a different object afterward. That doesn't make the object itself immutable though, a final list can still have items added to or removed from it, only the variable's binding is locked, not the object's internal state.

On a method, final means no subclass can override it, which matters when overriding it would break an invariant the class depends on internally. On a class, final means nobody can extend it at all. String and the numeric wrapper classes are final for exactly that reason, letting them be subclassed would break assumptions the JVM and standard library rely on about their immutability and identity.

A marker interface declares zero methods, its entire job is to tag a class with a type so other code can check "is this an instance of the marker" and change behavior accordingly. Serializable is the classic example, it declares nothing, but implementing it tells ObjectOutputStream this particular class is allowed to be serialized, and the JVM checks for that marker before serialization proceeds. Cloneable works the same way for Object.clone().

Modern code tends to reach for annotations instead of marker interfaces for this kind of metadata, since annotations can carry parameters and don't force every marked class into an inheritance-like relationship. But marker interfaces still show up constantly in older Java APIs and in codebases that predate widespread annotation use, so it's worth recognizing the pattern even if you'd choose an annotation writing something new today.

A copy constructor takes an existing instance of the same class and builds a new object with matching field values, something like Point(Point other) that sets this.x = other.x and this.y = other.y. It matters the moment a field is a reference to a mutable object, because a plain field-by-field copy just copies the reference, not the object it points to, so the original and the copy end up sharing and mutating the exact same inner list or array.

Write your own copy constructor whenever the class holds mutable references and you actually want copies to be independent of one another. It's the same underlying issue as the shallow-versus-deep-copy problem with clone(), just framed as a constructor instead of a clone() override, and for most classes it's the simpler, more explicit fix since you write out exactly what gets copied field by field.

Medium questions

25

Polymorphism means the same method call produces different behavior depending on context. Compile-time polymorphism is method overloading: the compiler picks which version to run based on the argument types it sees while compiling, before the program ever executes. Runtime polymorphism is method overriding: the JVM picks which version to run based on the actual object's type at the moment the call happens, not the type of the variable holding it.

Overloading is really just the compiler doing lookup work early. Overriding is the JVM doing lookup work late, through what's usually called dynamic dispatch. They get taught side by side because the word "polymorphism" covers both, but mechanically they have almost nothing in common.

Yes, and I'd go further: a public getter and setter for every private field is often weaker encapsulation than no accessors at all, just with extra ceremony bolted on. If every field has a getter and a setter, you haven't hidden the internal representation, you've exposed it through two extra method calls instead of one field access.

java
public class BankAccount {
  private double balance;

  // No setBalance(). Behavior, not raw access.
  public void deposit(double amount) {
    if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
    balance += amount;
  }

  public void withdraw(double amount) {
    if (amount > balance) throw new IllegalStateException("Insufficient funds");
    balance -= amount;
  }
}

This is sometimes called "Tell, Don't Ask": tell the object what to do (deposit, withdraw) instead of asking for its data and mutating it from outside. BankAccount never exposes a raw setBalance(), so the invariant "balance can't go negative" lives in exactly one place and can't be bypassed. I don't think this rule applies everywhere, plain data-holder classes (DTOs, request bodies) are fine with generated getters and setters. It matters most for classes that actually enforce rules about their own state.

It breaks down at static members. A blueprint is inert, it doesn't do anything by itself, it just describes what gets built. But a Java class with a static field or static method has actual live state and behavior of its own, independent of any object ever being created from it. A static counter tracking "how many objects have been made" lives on the class, not on any instance, and you can call a static method without ever instantiating the class at all.

So "blueprint" is fine for a first-pass explanation, but if an interviewer pushes on it, the more accurate answer is that a class is both a template for creating objects and, separately, a namespace that can hold its own state.

Overloading resolution only needs to know the argument types you're passing, and those are known at compile time from the call site itself, so the compiler can pick the matching overload before the program ever runs. That's static binding.

Overriding resolution needs to know the actual object's runtime type, which isn't knowable until the program is executing and an object actually exists on the heap. The reference variable's declared type only determines which methods are visible to call; the JVM looks up the actual implementation through the object's own method table at the moment of the call. That's dynamic binding, and it's the mechanism that makes runtime polymorphism work at all.

No. Return type is not part of a method's signature for overload resolution purposes, only the method name and parameter list are. Two methods with identical parameter lists and different return types are a compile error, not a valid overload.

java
class Calculator {
  int compute(int a, int b) {
    return a + b;
  }

  // Compile error: compute(int, int) is already defined
  double compute(int a, int b) {
    return a + b;
  }
}

This trips people up because covariant return types are legal when overriding: a subclass overriding a method can narrow the return type to a more specific subtype. That's a different rule for a different mechanism, and interviewers ask this specifically to see if you confuse the two.

Reach for an abstract class when you're modeling closely related types that should share actual code rather than merely agree to a contract, and you need instance fields, constructors, or non-public members. Reach for an interface when unrelated classes need to agree to a common contract without sharing implementation, or when a class needs to honor more than one contract at once.

Oracle's own tutorial puts it plainly: consider abstract classes when you want to share code among closely related classes, and consider interfaces when unrelated classes would implement the same behavior (Java Tutorials, Abstract Methods and Classes). A Bird and a Plane can both implement a Flyable interface without being related at all. A Sparrow and an Eagle sharing a Bird abstract class makes sense because they're genuinely related and can share real code like wingBeat().

Default methods let an interface ship a method with an actual body, and any implementing class can use it as-is or override it. They exist mainly to solve a real evolution problem: before Java 8, adding a new method to an existing interface broke every class that implemented it. Adding stream() and forEach() to the Collection interface would have broken millions of lines of existing code without default methods to give those methods a fallback implementation.

My honest take: default methods are a good escape hatch for library maintainers and a bad habit for application code. If your own interface keeps growing default methods with real logic in it, that's usually a sign you actually wanted an abstract class and reached for an interface out of habit instead.

Because interfaces (pre-default-methods, at least) only supplied a contract, not state, there was never a conflict to resolve when a class agreed to multiple contracts at once. Classes can carry instance fields and constructors, so allowing a class to extend two classes reopens the classic diamond problem: if both parents define a field or a concrete method with the same name, which one wins?

Java sidesteps that entirely by allowing only single inheritance of implementation (one extends) alongside multiple inheritance of type (many implements). Default methods reintroduced a smaller version of the diamond problem for method bodies, which is why Java forces you to explicitly resolve it if two interfaces you implement both default the same method signature.

It means: when you need one class to reuse or extend another class's behavior, prefer holding a reference to that other class (has-a, composition) over extending it (is-a, inheritance), because composition keeps the relationship swappable and doesn't expose the internals of the class you're reusing.

It's not an absolute rule, and I don't think it should be treated like one. Inheritance is still the right tool when the IS-A relationship is genuinely true and stable, a small, closed hierarchy where every subclass really is substitutable for its parent. The advice is really a reaction to people overusing inheritance for code reuse alone, not a ban on ever using it.

No, not without an object reference. Static methods run without any particular object attached, there's no implicit "this" inside a static method, so there's nothing for an instance method call to be dispatched on. You have to either create a new object inside the static method or be handed a reference to one, then call the instance method through that reference explicitly.

An instance method, on the other hand, can call a static method freely, since it already has full access to the class the static method lives on.

A static initializer block, written as a bare static {... } block inside a class, runs exactly once, the first time the JVM loads that class, before any static field is accessed and before the first object is ever constructed. It's there to run setup logic that a simple field initializer can't express, filling a static lookup table from a file, for instance.

Order matters and it surprises people: static blocks run once at class-load time, ahead of everything else. Instance initializer blocks and constructors run every single time a new object is created, after the static setup is already long done. Create a hundred objects of the same class and the static block never runs again; the constructor runs a hundred times.

Constructor chaining is one constructor calling another, either this(...) to call a different constructor in the same class, or super(...) to call the parent class's constructor, so shared setup logic lives in one place instead of getting copy-pasted across every overload.

java
class Employee {
  String name;
  double salary;

  Employee() {
    this("Unknown", 0.0); // must be the first statement
  }

  Employee(String name, double salary) {
    this.name = name;
    this.salary = salary;
  }
}

class Manager extends Employee {
  Manager(String name, double salary) {
    super(name, salary); // runs Employee's constructor first
  }
}

Two rules trip people up here. First, this() or super() has to be the literal first statement in a constructor, you can't run any other code before it. Second, you can't call both in the same constructor, only one or the other. If you don't write a super() call at all, the compiler inserts an implicit no-arg one for you, which is exactly why a superclass with no no-arg constructor forces every subclass to write an explicit super(...) call, or the code won't compile at all.

Single Responsibility: a class should have one reason to change. A class that calculates an invoice total shouldn't also be the class that formats and prints it, those are two separate reasons to touch the code later. Open/Closed: a class should be open for extension but closed for modification, add new behavior with new code rather than editing tested code that already works. Liskov Substitution: a subclass should be usable anywhere its superclass is expected, without breaking correctness, the classic violation is a Square extending Rectangle and overriding setWidth so it also changes height, quietly breaking any code written against Rectangle's contract. Interface Segregation: don't force a class to implement methods it doesn't need, split one bloated interface into several small, specific ones. Dependency Inversion: high-level code should depend on an abstraction, not a concrete class, so a NotificationService depends on a MessageSender interface rather than being wired directly to an EmailSender.

I don't think fresher interviewers usually expect all five explained with this much precision on the spot. What they're actually screening for is whether you've internalized any of it beyond the acronym, one solid example beats five shaky one-liners every time.

Coupling measures how much one class depends on the internal details of another. Tight coupling means a class knows too much about a concrete class it depends on, so changing that dependency ripples outward and breaks things far away. Loose coupling means classes interact through an interface or abstraction instead, so one implementation can be swapped for another without touching the code that depends on it.

Cohesion is a separate axis: how focused a single class's own responsibilities are. A highly cohesive class does one well-defined job, which usually means it's also the Single Responsibility Principle in different words. A low-cohesion "God class" that handles database access, business rules, and formatting output all at once tends to also be tightly coupled to everything around it, because it has a reason to reach into every other part of the system. High cohesion and loose coupling aren't the same thing, but in practice, chasing one tends to improve the other.

The contract says objects that are equal according to equals() must return the same hashCode(). The reverse isn't required, two unequal objects are allowed to share a hash code, that's just a collision, but equal objects can never disagree on their hash. Override equals() to compare by value and forget hashCode(), and you get objects that are logically equal but land in different buckets inside a HashMap or HashSet.

The practical symptom: put(key, value) followed by get() using an equal-but-separately-constructed key silently returns null, or a Set that's supposed to reject duplicates ends up holding several "equal" entries because they hashed differently. I've seen this exact bug in a custom cache key class, someone added a field and updated equals() to include it, forgot hashCode(), and lookups started missing intermittently in a way that only showed up for keys that happened to hash into different buckets after the change. Most IDEs and record types generate both methods together for this reason, so if you're writing either one by hand, write both in the same change.

A shallow copy duplicates the object field by field, but if a field is a reference to another object, a list, an array, or a nested object, the copy gets the same reference, not a new object. A deep copy recursively duplicates every referenced object too, so the copy is fully independent all the way down.

Object.clone() does a shallow copy by default, and that's a common trap: you clone a Person object that holds a list of Address objects, the clone gets its own primitive fields but shares the exact same addresses list with the original, so calling clone.getAddresses().add(newAddress) silently mutates the original too. The fix is either overriding clone() to manually deep-copy the mutable fields, or skipping clone() entirely in favor of a copy constructor or static factory that explicitly builds new nested collections instead of assigning the old references directly. Deep copying gets genuinely hard once objects reference each other in cycles, which is why serialize-then-deserialize is sometimes used as a blunt but reliable deep-copy workaround for complex object graphs.

Both describe one object holding a reference to another, the difference is lifecycle ownership. In composition, the contained object's lifetime is tied to the container, a Car has an Engine, and when the Car is destroyed the Engine goes with it, since it was never shared with anything else. In aggregation, the contained object can outlive the container and can be shared elsewhere, a Department has Employees, but deleting the Department object doesn't delete the employees, they might belong to a different department tomorrow.

In code the distinction usually shows up in who creates the object. Composition typically means the container instantiates its own parts inside its constructor. Aggregation typically means the object is handed in from outside, an employee list passed to the Department rather than built by it. It's a design intent more than a language rule, most languages don't enforce it syntactically, but getting it right affects who owns null-checks and cleanup, and it maps directly onto whether a database foreign key should cascade a delete or not.

The default Object.toString() prints the fully qualified class name plus the hash code in hex, something like com.example.User@4554617c, which tells you nothing useful when you're debugging. Override it to return a readable summary of the object's meaningful state, and it pays off immediately, because string concatenation, println, and most log statements and debuggers call toString() implicitly, so a good override means you can log an object directly instead of manually picking out fields each time.

The common mistake is including sensitive fields, passwords, tokens, full personal data, in toString(), because it gets logged everywhere without anyone specifically intending to log that field. Another gotcha shows up with bidirectional relationships, if a parent's toString() prints its children and each child's toString() prints its parent back, you get infinite recursion and a stack overflow the first time anyone tries to log either object, which happens more often than expected with ORM entity classes.

Singleton guarantees a class has exactly one instance for the whole application, with one static access point to it, usually getInstance() backed by a private constructor so nothing outside can call new directly. The naive lazy version, check if the instance is null and create it inside getInstance(), works fine single-threaded but breaks under concurrency, two threads can both see null at the same time, both pass the check before either finishes construction, and you end up with two instances, which defeats the whole point.

The simplest fix that needs no locking at all is eager initialization, create the instance as a static field at class-load time instead of lazily, since the JVM classloader guarantees that happens exactly once and is inherently thread-safe. The tradeoff is paying the construction cost at startup even if the singleton never actually gets used, which matters when construction is expensive or has side effects, that's the case where you'd want lazy initialization done properly with real locking instead of the naive check.

Telescoping constructors is the pattern where you keep adding overloads to cover every combination of optional parameters, Pizza(size), Pizza(size, cheese), Pizza(size, cheese, pepperoni), and it gets unreadable fast past four or five optional fields. Callers have to remember positional argument order, and it's easy to accidentally pass one boolean where another was expected if the types happen to line up.

A Builder gives every optional field a named method, something like Pizza.builder().size(LARGE).cheese(true).build(), which reads clearly at the call site, lets you skip whatever isn't needed, and lets build() enforce invariants in one place, throwing if a required field was never set, instead of scattering that validation across a dozen constructor overloads. The real cost is the extra class and boilerplate, which is exactly what tools like Lombok's builder annotation exist to cut down. I'd reach for a builder once a constructor has more than three or four optional parameters, or once two parameters share a type and could be silently swapped by a caller without the compiler noticing.

A factory method centralizes object creation behind a method call instead of a constructor call, so the calling code depends on an interface or abstract type, not a concrete class. That matters the moment creation involves a decision, if the concrete implementation returned depends on a config value, an input type, or the runtime environment, a factory hides that branching in one place instead of scattering if/else chains full of concrete constructor calls across the codebase.

It matters for testing too, since swapping the factory to return a stub implementation doesn't require touching every call site that would otherwise be hardwired to a concrete constructor. The tradeoff is a layer of indirection, for a class with exactly one implementation and no real variability, a factory is pure ceremony, an extra method call just to reach new anyway. Introduce it once there's an actual reason for the abstraction, not as a default habit for every class you write.

The textbook one is Square extending Rectangle. Rectangle exposes setWidth() and setHeight() as independent operations, and code written against Rectangle reasonably assumes calling setWidth(5) only changes the width. Square overrides both setters so setting either dimension sets both, to keep it a square, which silently breaks that assumption, code that does rect.setWidth(5) then rect.setHeight(10) expecting a 5 by 10 rectangle gets a 10 by 10 square instead if a Square happens to be passed in.

It compiles perfectly and type-checks perfectly, and still violates LSP, because a Square isn't behaviorally substitutable for a Rectangle even though it's a perfectly valid IS-A relationship in plain English. A more common version in real code is a subclass override that throws a new checked exception the base method's contract never declared, or that narrows a precondition, the base method accepts null and the override throws on null, both of which compile cleanly but break calling code written trusting the base class's original contract.

ISP says clients shouldn't be forced to depend on methods they don't use, which in practice means favoring several small, focused interfaces over one interface that tries to cover every capability any implementer might ever need. The classic fat-interface example is a Worker interface with work() and eat() methods, then a Robot class has to implement Worker and now needs some throwaway implementation of eat(), an empty body or a thrown UnsupportedOperationException, purely to satisfy a method it has no business having.

That's the smell to watch for: an implementer forced to write dead code or a "not supported" exception just to compile. The fix is splitting Worker into Workable and Eatable, so Robot only implements Workable and never has to fake an eat() method. It matters beyond tidiness too, a fat interface couples every consumer to every method on it, so changing one unrelated method's signature can force a recompile and re-test of implementers that never call that method in the first place.

Dependency Inversion is a design principle, it says high-level modules shouldn't depend on low-level modules directly, both should depend on abstractions instead. Concretely, an OrderService shouldn't hold a hard reference to a concrete MySQLOrderRepository, it should depend on an OrderRepository interface, so the business logic doesn't get rewired every time a storage detail changes. Dependency injection is one specific mechanism for satisfying that principle, the technique of supplying an object's dependencies from the outside, constructor argument, setter, or a container like Spring, rather than the object constructing them itself.

You can follow DIP without any DI framework at all, by manually passing interface-typed dependencies through constructors, sometimes called poor man's DI. Conversely, you can misuse a DI framework and still violate DIP, if you inject a concrete class type instead of an interface, the framework is doing injection mechanically, but the code is still tightly coupled to one implementation, which defeats the point of the principle even though dependency injection, the mechanism, is technically in use.

A functional interface has exactly one abstract method, everything else on it can be a default or static method, but there's only one method left for a lambda expression to implement. That single-abstract-method constraint is what lets the compiler match a lambda to it, when you write (a, b) -> a + b, the compiler needs to know exactly which method signature the lambda stands in for, and it can only infer that unambiguously if there's one abstract method to bind to.

Runnable, Comparator, and the generic single-method Function interface are all functional interfaces for this reason, each has exactly one abstract method, run, compare, apply, even though Comparator has several default methods like thenComparing bolted onto it. The @FunctionalInterface annotation isn't required for any of this to work, it's a compile-time safety net, if someone later adds a second abstract method to the interface, the annotation turns that into a compile error instead of silently breaking every lambda that used to implement it.

Hard questions

12

No to both, and the reason is different for each. Private methods aren't inherited at all, so a subclass defining a method with the same signature isn't overriding anything, it's just an unrelated new method that happens to share a name. Static methods belong to the class, not to any instance, so there's no object to dispatch dynamically against, what looks like overriding a static method is actually method hiding, resolved by the reference's declared type at compile time.

java
class Parent {
  static void greet() {
    System.out.println("Parent static");
  }
}

class Child extends Parent {
  static void greet() {
    System.out.println("Child static");
  }
}

public class Main {
  public static void main(String[] args) {
    Parent p = new Child();
    p.greet(); // prints "Parent static", not "Child static"
  }
}

The reference type is Parent, so the compiler binds the call to Parent.greet() at compile time, regardless of what object p actually points to. Swap the reference type to Child and the exact same object prints something different. That's the tell that this is hiding, not overriding, real overriding would follow the object, not the variable.

A Car needing an engine is the textbook case, but it's textbook because it's genuinely clear: a Car is not a kind of Engine, it has one.

java
class Engine {
  void start() {
    System.out.println("Engine starting");
  }
}

class ElectricEngine extends Engine {
  @Override
  void start() {
    System.out.println("Electric engine starting silently");
  }
}

class Car {
  private final Engine engine; // has-a, not is-a

  Car(Engine engine) {
    this.engine = engine;
  }

  void start() {
    engine.start();
  }
}

Car never extends Engine. It holds one and delegates to it. Swapping a gas Engine for an ElectricEngine later means changing what gets passed into the constructor, nothing about Car's own class hierarchy changes, and nothing about Car's public API needs to change either. If Car had instead extended Engine, every quirk and every public method on Engine would leak straight into Car's interface whether Car wanted it or not.

Yes. A private constructor stops any code outside the class from creating an instance directly with new. It's most commonly used for the singleton pattern, where a class controls its own single instance and hands it out through a static method, or for a utility class made entirely of static methods (something like a Math-style helper class), where instantiating it at all doesn't make sense and a private constructor documents that intent to the compiler instead of just a comment.

The violation is an if/else chain keyed on a type string, where adding a new case means reopening and re-testing a method that already worked for every existing case.

java
// Before: violates OCP. Every new customer tier means editing this method.
double calculateDiscount(String customerType, double amount) {
  if (customerType.equals("REGULAR")) return amount * 0.05;
  else if (customerType.equals("PREMIUM")) return amount * 0.10;
  else if (customerType.equals("VIP")) return amount * 0.20;
  return 0;
}

// After: open for extension, closed for modification.
interface DiscountStrategy {
  double apply(double amount);
}

class VipDiscount implements DiscountStrategy {
  public double apply(double amount) {
    return amount * 0.20;
  }
}

Adding a new tier after the fix means writing one new class implementing DiscountStrategy, calculateDiscount itself never gets reopened, so nothing you already tested is put back at risk. The trade-off, worth admitting out loud in an interview: this pattern adds more files and more indirection for what might be three lines of logic. It earns its complexity once you've got five or six tiers and they keep growing, not necessarily on day one with two.

It's a compile error, not a runtime ambiguity, Java refuses to guess which default implementation should win. If InterfaceA and InterfaceB both declare a default method greet(), and MyClass implements both without overriding greet() itself, javac rejects the class with an error saying it inherits unrelated defaults for greet() from two different types. You resolve it by overriding the method in MyClass yourself, and inside that override you can reuse a specific parent's implementation with InterfaceA.super.greet() instead of writing new logic from scratch.

java
interface A { default String greet() { return "A"; } }
interface B { default String greet() { return "B"; } }
class C implements A, B {
  public String greet() { return A.super.greet() + B.super.greet(); }
}

The rule that saves you here is that a class's own method always wins over any inherited default, so the fix is always to override it yourself, the compiler will never silently pick one interface's default over another for you. That's exactly the design decision that avoids the murkier diamond inheritance ambiguity C++ has to resolve with virtual base classes.

The pattern checks if instance is null, and only synchronizes if it is, to avoid paying the lock cost on every call after the singleton is already built.

java
if (instance == null) {
  synchronized (Singleton.class) {
    if (instance == null) {
      instance = new Singleton();
    }
  }
}

Without volatile, this is broken under the Java Memory Model, and the reason is subtle. instance = new Singleton() isn't one atomic step, the JVM is allowed to reorder it into allocate memory, set the reference to point at that memory, then run the constructor body. If another thread reads the outer instance == null check between the second and third steps, it sees a non-null reference to an object whose constructor hasn't finished running, a partially initialized instance, and it happily returns that broken object instead of waiting.

Marking the field volatile establishes a happens-before relationship that forbids that reordering from being visible across threads, so any thread that sees a non-null instance is guaranteed to see the fully constructed object. This bit real production code for years before the memory model semantics around this were widely understood, which is part of why the initialization-on-demand holder idiom, a private static nested class the JVM classloader lazily loads and initializes exactly once, became the more commonly recommended alternative, since it sidesteps manual locking entirely by relying on classloader guarantees instead.

It's the situation where a change to a base class that looks completely safe in isolation breaks subclasses depending on implementation details the base class never explicitly promised to keep stable. A concrete example: a base Collection class implements addAll(items) by calling add() once per item, and a subclass overrides add() to also update a running count field. That works fine until someone optimizes the base class's addAll() to bulk-insert directly into the backing array instead of looping and calling add(), a change that's perfectly correct from the base class's own point of view.

Now the subclass's count silently stops updating, because the override it depended on is no longer being called, and nothing in either class's code looks wrong on review, the bug shows up as a wrong count somewhere downstream with no obvious cause. The real problem is the subclass was depending on an implementation detail, that addAll calls add internally, rather than a documented contract, and deep inheritance chains make this worse the more levels there are, since any class in the chain can break an assumption several levels down without knowing that assumption exists. It's one of the strongest practical arguments for composition over inheritance, and for documenting exactly which methods are safe to override and what they're allowed to assume about each other, rather than leaving that to be discovered by whoever eventually debugs the mismatch.

Most JVM and native implementations use a virtual method table, a per-class array of function pointers built once when the class is loaded. Every class has a vtable slot for each virtual method, and a subclass that overrides a method just replaces that slot's pointer with its own implementation's address, while any method it doesn't override keeps pointing at the parent's implementation. Every object carries a hidden pointer to its class's vtable, in the JVM that's part of the object header, in C++ it's a hidden vptr field sitting at the start of the object's memory layout.

When you call a virtual method through a base-typed reference, the runtime doesn't look at the compile-time type of the variable at all, it follows the object's vtable pointer, looks up the method's fixed slot index in that table, and jumps to whatever function pointer sits there, which is the subclass's override if one exists. That's why it's called dynamic dispatch, resolution happens at runtime based on the actual object, not the reference type, and it costs one extra pointer indirection compared to a direct, non-virtual call, which is the real performance cost of polymorphism that JIT compilers try to eliminate through devirtualization whenever they can prove only one concrete type ever shows up at a given call site.

Interface-based polymorphism checks the contract at compile time, if a class claims to implement PaymentProcessor, the compiler verifies every method that interface promises actually exists with a matching signature before the program ever runs, so a caller invoking processor.charge(amount) has a compile-time guarantee that method exists on whatever gets passed in. Duck typing checks nothing up front, any object that happens to have a charge(amount) method works, whether or not it declares any relationship to a PaymentProcessor concept at all, if it walks like a duck and quacks like a duck.

The failure mode duck typing introduces is that the check for "does this object support charge()" only happens the moment the call actually executes at runtime, so a typo in a method name, or an object missing that method entirely, doesn't surface until that exact code path runs, potentially deep inside a rarely hit branch, instead of failing the build. What duck typing buys you in exchange is genuine structural flexibility, you can pass any object satisfying the shape without it ever declaring an interface or inheriting from anything, which is why codebases built around it lean hard on unit tests and, in larger Python projects, on structural typing tools that check the shape statically to get some of the compile-time guarantee back without giving up that flexibility.

When you pass or assign a Derived object into something typed as Base by value, not by reference or pointer, the compiler only copies the Base portion of the object's memory layout, the extra fields Derived added get truncated off, sliced away. A function taking Base by value, called with a Derived instance, ends up operating on a plain Base copy inside the function, any Derived-specific state is gone, and any virtual method called on it dispatches using Base's vtable, not Derived's, because the object genuinely is a Base now, not a Derived object being treated as one.

cpp
void process(Base b) { b.speak(); } // slices, always calls Base::speak
void process(Base& b) { b.speak(); } // no slice, calls Derived::speak if applicable

The fix is to always pass polymorphic types by reference or pointer, never by value, which is exactly why most C++ style guides ban by-value parameters and return types for any class meant to be used polymorphically. Java and most managed languages don't have this problem at all, since objects there are always accessed through references, there's no such thing as an object's value living directly on the stack that a copy could truncate, worth knowing if you're explaining to someone coming from Java why this specific bug class simply doesn't exist for them.

Every serializable class gets a serialVersionUID, either declared explicitly or computed automatically by hashing the class's structure, its fields, methods, and interfaces, if you don't declare one yourself. Deserialization checks that the UID in the byte stream matches the UID of the class currently loaded in the JVM, and if you never declared one explicitly, adding, removing, or even reordering a field changes the computed hash, which means the UID silently shifts too, and every previously serialized object on disk becomes instantly unreadable with an InvalidClassException the moment the new class version gets deployed.

The fix is to explicitly declare a fixed serialVersionUID on any class meant to be serialized long-term, so the UID doesn't move under you when the class shape changes. With an explicit UID, adding a new field is generally safe, it deserializes as null or its default value on old data. Removing a field is the risky direction, old serialized bytes still contain that field's data in the stream, and depending on the reader it either gets silently discarded or causes a mismatch. Most teams that rely on Java serialization for anything persisted long-term end up writing custom readObject and writeObject methods to handle field migrations explicitly, or move off native serialization entirely to something like Protocol Buffers or Avro, which were built from the start with schema evolution as a first-class concern.

This escaping means a reference to the object under construction gets published somewhere else, stored in another object's field, registered as a listener, handed to a background thread, before the constructor has finished running. The danger is that other code can now see and use an object that isn't fully initialized, its fields might still sit at their default zero or null values because the constructor hasn't gotten around to setting them yet.

java
class EventSource {
  public EventSource(EventBus bus) {
    bus.register(this); // this escapes here, before construction finishes
    this.name = computeName(); // runs after registration
  }
}

If bus.register() immediately fires an event on another thread, or even synchronously calls back into the new object before the constructor's second line runs, whatever reads this.name gets null instead of the real value, and there's no compiler warning anywhere, it's a pure ordering bug. It gets worse across threads even without an obvious callback, the Java Memory Model doesn't guarantee another thread sees a fully constructed object just because the reference is non-null, so without proper synchronization a second thread can observe partially initialized field values even after the constructor has returned on the first thread, the same underlying issue behind the double-checked-locking bug. The fix is to never publish this inside a constructor, finish construction fully first, then register or hand off the reference afterward, often through a static factory method that builds the object, registers it, and only then returns it.

How to prepare for an OOP interview in 2026

Skip the flashcard pass on definitions alone. Most OOP interview questions test whether you can defend a design choice, not recite one. Build one small hierarchy, three or four classes deep, with at least one abstract class, one interface, and one place where you deliberately chose composition over extending something. Then break it on purpose: override a method wrong, make a constructor private and see what stops compiling, remove a super() call and read the actual compiler error instead of guessing what it says. Fixing your own mistakes teaches the mental model faster than reading someone else's explanation of it.

Across mock interviews run through LastRoundAI tagged fresher or campus placement, the overloading-versus-overriding pair and the private-constructor question trip up more candidates than the four pillars ever do, even though the pillars get the overwhelming majority of prep time. My guess is that pillar definitions feel safe to memorize, while the static-versus-dynamic-binding mechanics under overloading and overriding require actually reasoning about what the compiler and the JVM are each doing, at different times, which is a harder thing to fake. I don't have a clean pass-rate number to put on that pattern, only that it shows up often enough across sessions to be worth flagging here.

Get quizzed on your reasoning before an interviewer does

Reading a definition out loud is not the same as holding up under a follow-up question that changes one assumption on you, what if that field weren't private, what if you extended instead of composed. LastRoundAI's mock interview mode runs live rounds with real-time follow-ups in your browser, and the free plan includes 15 credits a month that reset monthly rather than piling up unused. Starter is $19/mo if fifteen sessions a month runs out before your placement season does.

If getting in front of enough companies is the harder part of the process right now, rather than passing the round once you're in one, Auto-Apply queues tailored applications to fresher and entry-level roles for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and every application waits for your approval before anything actually goes out.

Questions about either product go to contact@lastroundai.com, that's the only inbox we check.

How this list was built

Worth being straight about where these questions come from, because plenty of pages in this category are not. The set was compiled from a research pass across official documentation, vendor release notes, published engineering writing and public discussion of hiring processes, then cross-checked against the current version of each technology so nothing here describes behaviour that has since changed.

What that means in practice: these are the questions the material supports as reasonable and current for this role, not a transcript of any one company's loop. We have not sat in on your interview and we are not going to claim we have. Treat the list as well-sourced preparation rather than a leaked question bank, and expect your panel to phrase things their own way.

If you spot something out of date, tell us at contact@lastroundai.com and we will fix it.

Frequently asked questions

Is OOP still worth learning in 2026?

For interview purposes the question is really whether the teams you are targeting use it, which is worth checking against their actual job postings rather than general popularity rankings. Where it is in use it tends to be deeply embedded and slow to replace.

Should I memorise OOP syntax for the interview?

Rarely worth it. Most interviewers care that you know what to reach for and why, and will not fail you for forgetting an exact flag. Being confidently wrong about behaviour costs far more than admitting you would check the documentation.

What is the most common mistake in OOP interviews?

Answering the question that was asked and stopping there. The strongest candidates add the trade-off or the failure mode without being prompted, which is what signals real use rather than revision.

How long does it take to prepare for a OOP interview?

If you already work with OOP day to day, a focused week on the areas you avoid in practice is usually enough. Coming in cold, expect three to four weeks. The gap is rarely knowledge; it is being able to explain something you normally just use.

What OOP topics come up most often?

Interviewers concentrate on the parts that cause production incidents rather than the parts that are pleasant to learn. Expect the fundamentals to be assumed and the follow-up questions to sit one layer below what a tutorial covers.

Leave a Reply

Your email address will not be published. Required fields are marked *