Inheritance was the first OOP topic where I could write the code but couldn't explain the "why". I knew extends made one class borrow from another. But then more questions piled up. Why can a class extend only one class? What is this mysterious Object class that everything seems to come from? What's the difference between super() and this()? And when is it better to use inheritance, and when is it better to just put one object inside another?
In this post, we'll answer all of that in one place. I've drawn the important pictures in text, so you can see the structures instead of just reading about them.
Unless I show a full class, the snippets go inside a main method.
What is inheritance?
Inheritance lets one class build on another. The class you build on is the parent (also called the superclass). The new class is the child (also called the subclass). You connect them with the keyword extends.
The child automatically gets the parent's fields and methods, and can add its own.
class Animal {
String name;
void eat() {
System.out.println(name + " is eating");
}
}
class Dog extends Animal {
void bark() {
System.out.println(name + " says Woof!");
}
}
Dog d = new Dog();
d.name = "Bruno";
d.eat();
d.bark();
Output:
Bruno is eating
Bruno says Woof!
We never wrote eat() inside Dog, but it works, because Dog inherited it from Animal. That's the first benefit: reuse. Write common code once in the parent, and every child gets it.
Two things are not passed on in the usual way:
- Private members. A child object still contains the parent's private fields, but the child's code can't touch them directly. It has to use the parent's public or protected methods.
-
Constructors. Each class has its own constructors. A child doesn't inherit the parent's constructors, but it calls them (we'll see how in the
super()section).
Is-a vs has-a: choosing the relationship
Not every relationship between classes should be inheritance. There are two main ones, and a quick sentence test tells them apart.
-
Is-a means inheritance. "A Dog is an Animal." It makes sense, so
Dog extends Animal. -
Has-a means composition. "A Car has an Engine." A car isn't an engine, so
Carholds anEngineobject inside it.
IS-A (inheritance) HAS-A (composition)
Animal Car
^ |
| extends | holds a reference to
| v
Dog Engine
"A Dog is an Animal" "A Car has an Engine"
Here's has-a in code:
class Engine {
void start() {
System.out.println("Engine started");
}
}
class Car {
private Engine engine = new Engine();
void drive() {
engine.start();
System.out.println("Car is moving");
}
}
new Car().drive();
Output:
Engine started
Car is moving
Car uses Engine's abilities without being an Engine. Many experienced developers lean toward composition ("favor composition over inheritance"), because it's more flexible. You can swap the engine for a different one later, but a parent-child link is locked in forever.
Types of inheritance
Here are the five types you'll see in textbooks. For each one, I'll say whether Java supports it with classes.
1. Single inheritance
One child, one parent. Supported.
A
^
|
B
2. Multilevel inheritance
A chain: grandparent, parent, child. Supported.
Animal
^
|
Dog
^
|
Puppy
class Animal {
void eat() { System.out.println("Eating"); }
}
class Dog extends Animal {
void bark() { System.out.println("Woof"); }
}
class Puppy extends Dog {
void weep() { System.out.println("Weeping"); }
}
Puppy p = new Puppy();
p.eat();
p.bark();
p.weep();
Output:
Eating
Woof
Weeping
Puppy gets eat() from its grandparent and bark() from its parent.
3. Hierarchical inheritance
One parent, many children. Supported.
Animal
/ | \
Dog Cat Bird
4. Multiple inheritance
One child, more than one parent class. Not supported with classes in Java. We'll look at why in the next section.
A B
\ /
C <- NOT allowed: class C extends A, B
5. Hybrid inheritance
A mix of the types above, such as hierarchical plus multiple. Because it usually includes multiple inheritance, it's not possible with classes alone. You can build it using interfaces.
The way around it: interfaces
A class can extend only one class, but it can implement many interfaces. This gives you the useful part of multiple inheritance:
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Duck implements Flyable, Swimmable {
public void fly() { System.out.println("Duck flies"); }
public void swim() { System.out.println("Duck swims"); }
}
Duck duck = new Duck();
duck.fly();
duck.swim();
Output:
Duck flies
Duck swims
Why multiple inheritance isn't possible in Java
Imagine Java allowed this:
class A {
void show() { System.out.println("A"); }
}
class B extends A {
void show() { System.out.println("B"); }
}
class C extends A {
void show() { System.out.println("C"); }
}
class D extends B, C { } // compile error in real Java
Now look at the shape this creates:
A
/ \
B C both B and C override show()
\ /
D which show() does D get?
If you write new D().show(), should Java run B's version or C's version? There's no good answer. This is called the diamond problem, named after the shape in the picture.
It gets worse once you think about the rest of the object:
- Fields: D would get A's fields through B and through C. Is that one copy or two?
- Constructors: Both B and C call A's constructor. Does it run once or twice, and in which order?
Some languages (like C++) have rules for all this, but the rules are complicated and easy to get wrong. Java's designers chose to keep the language simple and predictable: a class can have only one parent class.
But interfaces do allow it, so why isn't that a problem?
Interfaces used to have no code at all, only method signatures, so there was nothing to clash. Since Java 8, interfaces can have default methods with code, so the diamond can show up again. Java's answer: the compiler forces you to decide.
interface Walker {
default void move() { System.out.println("Walking"); }
}
interface Swimmer {
default void move() { System.out.println("Swimming"); }
}
class Duck implements Walker, Swimmer {
@Override
public void move() {
Walker.super.move();
Swimmer.super.move();
}
}
new Duck().move();
Output:
Walking
Swimming
If Duck didn't override move(), the code would not compile, because Java refuses to guess. The syntax Walker.super.move() means "call the move from the Walker interface." And interfaces never carry instance fields, which removes the two worst parts of the diamond (duplicated state and constructor order).
The Object class: the root of everything
Here is a fact that surprised me: every class in Java inherits from a class called Object. It sits at the top of the whole family tree, in the package java.lang.
If you write a class without extends, the compiler quietly adds extends Object for you. You can check it:
class Dog { }
Dog d = new Dog();
System.out.println(d.getClass().getName());
System.out.println(d.getClass().getSuperclass());
Output:
Dog
class java.lang.Object
What if your class already extends another one, like Dog extends Animal? Then Object is not its direct parent. It's the parent of Animal, so Dog still gets it indirectly, through the chain.
Object
^
|
Animal
^ ^
| |
Dog Cat
^
|
Puppy
This is also why "one parent only" doesn't break anything: every class has exactly one path up to Object. It's a tree, not a web.
What you get from Object
Because every class inherits from Object, every object in Java can do these things:
-
toString(): returns a text description of the object. -
equals(Object other): checks whether two objects are "equal". -
hashCode(): returns a number used byHashMapandHashSet. -
getClass(): tells you the object's class at run time. -
clone(): makes a copy (a bit tricky to use, so it's rarely used). -
wait(),notify(),notifyAll(): used for thread coordination.
Let's see the first two in action. By default they aren't very helpful:
class Student {
String name;
Student(String name) {
this.name = name;
}
}
Student s1 = new Student("Asha");
Student s2 = new Student("Asha");
System.out.println(s1);
System.out.println(s1.equals(s2));
Output (the number after the @ will differ on your machine):
Student@1b6d3586
false
The default toString() prints the class name and a code. The default equals() only checks whether both variables point to the same object, so two students with the same name are "not equal". To fix the first one, override toString():
class Student {
String name;
Student(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{name=" + name + "}";
}
}
System.out.println(new Student("Asha"));
Output:
Student{name=Asha}
(To compare students by name, you'd override equals() and hashCode() together. That's a topic for its own post.)
One more benefit: because everything is an Object, a variable of type Object can hold anything: Object o = new Dog(); or Object o = "hello";. That's how very general code, like old-style collections, was written.
super: reaching up to the parent
The keyword super refers to the parent part of the current object. It has three uses.
Use 1: super(...) calls the parent's constructor
When you create a child object, the parent part has to be built first. Every constructor starts by calling a parent constructor. If you don't write it, the compiler adds an invisible super(); as the first line.
class Animal {
Animal() { System.out.println("Animal constructor"); }
}
class Dog extends Animal {
Dog() { System.out.println("Dog constructor"); }
}
class Puppy extends Dog {
Puppy() { System.out.println("Puppy constructor"); }
}
new Puppy();
Output:
Animal constructor
Dog constructor
Puppy constructor
Here's the chain Java follows:
new Puppy()
|
v
Puppy() --calls super()--> Dog() --calls super()--> Animal() --calls super()--> Object()
|
(runs last) (runs first)
Constructors FINISH in this order: Object -> Animal -> Dog -> Puppy
The parent is always fully built before the child starts. That makes sense, because the child may depend on what the parent set up.
The object that comes out is one block on the heap that contains all the layers:
Puppy object on the heap
+----------------------------+
| Object part |
| Animal fields |
| Dog fields |
| Puppy fields |
+----------------------------+
A common trap: if the parent has no no-argument constructor, the invisible super(); fails. You must call super(...) yourself with the right arguments:
class Vehicle {
String brand;
Vehicle(String brand) {
this.brand = brand;
System.out.println("Vehicle: " + brand);
}
}
class Car extends Vehicle {
Car() { } // compile error
}
constructor Vehicle in class Vehicle cannot be applied to given types;
The fix is Car(String brand) { super(brand); }.
Use 2: super.method() calls the parent's version
When a child overrides a method, super.method() lets it still use the parent's version. You'll see this in the overriding section.
Use 3: super.field reaches the parent's field
If a child declares a field with the same name as the parent's, the child's hides the parent's. super.field gets the parent's one:
class Parent {
String type = "Parent";
}
class Child extends Parent {
String type = "Child";
void show() {
System.out.println(type + " " + super.type + " " + this.type);
}
}
new Child().show();
Output:
Child Parent Child
this(): calling another constructor of the same class
While super(...) goes up to the parent, this(...) stays in the same class and calls a different constructor. It's used to avoid repeating setup code.
Let's combine both in one example:
class Vehicle {
String brand;
Vehicle(String brand) {
this.brand = brand;
System.out.println("Vehicle: " + brand);
}
}
class Car extends Vehicle {
int doors;
Car(String brand, int doors) {
super(brand);
this.doors = doors;
System.out.println("Car doors: " + doors);
}
Car(String brand) {
this(brand, 4);
System.out.println("Car default");
}
}
new Car("Tata");
Output:
Vehicle: Tata
Car doors: 4
Car default
Follow the trace:
new Car("Tata")
|
Car(String) ---- this(brand, 4) ----> Car(String, int) ---- super(brand) ----> Vehicle(String)
| |
prints "Car doors: 4" <----------- prints "Vehicle: Tata"
|
prints "Car default"
The rules to remember
-
super(...)andthis(...)must go on the first line of a constructor. (Very recent Java versions loosen this a little, but keeping them first is the safe habit.) - You can't use both in the same constructor, because each one wants to be first. In the example above,
Car(String)usesthis(...), andCar(String, int)usessuper(...). - If you write neither, the compiler adds
super();for you. - Neither
thisnorsupercan be used inside astaticmethod, because static code has no object.
A quick way to keep them straight:
-
thisis the current object, andthis(...)calls another constructor in the same class. -
superis the parent part of the current object, andsuper(...)calls the parent's constructor.
Method overriding
Overriding means a child class provides its own version of a method it inherited, with the same name and parameters.
class Animal {
void sound() {
System.out.println("Some sound");
}
void eat() {
System.out.println("Eating");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Woof");
}
@Override
void eat() {
super.eat();
System.out.println("...and wagging its tail");
}
}
Animal a = new Dog();
a.sound();
a.eat();
Output:
Woof
Eating
...and wagging its tail
Two different styles appear here:
-
sound()replaces the parent's version completely. -
eat()extends it, by callingsuper.eat()first and then adding more.
Also notice that the variable a has type Animal, yet Dog's methods ran. Java picks the method based on the real object, not the variable's type.
How Java finds the method (behind the scenes)
This choice happens when the program runs, and it's called dynamic dispatch. Roughly, the JVM looks at the real object's class and searches upward until it finds the method:
a.sound() (a is an Animal variable, but it holds a Dog)
|
v
Look in Dog ----- has sound()? ---- yes ---> run it
| no
v
Look in Animal -- has sound()? ---- yes ---> run it
| no
v
Look in Object -- has sound()? ---- yes ---> run it
(Real JVMs use clever tables to make this very fast, but the idea is the same.)
The rules of overriding
- Same name and same parameters. Change the parameters and you've written a different method (that's overloading, not overriding).
- Same return type, or a more specific type of it (called a covariant return type).
-
Access can't get narrower. If the parent method is
public, the child's can't beprotectedor default. It can stay the same or get wider. -
You can't override
private,staticorfinalmethods. A private method isn't visible to the child. Afinalmethod is locked on purpose. Astaticmethod is hidden, not overridden. -
Use
@Override. If you misspell the name or change a parameter, the compiler gives an error instead of silently creating a new method.
A few related facts:
- Fields are not overridden. Only instance methods take part in dynamic dispatch.
-
You can stop inheritance with
final. Afinalclass can't be extended (Stringis one), and afinalmethod can't be overridden.
Which one should I use?
- Inheritance or composition? Use inheritance for a true "is-a" relationship where the child can be used anywhere the parent can. Otherwise, use composition and hold the other object inside.
-
Replace or extend a parent method? Replace it when the behavior is completely different. Call
super.method()first when you only want to add something. - Abstract class or interface? Use an abstract class when related classes share code or fields. Use an interface when you only need a contract, or when you need to "inherit" from more than one place.
- Hierarchy depth: keep it short. Two or three levels is comfortable. Beyond that, the code becomes hard to follow.
Common mistakes
-
Using inheritance just to reuse code. If "B is an A" sounds wrong, don't extend. Even the Java library has an old example of this mistake:
StackextendsVector, so a stack accidentally gets list methods like inserting in the middle. -
Forgetting the parent's constructor needs arguments. If the parent has no no-argument constructor, call
super(...)explicitly in every child constructor. -
Skipping
@Override. A small typo then creates a new method instead of overriding, and your child's version never runs.@Overrideturns that into a compile error. - Expecting fields or static methods to be overridden. Only instance methods are chosen by the real object. Fields and static methods follow the variable's type.
- Building deep inheritance chains. Changing something at the top can break everything below. Prefer shallow hierarchies and composition.
- Calling an overridable method from a constructor. The parent's constructor runs before the child's fields are set up, so the child's version of that method may run on half-built data. Keep constructors simple.
Quick recap
-
Inheritance (
extends) lets a child reuse and extend a parent. The parent is built first. - Is-a means inheritance, has-a means composition. Prefer composition when "is-a" isn't clear.
- Java supports single, multilevel and hierarchical inheritance with classes. Multiple and hybrid work only through interfaces.
- Multiple inheritance of classes is blocked because of the diamond problem: Java can't tell which parent's method, field or constructor to use.
-
Objectis the root of every class. If you don't writeextends, the compiler addsextends Object, and children get it indirectly. -
super(...)calls the parent constructor, andthis(...)calls another constructor in the same class. Each goes on the first line, and you can't use both in one constructor. - Overriding replaces a parent's method. The real object decides which version runs, at run time.
Try it yourself
Here's a small practice idea: build a Shape parent class with a name field and an area() method that returns 0. Create Circle and Rectangle children that override area() and call super(name) in their constructors. Add a toString() that prints the shape's name and area. Then put all the shapes in a Shape[] array and loop through them.
For a bonus experiment, add a System.out.println inside each constructor in a three-level chain and predict the output order before you run it. Then add this(...) to one of the constructors and see how the order changes.
If this helped, or if you spotted something I should fix, tell me in the comments. I'm still learning too.
Top comments (0)