Java Polymorphism and Method Overriding

Java polymorphism allows one declared reference type to represent different implementations.

Lesson 57 of 127Intermediate14 min read

Java polymorphism allows one declared reference type to represent different implementations. Overriding supplies the implementation selected for an instance call at runtime. This lesson compares overriding with overloading and field hiding, then shows why a superclass variable can still call a subclass method.

Review the related foundation lesson when you need a prerequisite. The complete programs below use Java 17-compatible syntax.

Runtime dispatch and override signatures

Polymorphism lets a common reference type refer to different implementations. Calling an overridden instance method chooses behavior from the actual object type.

An override replaces an inherited instance method implementation for compatible calls on a subclass object. Match the method name and parameter types; a covariant reference return type may be allowed. An override cannot reduce accessibility and cannot broaden checked exceptions beyond the inherited contract. The @Override annotation asks the compiler to verify the relationship and catches accidental overloads caused by a misspelled signature.

Declared types and overriding contracts

Overriding replaces inherited instance behavior with a compatible method. @Override asks the compiler to catch accidental signature mistakes.

The declared type determines which members and overloads the compiler considers. After a particular instance method signature is selected, runtime dispatch uses the actual object's implementation. A Shape variable can refer to a Circle and call an overridden area method, but it cannot call a Circle-only method without a suitable narrower type. This separates the compile-time interface from runtime behavior.

Overloading versus overriding

Overloading is selected using compile-time argument types; overriding dispatches at runtime. Fields and static methods do not follow the same overriding rules as instance methods.

Overloading chooses among different parameter lists at compilation, rather than discovering a new signature at runtime. Static methods are hidden, not dynamically overridden. Fields are also selected according to the reference expression's declared type; a same-named subclass field does not behave like an overridden method. Keeping these three cases separate prevents surprising output from otherwise similar-looking access expressions.

Interface-based callers

Design callers around a useful interface instead of checking each concrete class. This makes a new notification implementation easier to add without editing every caller.

A substitutable implementation preserves the expectations attached to the parent type. A subclass that arbitrarily refuses an operation guaranteed by the parent can defeat callers even when its override compiles. Use an interface to describe a capability and inject different implementations when the variability is behavior rather than shared state. That approach lets callers depend on the contract while tests supply controlled alternatives.

Running the examples

Save name.java, compile it with javac --release 17 name.java, and run java name. Use each program's own public class name and filename.

How NoticePolymorphism produces its output

Notice declares send(String), and DeskNotice supplies its public implementation. The Notice variable holds a DeskNotice instance, so notice.send("Tour starts") dispatches to that implementation. It prepends Desk: and returns the combined string, which println displays. The interface controls what can be called; the actual receiver controls this instance method's body.

How NotificationPolymorphism produces its output

The array stores references through the common Channel interface. Each loop iteration calls the same declared method, send, but the first receiver is an Email and the second is a Text. Their implementations produce different strings. A caller can add another Channel implementation without rewriting the loop or inspecting class names.

How FieldsVersusOverrides produces its output

The field expression item.name uses the declared Base type, so it reads Base.name. The instance method expression item.label dispatches to the Child object's override. Although both expressions use the same reference variable, fields and overridden instance methods follow different selection rules. Avoid hiding fields in ordinary designs because this difference is easily overlooked.

Check substitutability with a concrete caller

Imagine a caller that accepts Notice and expects send to return a description for every permitted message. EmailNotice and DeskNotice can format that description differently, but neither should unexpectedly reject inputs that the interface contract permits. Write a small helper that accepts Notice and exercise it with both implementations. This makes polymorphism visible at the call boundary rather than only inside a class hierarchy diagram. If a new implementation needs extra configuration, provide it at construction instead of teaching the caller to identify concrete classes. The caller should use the published operation; implementation-specific setup belongs outside the ordinary dispatch path.

Java Summary

  • Polymorphism lets a common reference type refer to different implementations. Calling an overridden instance method chooses behavior from the actual object type.
  • Overriding replaces inherited instance behavior with a compatible method. @Override asks the compiler to catch accidental signature mistakes.
  • Overloading is selected using compile-time argument types; overriding dispatches at runtime. Fields and static methods do not follow the same overriding rules as instance methods.
  • Design callers around a useful interface instead of checking each concrete class. This makes a new notification implementation easier to add without editing every caller.
  • Continue with Java exercises, Java quizzes and the Java study roadmap.

Java Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Java Polymorphism and Method Overriding in a complete program

Java
public class NoticePolymorphism  {
    interface Notice  {
        String send(String text);
    }
    static class DeskNotice implements Notice  {
        public String send(String text) {
            return "Desk: "+text;
        }
    }
    public static void main(String[] args)  {
        Notice notice = new DeskNotice();
        System.out.println(notice.send("Tour starts"));
    }
}
Output
Desk: Tour starts
Line-by-line explanation
  1. Line 12Evaluates notice.send("Tour starts") and prints Desk: Tour starts.
Example 2

One interface, two implementations

Java
public class NotificationPolymorphism  {
    interface Channel  {
        String send();
    }
    static class Email implements Channel  {
        public String send()  {
            return "email";
        }
    }
    static class Text implements Channel  {
        public String send()  {
            return "text";
        }
    }
    public static void main(String[] args)  {
        Channel[] channels =  {
            new Email(), new Text()
        };
        for (Channel channel : channels) System.out.println(channel.send());
    }
}
Output
email
text
Line-by-line explanation
  1. Line 19Evaluates channel.send() and prints the sequence email / text as the loop advances.
Example 3

Field access and method calls differ

Java
public class FieldsVersusOverrides  {
    static class Base  {
        String name = "base field";
        String label()  {
            return "base method";
        }
    }
    static class Child extends Base  {
        String name = "child field";
        @Override String label()  {
            return "child method";
        }
    }
    public static void main(String[] args)  {
        Base item = new Child();
        System.out.println(item.name);
        System.out.println(item.label());
    }
}
Output
base field
child method
Line-by-line explanation
  1. Line 16Evaluates item.name and prints base field.
  2. Line 17Evaluates item.label() and prints child method.

Java FAQ

Does overriding depend on the variable's type?

The declared type controls what calls compile, while the runtime object controls an overridden instance method's implementation. Both matter. A parent reference can call the parent's published methods and receive subclass behavior without gaining access to every subclass-specific method.

Can private methods be overridden?

No. A private method is not inherited as an overridable member. A subclass may declare a same-named method, but that is a separate declaration. Similarly, final methods prohibit overriding and static methods use hiding rather than instance dispatch.

What is the difference between compile-time and run-time polymorphism?

Compile-time polymorphism is overloading: the compiler chooses a method from the argument types. Run-time polymorphism is overriding: the JVM chooses the implementation from the actual object when the program runs. When people say "polymorphism" in Java they usually mean the run-time kind.

Can an overriding method change the return type?

Only to a subtype of the original return type, which is called a covariant return. If the parent declares Animal create(), a subclass may override it as Dog create(). Changing the return type to something unrelated is a compile error.

Can an overriding method be less visible or throw more exceptions?

No. An override can keep or widen the access level, for example from protected to public, but not narrow it. It may throw fewer or narrower checked exceptions, but not new or broader ones, because callers using the parent type must still be able to rely on its contract.

Java Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

Why add @Override?

Multiple choicemedium

Which operation uses runtime dispatch?

Java Quiz

2 questions to check what you learned.

  1. Can an override reduce public access to private?

    • No
    • Yes if the return type is unchanged
    • Yes if @Override is omitted
  2. Why can one Channel array hold Email and Text?

    • Both objects implement the Channel contract
    • The array ignores element types
    • Interfaces automatically copy all objects