1. Method overriding
In real life, we often see situations where a “child” behaves in a special way. For example, all animals can make sounds, but a cat says “meow,” a dog says “woof,” and a programmer says “oops, another bug!”. In programming this is implemented through method overriding (override).
Method overriding is when a subclass provides its own implementation of a method that is already declared in the parent class. In other words, it “replaces” the default behavior with its own, more specific one.
Analogy. If you imagine the parent class as a signature borscht recipe, then method overriding is when grandma adds her secret ingredient. It’s still borscht, but each version has its own taste.
To override a method, you need to declare a method in the child class with exactly the same signature (name, parameters, return type) as the parent’s.
Example: animals and their sounds
class Animal {
void makeSound() {
System.out.println("Some generic animal sound");
}
}
class Dog extends Animal {
// Override makeSound()
void makeSound() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
// Override makeSound()
void makeSound() {
System.out.println("Meow!");
}
}
Now, if you create a Dog object and call makeSound(), you’ll hear "Woof!", not "Some generic animal sound".
Demonstration in code
public class Main {
public static void main(String[] args) {
Animal generic = new Animal();
Dog dog = new Dog();
Cat cat = new Cat();
generic.makeSound(); // Some generic animal sound
dog.makeSound(); // Woof!
cat.makeSound(); // Meow!
}
}
Important: if the subclass doesn’t have a method with the same signature, the parent’s method will be used.
2. The @Override annotation: why it matters and how to use it
In Java it’s customary to mark overridden methods with the @Override annotation. It’s not just code decoration; it’s a useful tool:
- The compiler checks that you really are overriding a parent method. If you make a mistake in the name, parameter type, or return type, the compiler will produce an error.
- Improves code readability. Another developer immediately sees: “Oh, this method overrides a parent’s method.”
Example with @Override
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof!");
}
}
If you accidentally write void makeSond() (a typo!) in a method marked with @Override, the compiler will complain: "Method does not override or implement a method from a supertype".
Modern standards. Using @Override is good practice and an industry standard. Even if the compiler doesn’t require it, always add this annotation — it makes life easier for you and your colleagues.
3. How calling an overridden method works
When you call a method on a subclass instance, the subclass’s implementation will be used, even if the variable is declared as the parent type.
Example: polymorphism in action
Animal animal = new Dog();
animal.makeSound(); // "Woof!", not "Some generic animal sound"
Here the variable is of type Animal, but it actually holds a Dog object. Java “understands” that it should call the overridden method from Dog. This is polymorphism (more on this in later lectures).
4. Overriding rules and restrictions
Method signature
- The name, type, and order of parameters must match the method in the parent.
- The return type must match or be covariant (a subtype of the parent’s return type). For example, if the parent returns Animal and the child returns Dog, that’s allowed.
Access modifiers
- You cannot make access more restrictive than in the parent.
- If the parent method is public, the overridden method must also be public.
- If the parent’s is protected, the overridden one can be protected or public.
If you try to do the opposite, the compiler will say: "Cannot reduce the visibility of the inherited method".
Exceptions
- An overridden method cannot throw a new checked exception that is not declared by the parent.
- You can throw fewer exceptions than the parent, or their subtypes.
static, final, private
- You cannot override methods declared as static or final, nor private methods.
- static is hiding, not overriding.
- final cannot be overridden at all; Java protects such methods.
- private is not visible in the subclass; you cannot override it (you can only declare a new method with the same name).
Constructors
Constructors are not inherited and not overridden. Each class has its own constructors.
5. Evolve the “Zoo” learning app
Time to apply the theory in practice! Let’s keep developing our “zoo” app.
Step 1. Base class Animal
public class Animal {
public void makeSound() {
System.out.println("Some generic animal sound");
}
public void sleep() {
System.out.println("Zzz...");
}
}
Step 2. Subclasses Dog and Cat
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Woof!");
}
// Additional method only for Dog
public void fetch() {
System.out.println("Dog brings the stick!");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow!");
}
// Additional method only for Cat
public void scratch() {
System.out.println("Cat scratches the sofa!");
}
}
Step 3. Use overriding
public class ZooTest {
public static void main(String[] args) {
Animal generic = new Animal();
Animal dog = new Dog();
Animal cat = new Cat();
generic.makeSound(); // Some generic animal sound
dog.makeSound(); // Woof!
cat.makeSound(); // Meow!
// dog.fetch(); // Error! A variable of type Animal doesn't know about fetch()
// cat.scratch(); // Same here
// But if you specify the type explicitly:
if (dog instanceof Dog) {
((Dog) dog).fetch(); // Dog brings the stick!
}
if (cat instanceof Cat) {
((Cat) cat).scratch(); // Cat scratches the sofa!
}
}
}
Comment:
The makeSound() method is polymorphic — the version from the object’s actual class is called. But class-specific methods (fetch, scratch) are available only via explicit casting — this is important for understanding how inheritance and overriding work.
6. Example with return type (covariance)
Sometimes you want the overridden method to return a more “narrow” type. For example:
class Animal {
Animal getFriend() {
return new Animal();
}
}
class Dog extends Animal {
@Override
Dog getFriend() { // Return type — Dog, a subtype of Animal
return new Dog();
}
}
This is called a covariant return type and is allowed in Java (since Java 5).
7. What happens if you don’t use @Override?
If you accidentally make a mistake in the method name or parameters, Java won’t complain if there’s no @Override annotation. As a result, you won’t override but will create a new method, and the expected behavior won’t change.
Example mistake
class Dog extends Animal {
// Typo: makeSoud instead of makeSound
void makeSoud() {
System.out.println("Woof!");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
dog.makeSound(); // Prints "Some generic animal sound"
}
}
If there had been @Override, the compiler would have produced an error: "Method does not override or implement a method from a supertype".
8. Common mistakes when overriding methods
Error #1: missing @Override annotation.
Without it, it’s easy to make a mistake in the method name or parameters. As a result, the method won’t be overridden, and the program will behave differently from what you expect.
Error #2: attempting to narrow the access modifier.
If the parent method is public and you write protected or private, you’ll get a compilation error.
Error #3: signature mismatch.
If the parameters differ even by type, that’s not overriding but overloading (overloading).
Error #4: trying to override a final or static method.
Java won’t allow this: final protects against overriding, and static methods aren’t overridden at all (only hidden).
Error #5: changing the return type to an incompatible one.
You can return only a subtype of the parent’s return type (covariance), not a completely different type.
GO TO FULL VERSION