Polymorphism is the essence of OOP. Overloading and overriding are two tools for achieving polymorphic behavior in Java.
Overloading implements
ad-hoc polymorphism. This means that the "same" method can work with different parameters. Technically, these are simply two different methods with the same name but different parameter sets. It's important to remember that for overloading, differences in modifiers, return types, and exception lists alone are
not sufficient.
Ad-hoc polymorphism isn't truly polymorphic because it involves
early binding (aka
static binding, or
static dispatch). This means that the choice of the specific method variant is based on the type of the
variable, not the object it references, and this determination is made at compile time.
If a class declares two overloaded methods and the argument in a call matches both, a compilation error occurs. For example, in the code below, the compiler cannot choose between the
println
method variants with
char[]
and
String
parameters because
null
can match both.
Overriding implements
subtype polymorphism. It involves the implementation or replacement of a method from a non-final superclass or interface. This mechanism allows behavior where an instance is stored under the type of its parent, but the method implementation specific to the actual subtype is used. For example:
List<String> list = new LinkedList<>();
list.add("foo");
Here, the
add
method called is common to all lists, but the element is added specifically to the
linked list.
The specific method choice is made at the last moment, during program execution, based on the object's type. This is known as
late binding or
dynamic dispatch.
Overriding is directly related to the
Liskov Substitution Principle (LSP): in well-designed object-oriented code, an overridden method should not be distinguishable from the original
for the calling code.
It's customary to annotate overridden methods with
@Override
. This annotation is optional, however if a method marked with
@Override
does not actually override anything, compilation will fail.
When overriding, you can narrow the range of thrown exceptions or the return type, and you can replace the access modifier with a less restrictive one.
Static methods cannot be overridden, only overloaded.
You can read more about the inner workings of method binding in
this article.