Skip to content
SparkTime, home
Java From Zero to Job-Ready
Lessons
1 · Java Foundations
2 · Object-Oriented Java
3 · Core Java
4 · Collections and Generics
5 · Functional Java
6 · The JVM and Concurrency
7 · Tools and Craft
8 · Databases and JDBC
9 · Web and Spring
10 · Persistence with JPA
11 · Secure, Tested, Production-Ready
12 · Interviews and Readiness
Lesson 5 of 41

Lesson 5 · Object-Oriented Java

Inheritance, Overriding and Overloading

How one Java class can build on another, how overriding changes behaviour at runtime, how overloading is chosen at compile time, and why inheritance is easy to misuse. With the constructor-order trap, field hiding and the exact differences interviewers ask about.

On this page

Key takeaways

  • Inheritance lets a subclass reuse and specialise a superclass. It models an is-a relationship, and the subclass must be usable anywhere the superclass is expected.
  • Overriding replaces an inherited method and is chosen at runtime from the actual object's class. Overloading provides several methods with one name and is chosen at compile time from the declared argument types.
  • A subclass constructor must call a superclass constructor first, and calling an overridable method from a constructor can observe a half-built object.
  • Fields are not overridden, only hidden, so the declared type decides which field you read. Keep fields private.
  • Inheritance creates tight coupling between parent and child. Use it for genuine is-a relationships and prefer composition when you only want to reuse code.

Suppose a company's payroll program needs Employee and Manager. A manager is an employee with some extra things: a team, and a bonus. If you write two unrelated classes, you copy the name, the salary and the code that formats them into both. Fix a bug in one and you must remember the other.

Inheritance is Java's mechanism for sharing and specialising code between related classes. It is powerful and often overused, so this article teaches both the mechanics and the judgement.

What you will learn

#
  • How extends, super and constructors work together.
  • What overriding is, and how Java picks the method at runtime.
  • What overloading is, and how Java picks the method at compile time.
  • The difference between them, which is a standard interview question.
  • Why constructors that call overridable methods are dangerous.
  • When inheritance is the wrong tool.

Inheritance: reuse and specialise

#

A class can extend another. The new class, the subclass, inherits the accessible fields and methods of the superclass, and can add new ones.

Java
// File: Employee.javapublic class Employee {    private final String name;    private final double monthlySalary;    public Employee(String name, double monthlySalary) {        this.name = name;        this.monthlySalary = monthlySalary;    }    public String getName() {        return name;    }    public double monthlyPay() {        return monthlySalary;    }    public String describe() {        return name + " earns " + monthlyPay() + " per month";    }}
Java
// File: Manager.javapublic class Manager extends Employee {    private final double bonus;    public Manager(String name, double monthlySalary, double bonus) {        super(name, monthlySalary);     // must come first: build the Employee part        this.bonus = bonus;    }    @Override    public double monthlyPay() {        return super.monthlyPay() + bonus;   // reuse, then extend    }}
Java
// File: Main.javapublic class Main {    public static void main(String[] args) {        Employee clerk = new Employee("Ravi", 40_000);        Employee boss = new Manager("Meena", 90_000, 15_000);        System.out.println(clerk.describe());        System.out.println(boss.describe());        System.out.println(boss.getName());    }}
Code
Ravi earns 40000.0 per monthMeena earns 105000.0 per monthMeena

Several rules are at work:

  • extends declares the relationship. A Java class can extend exactly one class. Every class that does not say otherwise extends java.lang.Object, so every object has methods like toString and equals (article 7).
  • Constructors are not inherited, but a subclass constructor must call one: super(...) must be the first statement. If you do not write it, Java inserts a call to the superclass's no-argument constructor, and it is an error if there is none.
  • A subclass cannot access the private members of its superclass. Manager reads the name through getName(). This is encapsulation at work, because the parent keeps control of its data.
  • protected lets subclasses (and the same package) access a member, but it widens the surface of the class, so use it rarely.

Notice what happened in boss.describe(). The method describe is defined only in Employee, but when it calls monthlyPay(), the Manager's version runs, and the total includes the bonus. The variable boss has the type Employee, yet the object is a Manager. That is overriding at work.

Overriding: the object decides at runtime

#

Overriding means a subclass provides its own implementation of a method that it inherited, with the same name and parameter types. When you call the method on an object, Java runs the version belonging to the object's actual class, not the declared type of the variable. This is called dynamic dispatch, and it is the engine behind polymorphism, which the next article develops.

Always write the @Override annotation. It makes the compiler check that you really are overriding something, so a typo in the name or parameters becomes a compile error instead of a silent new method.

Rules for overriding:

Rule

Detail

Same signature

Same name and parameter types

Return type

The same, or a subtype of the original's (a covariant return)

Access

Not more restrictive than the original (you cannot weaken public to private)

Not overridable

private, static and final methods

Exceptions

The overriding method cannot throw broader checked exceptions (article 10)

The compiler enforces the access rule:

Java
// File: Main.javaclass Base {    public String label() {        return "base";    }}class Derived extends Base {    @Override    String label() {            // package-private is weaker than public        return "derived";    }}public class Main {    public static void main(String[] args) {        System.out.println(new Derived().label());    }}
Code
Main.java:10: error: label() in Derived cannot override label() in Base    String label() {            // package-private is weaker than public           ^  attempting to assign weaker access privileges; was public1 error

Anyone holding a Base reference is entitled to call label() publicly. If a subclass could hide that, code that works with a Base would break when given a Derived. This idea, that a subclass must work wherever the superclass does, is the Liskov substitution principle, and we return to it below.

Overloading: the compiler decides at compile time

#

Overloading is different. It means a class has several methods with the same name but different parameter lists. The compiler chooses between them using the declared types of the arguments, before the program runs.

Java
// File: Main.javapublic class Main {    static String describe(int n) {        return "int " + n;    }    static String describe(double d) {        return "double " + d;    }    static String describe(Object o) {        return "object " + o;    }    static String describe(String s) {        return "string " + s;    }    static class Animal {        String sound() {            return "...";        }    }    static class Dog extends Animal {        @Override        String sound() {            return "woof";        }    }    static String meet(Animal a) {        return "met an animal";    }    static String meet(Dog d) {        return "met a dog";    }    public static void main(String[] args) {        System.out.println(describe(5));        System.out.println(describe(5L));          // long: no long version, so it widens to double        System.out.println(describe("hi"));        Object disguised = "hi";        System.out.println(describe(disguised));   // the declared type is Object        Animal pet = new Dog();        System.out.println(pet.sound());           // overriding: the object's class decides        System.out.println(meet(pet));             // overloading: the declared type decides        System.out.println(meet(new Dog()));    }}
Code
int 5double 5.0string hiobject hiwoofmet an animalmet a dog

This output is the clearest demonstration of the difference:

  • describe(disguised) printed object hi, even though the object really is a String. The compiler saw a variable of type Object and chose the Object overload. Overloading uses the declared (static) type.
  • pet.sound() printed woof, even though pet is declared as Animal. The runtime looks at the real object. Overriding uses the actual (runtime) type.
  • meet(pet) printed met an animal, because the declared type of pet is Animal. It is an overloaded method, so the declared type decides.

Overriding

Overloading

Where

Between a subclass and its superclass

Usually within one class (or across an inheritance chain)

Signature

Same name and parameters

Same name, different parameters

Chosen

At runtime, by the object's class (dynamic dispatch)

At compile time, by the argument types

Purpose

Specialise behaviour

Convenient variants of an operation

Return type

Same or covariant

Can differ, but the return type alone cannot distinguish overloads

Overloading has traps. When the types do not match exactly, Java applies conversions in a fixed order: widening (as 5L to double), then boxing, then varargs, and the result can surprise you. Keep overloads clearly different, and avoid designs where a call could plausibly match two methods.

Constructors, initialisation order and a classic trap

#

When you create a Manager, the Employee part must be built first, because the subclass depends on it. The order is: superclass constructor, then the subclass's field initialisers, then the subclass constructor body. This produces one of the best-known traps in Java: calling an overridable method from a constructor.

Java
// File: Main.javapublic class Main {    static class Parent {        Parent() {            System.out.println("Parent constructor, label = " + label());        }        String label() {            return "parent";        }    }    static class Child extends Parent {        private String name = "child-name";        Child() {            super();            System.out.println("Child constructor, name = " + name);        }        @Override        String label() {            return "label uses name = " + name;        }    }    public static void main(String[] args) {        new Child();    }}
Code
Parent constructor, label = label uses name = nullChild constructor, name = child-name

The Parent constructor called label(), which is overridden, so the Child's version ran, but the Child's fields had not been initialised yet, so name was still null. The method observed an object that was only half built. The rule: never call an overridable method from a constructor. Call private or final methods, or do the work after construction.

Fields are not overridden

#

Methods are overridden. Fields are only hidden. If a subclass declares a field with the same name as one in its superclass, they are two separate fields, and which one you see depends on the declared type of the variable.

Java
// File: Main.javapublic class Main {    static class A {        String name = "A";    }    static class B extends A {        String name = "B";    }    public static void main(String[] args) {        B b = new B();        A viaA = b;        System.out.println(b.name + " " + viaA.name + " " + ((A) b).name);    }}
Code
B A A

The same object, read through two different declared types, gives two different answers. This is confusing, which is one more reason to keep fields private and access them through methods, which are overridden consistently.

Preventing inheritance: final

#

A final class cannot be extended (String is one), and a final method cannot be overridden. Use final when a class is not designed for inheritance. Joshua Bloch's well-known advice in Effective Java is to "design and document for inheritance or else prohibit it". Allowing subclasses is a commitment: the parent's internal structure becomes part of its contract. Since Java 17, sealed classes let you allow specific subclasses and no others, which we study in article 9.

When inheritance goes wrong

#

Inheritance gives you code reuse, and also a very tight coupling: a subclass depends on its parent's implementation, not just its public behaviour. Three recurring problems:

1. The is-a test is satisfied by English and not by behaviour. A square is a rectangle in geometry. But if Rectangle has setWidth and setHeight, a Square that extends it cannot keep its sides equal without surprising callers who set the width and expect the height to stay put. A subclass must honour everything the parent promises, and that is the Liskov substitution principle. "Is-a" must mean "can be used wherever the parent is expected, without surprises".

2. The fragile base class. Changing a superclass can silently break subclasses that relied on how it works internally. For example, a subclass that overrides add and addAll to count insertions can count some items twice if the parent's addAll happens to call add. The parent's implementation detail leaked into the subclass.

3. Reuse without a relationship. If Stack extends ArrayList just to reuse the storage, a Stack now has add(index, element) and everything else on a list, which breaks the idea of a stack. The Java library itself has such a mistake (java.util.Stack extends Vector) that is kept for compatibility.

The usual remedy is composition: a class has an object of another class and uses it, instead of being one. We study that in the next article. A reasonable rule of thumb is to use inheritance for a true is-a relationship that you control and have designed for, and to prefer composition for reuse.

Common mistakes

#
  • Forgetting @Override, so a typo creates a new method instead of overriding.
  • Confusing overriding and overloading, especially what is decided at compile time and what at runtime.
  • Calling an overridable method in a constructor.
  • Expecting fields to behave polymorphically.
  • Using inheritance only for code reuse.
  • Making everything protected, which leaks the superclass internals.
  • Deep inheritance hierarchies, which become hard to understand and change.
  • Forgetting super(...) when the superclass has no no-argument constructor, which is a compile error.

Exercises

#

Exercise 1

Predict

#

Employee e = new Manager("A", 100, 10); System.out.println(e.monthlyPay()); Using the classes from this article, what is printed and why?

Exercise 2: Overriding or overloading?

#

Classify each: (a) Dog defines String sound() and Animal defines String sound(), (b) a class defines int add(int a, int b) and double add(double a, double b), (c) Dog defines String sound(int times) while Animal defines String sound().

Exercise 3: Find the bug

#

A developer wanted to override toString in a class but wrote public String tostring() { return "Account"; }. The output still shows the default text. Why, and how does the language help prevent it?

Exercise 4: Fix the constructor

#

A parent class constructor calls initialise(), which a child overrides and which uses a child field. The field is null when it is used. Propose two designs that avoid the problem.

Exercise 5: Design

#

You need Circle, Square and Triangle, each with an area. Would you use inheritance from a common class, or something else? Say what you would do and what you would defer to the next article.

Show solutionHide solution

It prints 110.0. The variable's type is Employee, but the object is a Manager, so the runtime calls Manager.monthlyPay(), which returns the salary plus the bonus. This is dynamic dispatch: overridden methods are chosen by the object's actual class.

Exercise 2

Show solutionHide solution

(a) Overriding: same name and parameters, in a subclass. (b) Overloading: same name, different parameter types, in the same class. (c) Overloading, not overriding: the parameter lists differ (int versus none), so Dog now has two methods named sound, and does not replace the inherited one. A call dog.sound() still runs Animal's version, which is why @Override on method (c) would have caused a compile error and caught the confusion.

Exercise 3

Show solutionHide solution

Method names are case sensitive, so tostring is a new, unrelated method, and Object.toString() is not overridden. If the developer had written @Override above it, the compiler would have reported "method does not override or implement a method from a supertype". That is why you should always use @Override: it turns a silent logic bug into an immediate compile error.

Exercise 4

Show solutionHide solution

Option 1: do not call an overridable method from the constructor. Make the method private or final, or require callers to call initialise() explicitly after construction. Option 2: pass what the parent needs to the constructor (super(config)) so that no overridable hook is needed, or use a factory method that builds the object fully and then calls the initialisation. A third approach is composition: give the parent a collaborator object that does the work, instead of an overridable method. The principle is that an object should be fully constructed before any overridable behaviour can run.

Exercise 5

Show solutionHide solution

The shapes share one capability (they have an area) but little implementation: a circle needs a radius, a square a side, a triangle a base and height. Inheriting from a common class would give you nothing to reuse and only the promise of area(). The better tool is an interface such as Shape with an area() method, and three small implementing classes. That is exactly what the next article covers, where we also meet abstract classes and polymorphism formally.

Where to go next

#
  • Guide · Technology

    Java Essentials for DSA

  • Guide · Technology

    Heaps and Priority Queues

  • Course · Technology

    DSA From Zero to Job-Ready: A Practical Guide with Java