1. Method overriding
Overriding (overriding) is the ability in a subclass to write its own version of a method that already exists in the parent class. Thanks to this, true polymorphism works at run time (run-time).
In simple terms:
If you have a base class with a method and you want a subclass to perform that method in its own way, you simply declare a method with the same signature in the subclass. When the method is called through a reference of the base type, the version from the actual type of the object will be invoked.
Real-world example.
Imagine you have a team of animals and you ask each one to “make a sound.” For all of them the command is makeSound(), but a dog barks, a cat meows, and a cow moos. In code it looks like a call to the same method, but the result differs — that’s the magic of overriding!
Overriding syntax
Basic example
class Animal {
void makeSound() {
System.out.println("An animal makes some kind of sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Bark!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Meow!");
}
}
Here, the Animal class defines the makeSound() method. The subclasses Dog and Cat override this method, providing their own implementation.
The @Override annotation
In Java it’s recommended (and a good habit) to mark overridden methods with the @Override annotation:
@Override
void makeSound() { ... }
This is not required for the code to work, but:
- The compiler will check that you are actually overriding a method (and not accidentally misspelling its name or parameters).
- It improves code readability — other developers can immediately see that this method overrides a parent method.
Calling an overridden method
Animal myDog = new Dog();
myDog.makeSound(); // Prints: Bark!
Although the variable is of type Animal, it actually points to a Dog object, and the method from the Dog class is called. This is dynamic (late) binding.
2. Difference between overriding and overloading
Overloading
- Within a single class (or in a hierarchy, but always “side by side”).
- Methods have the same name but different parameters (type, count, order).
- Method selection happens at compile time.
void print(int x) { ... }
void print(String s) { ... }
Overriding
- In different classes: a method is declared in a superclass and overridden in a subclass.
- Methods have the same name and signature (parameters and return type).
- Method selection happens at run time, based on the actual type of the object.
Comparison table
| Overloading | Overriding | |
|---|---|---|
| Where | Within a single class | In a superclass and a subclass |
| Method name | Same | Same |
| Parameters | Different | Same |
| Return type | May differ | Must match or be a subtype |
| When selected | Compile time | Run time |
| Annotation | Not required | @Override (recommended) |
3. Rules for method overriding
Overriding is powerful, but it comes with strict rules. Let’s go through them one by one.
The method signature must match
- The method name, parameter types, and order must be identical to the method in the superclass.
- The return type must match or be covariant (that is, a subtype of the parent method’s return type).
Example with a covariant return type:
class Animal {
Animal reproduce() { return new Animal(); }
}
class Cat extends Animal {
@Override
Cat reproduce() { return new Cat(); } // OK! Cat is a subtype of Animal
}
Access modifier
The access modifier of an overridden method cannot be more restrictive than the one in the superclass. If the method in the parent is public, then in the subclass it must remain public. You cannot make it less accessible (protected or private).
Example:
class Parent {
public void greet() { }
}
class Child extends Parent {
// void greet() { } // Error! The default (package-private) modifier is less accessible than public
@Override
public void greet() { } // OK
}
Exceptions
- An overridden method cannot throw new checked exceptions that are not declared in the base method.
- You may throw fewer or the same exceptions.
Example:
class Parent {
void doWork() throws IOException { }
}
class Child extends Parent {
@Override
void doWork() throws FileNotFoundException { } // OK, FileNotFoundException is a subtype of IOException
// void doWork() throws SQLException { } // Error! SQLException is not declared in the parent
}
Static methods are not overridden
Static methods can be hidden (hidden) but not overridden. If you declare a static method with the same signature in a subclass, this is not overriding! It will be method hiding, not polymorphism.
class Animal {
static void info() { System.out.println("Animal"); }
}
class Dog extends Animal {
static void info() { System.out.println("Dog"); }
}
Calling Dog.info() will print "Dog", but if you call it through a variable of type Animal, the Animal.info() method will be invoked. This is not polymorphism!
final methods cannot be overridden
If a method in the superclass is declared as final, attempting to override it will result in a compilation error.
class Animal {
final void sleep() { }
}
class Dog extends Animal {
// @Override
// void sleep() { } // Error! You cannot override a final method
}
4. Practical examples
Let’s look at how overriding works in practice and how it differs from overloading.
Example 1: The Shape class and its descendants
class Shape {
void draw() {
System.out.println("Drawing a shape");
}
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("Drawing a circle");
}
}
class Rectangle extends Shape {
@Override
void draw() {
System.out.println("Drawing a rectangle");
}
}
Using polymorphism:
public class Main {
public static void main(String[] args) {
Shape s1 = new Circle();
Shape s2 = new Rectangle();
s1.draw(); // Drawing a circle
s2.draw(); // Drawing a rectangle
}
}
Although the variables are declared as Shape, the method of the class to which the object actually belongs is called.
Example 2: Difference from overloading
class Printer {
void print(String s) {
System.out.println("String: " + s);
}
void print(int n) {
System.out.println("Number: " + n);
}
}
Here both methods are named print, but they have different parameters — this is overloading, not overriding.
5. Overriding and calling the parent method (super)
Sometimes in an overridden method you want to first execute the parent’s logic and then add your own. Use the super keyword for this.
class Animal {
void makeSound() {
System.out.println("The animal makes a sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
super.makeSound(); // Call the parent method
System.out.println("Bark!");
}
}
The call new Dog().makeSound() will output:
The animal makes a sound
Bark!
How dynamic binding (late binding) works
When you call a method through a reference of a base type, Java at run time looks at which actual object this reference points to and calls the version of the method that is defined in that object’s class.
Animal a = new Cat();
a.makeSound(); // Will call Cat.makeSound(), not Animal.makeSound()
This is the foundation of polymorphism in Java.
6. How this relates to your application
In our training application (for example, an employee management system) you can create a base class Employee with a work() method, and the subclasses Manager and Developer can implement this method in their own way:
class Employee {
void work() {
System.out.println("The employee is working");
}
}
class Manager extends Employee {
@Override
void work() {
System.out.println("The manager leads");
}
}
class Developer extends Employee {
@Override
void work() {
System.out.println("The developer writes code");
}
}
Now you can store all employees in a single array or list:
Employee[] employees = {new Manager(), new Developer(), new Developer()};
for (Employee e : employees) {
e.work(); // Each produces its own output!
}
7. Common mistakes when overriding methods
Mistake #1: a typo in the method name or parameters. If you accidentally make a mistake in the method name or its parameters, you are not overriding the method — you are creating a new one. As a result, polymorphism won’t work. That’s why you should always use the @Override annotation — the compiler will immediately catch it.
Mistake #2: a more restrictive access modifier. If the method in the parent is public and you declare it as protected or without a modifier in the child class, you will get a compilation error.
Mistake #3: trying to override a static or final method. Static methods are not overridden, and final methods cannot be overridden at all. If you try — the compiler will stop you.
Mistake #4: changing the return type to an incompatible one. If the return type of the method in the subclass does not match the type in the superclass (and is not its subtype), the compiler will not allow you to override the method.
Mistake #5: adding new checked exceptions. An overridden method cannot throw new checked exceptions that are not in the base method’s declaration. If you do this — the compiler will produce an error.
Mistake #6: forgetting about super. If in an overridden method you want to preserve part of the parent’s behavior, don’t forget to explicitly call super.methodName(). Java won’t do this for you.
Now you know how method overriding works, how it differs from overloading, and how it enables polymorphism in Java. In the next lecture we’ll look at how to apply polymorphism in practice — with collections, arrays, and real-world tasks!
GO TO FULL VERSION