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
- 21File I/O and Serialization
- 22Debugging: Errors, Stack Traces, the Debugger and Logging
- 23Maven: What It Does and How to Use It
- 24Git for Java Projects: Commits, Branches, Merging and Pull Requests
- 25Testing with JUnit and Mockito: Unit Tests, Mocks and Their Limits
- 26Project 1: Build an Expense Tracker CLI in Plain Java
8 · Databases and JDBC
9 · Web and Spring
10 · Persistence with JPA
11 · Secure, Tested, Production-Ready
- 36Security Fundamentals and Spring Security: Passwords, JWT, Access Control, CORS and Secrets
- 37Testing Spring Boot Applications: Slice Tests, MockMvc, Real Databases and the Test Pyramid
- 38Production Practices: Configuration, Logging, Docker and Health Checks for Spring Boot
- 39Project 3: The Job Application Tracker API, Assembled and Reviewed
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.
For
Learners who can write classes and want to understand inheritance, overriding and overloading, including their pitfalls.
Level
Before you start
Classes, Objects and Encapsulation (article 4).
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,superand 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.
// 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"; }}// 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 }}// 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()); }}Ravi earns 40000.0 per monthMeena earns 105000.0 per monthMeenaSeveral rules are at work:
extendsdeclares the relationship. A Java class can extend exactly one class. Every class that does not say otherwise extendsjava.lang.Object, so every object has methods liketoStringandequals(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
privatemembers of its superclass.Managerreads the name throughgetName(). This is encapsulation at work, because the parent keeps control of its data. protectedlets 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 |
Not overridable |
|
Exceptions | The overriding method cannot throw broader checked exceptions (article 10) |
The compiler enforces the access rule:
// 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()); }}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 errorAnyone 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.
// 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())); }}int 5double 5.0string hiobject hiwoofmet an animalmet a dogThis output is the clearest demonstration of the difference:
describe(disguised)printedobject hi, even though the object really is aString. The compiler saw a variable of typeObjectand chose theObjectoverload. Overloading uses the declared (static) type.pet.sound()printedwoof, even thoughpetis declared asAnimal. The runtime looks at the real object. Overriding uses the actual (runtime) type.meet(pet)printedmet an animal, because the declared type ofpetisAnimal. 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.
// 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(); }}Parent constructor, label = label uses name = nullChild constructor, name = child-nameThe 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.
// 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); }}B A AThe 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
#- Next: Interfaces, Polymorphism and Composition shows how to get flexible behaviour without deep inheritance.
- To see the methods every class inherits from
Object:equals,hashCode,toStringand the Object Contract. - For restricted hierarchies: Enums, Records and Sealed Types.
- Technology
- Software Engineering
- #interview preparation
- #Java
- #Java backend
- #Java basics
- #Java interview
- #JPA
- #learn Java
- #OOP
- #Spring Boot
- #SQL
Connected lessons
Linked from 1 guide
Other pages in the library that point here.
Keep reading
More in Technology →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

