Polymorphism: overriding & overloading
Dynamic dispatch, @Override rules, overloading resolution, upcasting, downcasting and instanceof.
Polymorphism means "many forms". In Java it means one variable, parameter or array of a general type can hold objects of many specific types, and each object responds to the same method call in its own way. It is what lets you write code against Shape or PaymentMethod today and have it work with subclasses you will invent next year.
Java has two kinds:
- Runtime polymorphism: method overriding, resolved while the program runs.
- Compile-time polymorphism: method overloading, resolved by the compiler.
Upcasting: a child fits in a parent variable#
Because a Dog is an Animal, you can store it in an Animal variable. This is called upcasting, and it is automatic:
The declared type (Animal) decides what you are allowed to call. The runtime type (Dog) decides which version runs.
Overriding and dynamic dispatch#
The loop only knows about Shape, yet each object runs its own area(). This is dynamic dispatch: at runtime, the JVM looks at the actual class of the object and finds the most specific override, walking up the hierarchy if needed (Square uses Rectangle.area()).
The big win: adding a Triangle class requires no changes to the loop. Code that depends on the general type is open to new subclasses.
Rules for a valid override#
To override, the subclass method must have:
- The same name and parameter list as the parent method.
- A compatible return type: the same type, or a subtype (a covariant return, e.g. parent returns
Animal, child returnsDog). - Equal or wider access: a
publicmethod can't becomeprotectedorprivatein the child. - No new or broader checked exceptions than the parent declares (exceptions get their own lesson).
And some things cannot be overridden at all:
privatemethods (they are invisible to the child, so a same-named method is just a new method).finalmethods.staticmethods. A static method with the same signature in a child hides the parent's; there is no dynamic dispatch for statics.- Fields. Fields are never polymorphic; they are chosen by the declared type.
Always use @Override
@Override asks the compiler to confirm you really are overriding something. Without it, a typo silently creates a brand-new method:
With @Override, the mistake becomes a compile error instead of a subtle bug.
Fields and static methods are not polymorphic#
Keep fields private and you will never be bitten by field hiding.
Overloading: same name, different parameters#
Overloading means several methods in the same class (or inherited) share a name but have different parameter lists. The compiler picks one based on the declared types of the arguments:
That last line is the key difference from overriding: even though o holds a String, the compiler only sees Object, so print(Object) is chosen. Overloading is decided at compile time; overriding at runtime.
The compiler's preference order, roughly: exact match, then widening primitive conversion (int to long), then autoboxing (int to Integer), then varargs.
Downcasting and instanceof#
Sometimes you hold an Animal and need a Dog-only method. You must downcast explicitly, and it can fail at runtime with a ClassCastException if the object isn't really a Dog. Check first with instanceof.
Since Java 16, pattern matching for instanceof tests and casts in one step, binding a new variable:
The old style, which you'll see in older code, needs a separate cast:
Some casts are rejected at compile time because they can never succeed, such as (Integer) "hello", since String and Integer are unrelated classes.
If you find yourself writing long chains of
instanceofchecks, it is usually a sign the behaviour belongs in an overridden method instead. Let the object decide. (Modern Java'sswitchpattern matching with sealed types is the exception, covered in the Modern Java lesson.)
Polymorphic parameters#
Methods that accept the parent type work with every subclass:
checkout never changes when you add NetBankingPayment. Programming to the general type is one of the most useful habits in object-oriented design.
Common mistakes#
- Expecting the declared type to choose the overridden method. It is the runtime object that decides.
- Expecting the runtime type to choose an overloaded method. It is the declared type that decides.
- Downcasting without an
instanceofcheck, leading toClassCastException. - Overloading
equals(Point)instead of overridingequals(Object). Always add@Override. - Trying to "override" a
staticorprivatemethod; you only hide it or create a new one. - Reducing visibility in an override (
protectedwhen the parent ispublic), which won't compile.
What's next#
Our Shape.area() returned a meaningless 0 just so subclasses had something to override. Next, abstract classes let you declare a method that must be implemented, and stop anyone creating a plain, meaningless Shape.
Check your understanding
Quick quiz
1.
Animal a = new Dog();andDogoverridesspeak(). Whichspeak()runs fora.speak()?2.What decides which *overloaded* method is called?
3.
Object o = "hello"; Integer i = (Integer) o;What happens?
Finished reading?
Mark this lesson complete to track your progress.