inheritance
extends, super, and how a subclass object is laid out in memory.
- chapter
- chapter 03.06
- reading time
- 7 min read
class Dog extends Animal makes every Dog also an Animal: it inherits Animal's fields and methods, and a Dog object physically contains Animal's fields. methods can be overridden (the object's class decides which runs). fields can only be hidden (the variable's type decides which you get). constructors aren't inherited at all, they chain.
the picture
think of a form with a stapled extension. the base form (Animal) has name and legs. the dog extension adds breed and good-boy status. a filled-in dog form is the base form plus the extension, stapled together. anyone who only knows how to read base forms can still read the first page.
the Animal part sits at the same offsets it has in a plain Animal. so code compiled against Animal reads legs from a Dog without knowing it's a Dog.
new Animal() and new Dog(): the Animal part doesn't move. purple is the JVM's header, blue are references. sizes measured on jdk 25.that "same offsets" detail is the trick that makes inheritance cheap. a method compiled for Animal reads legs at a fixed position. hand it a Dog and the legs is right where it expects. no conversion, no copy. that's why a Dog can go anywhere an Animal is expected.
what you get, and what you don't
| member of Animal | in Dog? |
|---|---|
public / protected fields and methods | inherited, usable |
| package-private members | inherited, usable only if Dog is in the same package |
private fields | in every Dog object, but Dog's code can't name them |
private methods | not inherited (Dog can declare one with the same name, unrelated) |
| constructors | never inherited. Dog's constructors call them via super(...) |
static members | reachable through Dog too, but hidden, not overridden |
overriding and super
a subclass can replace an inherited method by declaring one with the same signature. super.method() calls the parent's version from inside the override, which is how you extend behaviour instead of replacing it.
class Animal {
final String name;
Animal(String name) {
this.name = name;
}
String describe() {
return "an animal named " + name;
}
}
class Dog extends Animal {
final String breed;
Dog(String name, String breed) {
super(name); // constructors chain, they aren't inherited
this.breed = breed;
}
@Override
String describe() {
return super.describe() + ", who is a good " + breed;
}
}
void main() {
Animal plain = new Animal("rex");
Animal dog = new Dog("rex", "corgi");
IO.println(plain.describe());
IO.println(dog.describe());
}an animal named rex an animal named rex, who is a good corgi
look at the last line: dog is declared as Animal, and the Dog version ran anyway. the variable's type says what you're allowed to call. the object's class decides which code runs. that's polymorphism, and it gets its own chapter. the constructor chain gets one too: constructors and init order.
fields don't override. they hide.
here's the surprise. declare a field in Dog with the same name as one in Animal, and you don't replace it. you get two fields, both in the object.
class Animal {
String name = "animal";
String name() { return "animal"; }
}
class Dog extends Animal {
String name = "dog";
@Override String name() { return "dog"; }
}
void main() {
Animal a = new Dog();
IO.println(a.name + " " + a.name());
}Dog, two references to it with different declared types, and two fields called name inside it. a.name picks Animal's slot because a is declared as Animal. d.name picks Dog's.class Animal {
String name = "animal";
String name() { return "animal"; }
}
class Dog extends Animal {
String name = "dog";
@Override String name() { return "dog"; }
}
void main() {
Animal a = new Dog();
Dog d = (Dog) a;
IO.println(a.name + " " + a.name());
IO.println(d.name + " " + d.name());
IO.println(((Animal) d).name); // a cast is enough to switch fields
}animal dog dog dog animal
the lesson: never reuse a field name in a subclass. better yet, keep fields private and you physically can't.
casting up and down
going up the tree (Animal a = dog) is always safe, so it's implicit. going down (Dog d = (Dog) a) is a promise that javac can't check, so the JVM checks it at runtime and throws ClassCastException if you lied. the modern way is to test and cast in one step:
if (a instanceof Dog d) { // pattern matching, java 16+
IO.println(d.breed);
}one parent, and a root for everyone
java classes have exactly one direct superclass. leave out extends and you get Object. so every class sits in one tree with Object at the top, and every object has toString(), equals() and hashCode().
- the root. 11 methods, no fields
- ├── abstract
- │ ├── Integerfinal
- │ └── Longfinal
- ├── Stringfinal: can't be extended
- ├──
- │ └──
- │ └── ArrayList
- └── yours
- ├── Dog
- └── Cat
final on a class stops the tree: String, Integer and every record are final. final on a method stops overriding just that method. both are how a class says "i wasn't designed to be extended, and i'm not pretending".
- a subclass object contains every field of its superclasses, private ones included.
- the parent's fields keep their positions, so a Dog works anywhere an Animal does.
- methods override: the object decides. fields hide: the variable's type decides.
- constructors chain with
super(...). they're never inherited. - always
@Override. never reuse a field name.