A guide to inheritance in Java
How extends works, why Java allows one superclass but many interfaces, what is and isn't inherited, field hiding versus overriding, object layout and vtables, and the fragile base class problem.
On this page
🧬 What is inheritance?
Inheritance lets one class reuse and specialise another class by declaring class CardPayment extends Payment. The subclass gets every accessible field and method of the parent, can add its own, and can replace (override) instance methods. A CardPayment object is a Payment, so you can pass it anywhere a Payment is expected, for example into checkout(Payment p).
Every candidate can write extends. Interviewers use inheritance to find out whether you know what actually happens: why Java allows one superclass but many interfaces, why a field declared twice gives you two fields, why private members are "not inherited" but still take up memory in the object, and how the JVM finds the right method in a few nanoseconds. Then comes the design question that separates seniors: when should a class not be extendable at all?
What you’ll learn
- Why Java has single class inheritance and how interfaces avoid the diamond problem, including
X.super.m()for clashing defaults - Exactly what is inherited: accessible members yes; constructors, initializers and
privatemembers no (thoughprivatefields still live in the object) - Why fields and
staticmethods are hidden while instance methods are overridden - How a subclass object is laid out in memory (parent fields first) and how
invokevirtualuses a vtable - The fragile base class problem and when to make a class
final,sealedor designed for extension
For this lesson, we'll focus on extends and the single-inheritance rule, the diamond problem and how interfaces dodge it, exactly what is and isn't inherited, Object as the root, field hiding versus method overriding, how a subclass object is laid out in memory, vtables, and the fragile base class problem.
Below the line (out of scope)
- The
superkeyword and constructor chaining: covered in The super keyword in Java - The full overriding rules, bridge methods and overload resolution: covered in Method overloading and overriding
- Abstract classes, interfaces and sealed hierarchies: covered in Abstract classes, Interfaces and Sealed classes
- When to prefer composition: covered in Inheritance vs composition
The 30-second answer
A class extends exactly one superclass (Object if you write nothing) and can implement any number of interfaces. Java forbids multiple class inheritance to avoid the diamond problem: two parents with state and conflicting method bodies. The subclass inherits accessible fields and methods, but not constructors, and private members are present in the object yet invisible to the subclass. Instance methods are overridden and dispatched on the runtime class through a vtable; fields and static methods are only hidden and resolved on the compile-time type. Because subclasses depend on the parent's internals, I either design a class for inheritance and document it, or make it final.
Interview radar
Asked very often: Why does Java allow one superclass but many interfaces, and exactly what does a subclass inherit?
Senior rounds probe: How does invokevirtual find an overridden method through the vtable, and why are fields and static methods hidden rather than overridden?
Production angle: A base-class change silently broke every subclass in a shared library, the classic fragile base class incident that argues for final, sealed, or composition.
01Picture it first: a franchise and its operations manual
Think of a restaurant franchise. Head office writes the operations manual: how to take an order, how to handle refunds, how to close the till. A franchise outlet gets the whole manual on day one, adds local pages (a regional menu), and is allowed to rewrite some pages (its own refund policy). It cannot read head office's safe (private), it cannot rewrite pages stamped "do not modify" (final), and it can only sign a contract with one head office, even though it can hold many certifications (food safety, card payments), which are the interfaces.
Here's how that maps to a payments hierarchy. Object sits at the root, each class has exactly one parent, and interfaces sit to the side, adding capabilities without adding a second parent.
A class hierarchy with Object at the root, Payment extending Object, CardPayment and UpiPayment extending Payment, and CardPayment also implementing the Refundable and Auditable interfaces
Notice two things. The solid arrows (one per class) form a tree, never a graph, so every class has a single, unambiguous chain up to Object. The dashed arrows to interfaces can fan out, because interfaces bring method contracts, not instance fields.
02How inheritance works under the hood
Try it yourself first
Before reading on, predict the output of main. CardPayment adds a field, overrides one method and inherits another.
class Payment {
protected final long amountPaise;
private String status = "NEW";
Payment(long amountPaise) { this.amountPaise = amountPaise; }
long fee() { return 0; }
String status() { return status; }
String summary() { return getClass().getSimpleName() + " " + amountPaise + " fee=" + fee(); }
}
class CardPayment extends Payment {
private final String last4;
CardPayment(long amountPaise, String last4) {
super(amountPaise);
this.last4 = last4;
}
@Override
long fee() { return amountPaise * 2 / 100; } // 2% card fee
}
public class Checkout {
public static void main(String[] args) {
Payment p = new CardPayment(50_000, "4242");
System.out.println(p.summary()); // prints: CardPayment 50000 fee=1000
System.out.println(p.status()); // prints: NEW
}
}
Notice three things:
summary()lives inPayment, yet its call tofee()runsCardPayment.fee(). Inherited code calls back into the subclass, because instance method calls are dispatched on the object's runtime class, not on the class where the call is written.statusisprivatetoPayment.CardPaymentcan't name it, but everyCardPaymentobject still carries that field, and the inheritedstatus()method reads it.- The constructor isn't inherited.
CardPaymentmust declare its own and chain toPayment(long)withsuper(...).
What is and isn't inherited
This table is worth memorising, because interviewers love the "is a private field inherited?" trap.
| Member | Inherited? | What really happens |
|---|---|---|
public / protected fields and methods |
Yes | Usable in the subclass as if declared there |
| Package-private members | Only if the subclass is in the same package | From another package they're invisible, and a same-signature method there is a new method |
private fields |
No (not a member of the subclass) | Still physically present in every subclass object |
private methods |
No | A same-signature method in the subclass is unrelated, not an override |
| Constructors and initializer blocks | No | Each class declares its own; they run in a chain |
static fields and methods |
Accessible through the subclass name | Hidden, never overridden; resolved at compile time |
JLS §8.2 states it precisely: members of a class are the inherited members plus the declared ones, and constructors, static initializers and instance initializers are not members. "Not inherited" is about visibility and naming, not about memory.
One parent class, many interfaces: the diamond problem
Imagine Java allowed class Wallet extends CardPayment, UpiPayment. Both parents extend Payment, so the diagram forms a diamond. Two questions have no good answer: does a Wallet contain one copy or two copies of Payment's amountPaise, and if both parents override fee(), which body runs? C++ answers with virtual inheritance and explicit qualification, which is exactly the complexity Java's designers chose to avoid.
The diagram below contrasts the forbidden class diamond, where Payment's state and fee() body would arrive by two paths, with the interface version that keeps a single state spine and only needs a method tie-break.
A comparison showing a forbidden class diamond where Wallet inherits Payment twice through CardPayment and UpiPayment, beside an allowed version where Wallet extends one class and implements two stateless interfaces
Interfaces don't have instance fields, so the state half of the diamond disappears. Since Java 8 they can have default method bodies, so the behaviour half can still clash, and the compiler forces you to resolve it:
interface Refundable { default String policy() { return "refund within 7 days"; } }
interface Auditable { default String policy() { return "keep records 8 years"; } }
class CardPayment extends Payment implements Refundable, Auditable {
CardPayment(long amountPaise) { super(amountPaise); }
// Without this override: "types Refundable and Auditable are incompatible;
// class CardPayment inherits unrelated defaults for policy()"
@Override
public String policy() {
return Refundable.super.policy() + "; " + Auditable.super.policy();
}
}
The resolution rules are short. A class always wins over an interface default (a method inherited from a superclass beats any default). Among interfaces, the more specific one wins (if Auditable extends Refundable, Auditable's default is chosen). If neither rule decides, you must override, and X.super.method() lets you call a specific parent default, which we cover in the super keyword.
Object is the root of everything
A class with no extends clause implicitly extends java.lang.Object. Arrays are objects too: int[] is a subtype of Object, Cloneable and Serializable. That's why every reference type has equals, hashCode, toString, getClass, wait/notify/notifyAll, clone (protected) and finalize (deprecated for removal since Java 18, JEP 421). Object is the only class with no superclass: Object.class.getSuperclass() returns null.
Fields are hidden, methods are overridden
Try it yourself first
Both classes declare a field called channel. What do the two lines print?
class Payment { String channel = "generic"; String channel() { return channel; } }
class UpiPayment extends Payment { String channel = "upi"; @Override String channel() { return channel; } }
Payment p = new UpiPayment();
System.out.println(p.channel); // prints: generic
System.out.println(p.channel()); // prints: upi
A UpiPayment object now has two channel fields: Payment.channel and UpiPayment.channel. Field access is decided by the compiler using the static type of the reference (Payment), so p.channel reads the parent's slot. The method call is decided at runtime by the object's class, so p.channel() runs the override, which reads the subclass's field. Polymorphism applies to instance methods only. Field hiding is legal but almost always a bug; keep fields private and the problem can't arise.
How a subclass object is laid out in memory
When you write new CardPayment(...), the JVM allocates one object, not a Payment object plus a CardPayment object. Its memory starts with a header, then the fields declared in Payment, then the fields declared in CardPayment. The figure below shows a typical layout on a 64-bit HotSpot JVM with compressed pointers, where the header is 12 bytes: an 8-byte mark word (hash, lock and GC bits) and a 4-byte class pointer.
Memory layout of one CardPayment object: a 12-byte header, then Payment's fields at fixed offsets, then CardPayment's fields, with the class pointer leading to a vtable whose fee slot points to CardPayment.fee
Notice why parent fields come first. Code compiled against Payment reads amountPaise at offset 16. Because every subclass keeps the parent's fields at the same offsets, that code works unchanged for a CardPayment, a UpiPayment or a subclass written next year. Since JDK 15 the layout engine may tuck small subclass fields into gaps the parent left, but it never moves a parent field. Exact offsets vary with flags, so treat the numbers as illustrative and check real ones with JOL (Java Object Layout).
vtables: how the JVM picks the method
Each loaded class has a virtual method table (vtable) hanging off its class metadata. A subclass starts with a copy of its parent's table, replaces the slots it overrides and appends slots for new methods. So fee() has the same slot index in Payment, CardPayment and UpiPayment; only the pointer in the slot differs.
A call p.fee() compiles to invokevirtual Payment.fee:()J. At runtime the JVM follows the object's class pointer, indexes into that class's vtable at fee's slot, and jumps. That's two dependent loads and an indirect call, before any JIT optimisation. Interface calls use a separate itable, because one class can implement many interfaces and the slot numbers can't be shared across unrelated hierarchies. We'll see in overloading and overriding how the JIT usually removes even those loads.
03Real world: the fragile base class problem
Try it yourself first
A teammate extends the shared Cart class to count how many items were ever added. What does added() return after addAll with three SKUs?
class Cart {
private final List<String> items = new ArrayList<>();
public void add(String sku) { items.add(sku); }
public void addAll(Collection<String> skus) { for (String sku : skus) add(sku); }
public int size() { return items.size(); }
}
class AuditedCart extends Cart {
private int added;
@Override public void add(String sku) { added++; super.add(sku); }
@Override public void addAll(Collection<String> skus) { added += skus.size(); super.addAll(skus); }
int added() { return added; }
}
AuditedCart cart = new AuditedCart();
cart.addAll(List.of("SKU-1", "SKU-2", "SKU-3"));
System.out.println(cart.added()); // prints: 6
It prints 6, not 3. Cart.addAll calls add internally (its self-use), and that call dispatches to the override, so each item is counted twice. Worse, the subclass is now coupled to an implementation detail. If the Cart maintainer later rewrites addAll to call items.addAll(skus) directly, the count silently becomes correct, and if AuditedCart had been "fixed" by removing its addAll override, it silently becomes wrong. Nobody changed AuditedCart, yet its behaviour changed. That's the fragile base class problem: a subclass depends on how the parent is written, not only on what it promises.
Spring creates CGLIB proxies by subclassing your bean at runtime and overriding its methods to add transactions, caching or security. If you mark a @Transactional method final, the proxy can't override it, so calls run without a transaction and with no error at startup. Worse, the call executes on the proxy instance, whose own fields were never initialised, so injected dependencies read as null. Kotlin classes are final by default, which is why Spring projects need the kotlin-spring (all-open) compiler plugin. Inheritance is part of how frameworks work, so know which of your classes they extend.
The fix in your own code is a design decision, which Effective Java (Item 19) states as design and document for inheritance or else prohibit it:
- Prohibit it with
final class, or aprivateconstructor plus static factories. This is my default for value classes, services and utilities.String,IntegerandLocalDateare allfinal. - Restrict it with a
sealedclass that permits a known list of subclasses (Sealed classes, Java 17). - Design for it when extension is the point: document self-use with
@implSpec("this implementation callsaddfor each element"), expose deliberateprotectedhooks, make everything elseprivateorfinal, and never call an overridable method from a constructor. - Prefer composition when you want to reuse behaviour you don't own: wrap a
Cartinstead of extending it, and forward calls. We compare the two in Inheritance vs composition.
04Advanced: the edge cases interviewers probe
Static methods are hidden, not overridden. A static method with the same signature in a subclass hides the parent's. The call is bound at compile time to the static type, even when you call it through an instance:
class Payment { static String gateway() { return "default-gateway"; } }
class UpiPayment extends Payment { static String gateway() { return "npci"; } }
Payment p = new UpiPayment();
System.out.println(p.gateway()); // prints: default-gateway (IDEs flag static access via an instance)
System.out.println(UpiPayment.gateway()); // prints: npci
The bytecode is invokestatic Payment.gateway; the object in p is never consulted. @Override on a static method is a compile error, and an instance method can't override a static one or vice versa.
Calling an overridable method from a constructor. The parent constructor runs before the subclass's field initializers, so an override called from it sees default values (null, 0). We walk through the exact order in the super keyword, and it's one of the puzzles on the interview questions page.
Inheritance and access. An override can widen access (protected to public) but never narrow it, and a package-private method can't be overridden from another package, as covered in access modifiers.
Many candidates answer "private fields are not inherited, so the subclass object doesn't have them". The first half is right, the conclusion is wrong. A CardPayment object contains Payment's private String status, takes the memory for it, and inherited public methods read and write it. "Not inherited" means the subclass can't name the member. If you say that precisely, and mention that getDeclaredFields() on the subclass won't list it while the object still holds it, you've turned a trap into a point scored.
invokevirtual through a vtable costs two dependent loads plus an indirect branch, roughly 1 to 3 ns on a modern CPU if the branch predicts well. In practice the JIT does better: class hierarchy analysis (CHA) notices when only one loaded class implements fee() and calls it directly, even inlining it, with no final keyword needed. If a new subclass is loaded later, HotSpot deoptimises the affected compiled code. So mark classes final for design reasons, not speed; the JIT rarely needs the hint.
Here's how inheritance compares with the alternatives when you're designing:
Class inheritance (extends) |
Interface (implements) |
Composition (a field) | |
|---|---|---|---|
| How many | One parent | Many | Any number |
| Brings instance state | Yes | No (constants only) | Yes, encapsulated |
| Coupling | To parent's implementation | To a contract | To the wrapped type's public API |
| Polymorphism | Yes | Yes | Only if you also implement an interface |
| Typical use | A real "is-a" with designed hooks | Capabilities (Comparable, Refundable) |
Reusing behaviour you don't own |
My default: interfaces for types, composition for reuse, and extends only for a genuine is-a relationship in code my team controls, with the parent designed for it.
05How to say it in the interview
When I ask about inheritance, I'm listening for three things. First, the precise rules: one superclass, many interfaces, constructors and private members not inherited, static and fields hidden rather than overridden. Second, the mechanics: one object with parent fields first, and dispatch on the runtime class through a vtable. Third, an opinion: when you'd make a class final, and why composition is often safer.
In the interview
"In Java a class extends exactly one superclass, Object by default, and can implement many interfaces. Multiple class inheritance is banned because of the diamond: two parents with state and different method bodies. Interfaces have no instance state, and if two defaults clash, the compiler makes me override and pick one with X.super.m(). The subclass inherits accessible members but not constructors; private fields aren't inherited by name but are still inside the object. At runtime there's one object with the parent's fields first at fixed offsets, and instance calls go through a vtable indexed by the method's slot, so the runtime class decides. Fields and static methods are hidden, resolved by the static type. Because subclasses depend on the parent's self-use, the fragile base class problem, I design and document classes for inheritance or make them final, and I reach for composition when I'm reusing code I don't own."
On the whiteboard
class C extends P implements I1, I2 // 1 class, n interfaces
no extends → extends Object (arrays too)
diamond: classes ✗ (state + bodies), interfaces ✓
default clash → must override, I1.super.m()
rule: class wins > more specific interface > override
inherited: accessible fields/methods
NOT inherited: ctors, initializers, private (still in object!)
fields + static methods: hidden, static type decides
instance methods: overridden, runtime class decides
object: [header 12B][P fields][C fields] parent offsets fixed
invokevirtual → klass → vtable[slot] → code
fragile base class → final | sealed | document @implSpec | compose
Further reading (8)
- JLS §8.1.4 Superclasses and Subclasses
- JLS §8.2 Class Members
- JLS §8.4.8 Inheritance, Overriding, and Hiding
- JVMS §6.5 invokevirtual
- The Java Tutorials: Inheritance
- Effective Java, 3rd ed., Items 18 and 19: Favor composition; design and document for inheritance or else prohibit it
- OpenJDK JOL (Java Object Layout)
- Baeldung: Guide to Inheritance in Java