Lesson 53 of 140
Written lesson · 17 min

Method Overriding and Runtime Polymorphism

Override parent methods with `@Override`, see how runtime dispatch picks the child implementation, and keep the parent contract intact.

Preview

You are viewing a free preview lesson.

Lesson Overview

So far a child class has inherited its parent's behavior unchanged. Often, though, each child needs to do the same job its own way. That is method overriding, one of the most important behaviors of inheritance. Suppose we need to know the estimated learning time for every type of lesson content:

text
VideoLesson
→ video duration

ArticleLesson
→ reading time

QuizLesson
→ question count

The operation, calculateEstimatedMinutes(), is the same for every type of content, but the implementation is different:

java
ContentItem content = new VideoLesson(1L, "Method Overriding",
        "https://cdn.tarkev.com/videos/overriding", 18);
System.out.println(content.calculateEstimatedMinutes());

Even though the reference type is ContentItem, the actual VideoLesson implementation executes at runtime. This is runtime polymorphism: callers use one common operation, and each object supplies its own behavior.

Learning Objectives

After completing this lesson, you will be able to:

  • Override a parent method in a child class
  • Use @Override correctly
  • Explain runtime method dispatch
  • Execute child implementations through a parent reference
  • Use super.method()
  • Distinguish overriding from overloading
  • Identify a valid override signature
  • Design child behavior while preserving the parent contract

What Is Method Overriding?

When a child class provides a new implementation of an inherited instance method from the parent class, it is called method overriding. Parent:

java
public class ContentItem {

    public int calculateEstimatedMinutes() {
        return 0;
    }
}

Child:

java
public class VideoLesson extends ContentItem {

    private final int durationInMinutes;

    public VideoLesson(long id, String title, int durationInMinutes) {
        super(id, title);
        this.durationInMinutes = durationInMinutes;
    }

    @Override
    public int calculateEstimatedMinutes() {
        return durationInMinutes;
    }
}

VideoLesson provides its own implementation instead of using the parent implementation. To override a method, generally:

  • Method name must be the same
  • Parameter list must be the same
  • Return type must be compatible
  • Access level must be compatible
  • Parent method must be overridable

The VideoLesson method above meets all of these, so it is a valid override.

The @Override Annotation

Put @Override before an overriding method, as VideoLesson does above. This tells the compiler:

This method is intended to override an inherited method.

Why does that matter? Suppose the parent method is:

java
public int calculateEstimatedMinutes() {
    return 0;
}

The child accidentally writes:

java
public int calculateEstimateMinutes() {
    return durationInMinutes;
}

The names are different: calculateEstimatedMinutes and calculateEstimateMinutes. Without @Override, this compiles as a separate new method, and VideoLesson silently keeps the parent's return 0. With:

java
@Override
public int calculateEstimateMinutes() {
    return durationInMinutes;
}

the compiler reports an error:

text
error: method does not override or implement a method from a supertype

Useful rule:

Use @Override with every method that is intended to override another method.

One Parent, Three Overrides

A Common Parent Type

We add one method to the ContentItem from the previous lessons (id, title, publish() and the getters stay as they were):

java
public int calculateEstimatedMinutes() {
    return 0;
}

For now, calculateEstimatedMinutes() returns 0 by default. Later, after learning abstract methods, this design can be improved further.

Overriding in Each Child

For a VideoLesson, the estimate is simply its duration, as in the override above. An ArticleLesson with a wordCount field estimates from reading speed:

java
private static final int WORDS_PER_MINUTE = 200;

@Override
public int calculateEstimatedMinutes() {
    int estimatedMinutes = wordCount / WORDS_PER_MINUTE;
    return Math.max(1, estimatedMinutes);
}

A QuizLesson with a questionCount field allows two minutes per question:

java
private static final int MINUTES_PER_QUESTION = 2;

@Override
public int calculateEstimatedMinutes() {
    return questionCount * MINUTES_PER_QUESTION;
}

The constructors validate these fields as usual; the full classes are in the Complete Example below. The same operation, calculateEstimatedMinutes(), provides different behavior for different types.

Parent Reference, Child Object

With ContentItem content = new VideoLesson(1L, "Runtime Polymorphism", "https://cdn.tarkev.com/videos/runtime", 18); the compile-time type is ContentItem and the runtime type is VideoLesson. Now int minutes = content.calculateEstimatedMinutes(); gives 18: the VideoLesson implementation executed.

Runtime Method Dispatch

The compiler first checks: does ContentItem have calculateEstimatedMinutes()? Yes, so the call is allowed. At runtime, Java then looks at the actual object, a VideoLesson, and executes the most specific overridden implementation. Conceptually:

Drawing diagram…

Runtime Polymorphism

Multiple child objects can be used through parent-type references:

java
ContentItem video = new VideoLesson(1L, "Video",
        "https://example.com/video", 20);
ContentItem article = new ArticleLesson(2L, "Article", 1_000);
ContentItem quiz = new QuizLesson(3L, "Quiz", 10);

System.out.println(video.calculateEstimatedMinutes());
System.out.println(article.calculateEstimatedMinutes());
System.out.println(quiz.calculateEstimatedMinutes());

Output:

text
20
5
20

Same method call. Different runtime behavior.

Why Is Polymorphism Useful?

Without polymorphism, the caller might write conditions based on type:

java
if (type.equals("VIDEO")) {
    // video calculation
} else if (type.equals("ARTICLE")) {
    // article calculation
} else if (type.equals("QUIZ")) {
    // quiz calculation
}

The caller must know the details of every content type. With polymorphism, the caller writes int minutes = content.calculateEstimatedMinutes(); and each object knows how to perform its own calculation. The caller only needs to know the common contract.

One Method, Multiple Implementations

java
private static void printEstimate(ContentItem content) {
    System.out.println(content.calculateEstimatedMinutes());
}

Call:

java
printEstimate(video);
printEstimate(article);
printEstimate(quiz);

printEstimate() does not check the child type. Runtime dispatch selects the appropriate implementation.

Calling Parent Behavior with super.method()

A child override can reuse the parent implementation. Parent:

java
public String createSummary() {
    return "Title: " + getTitle();
}

Child:

java
@Override
public String createSummary() {
    return super.createSummary() + ", Type: Video";
}

Here, super.createSummary() explicitly calls the parent implementation. A child can either replace or extend parent behavior. This override completely replaces the parent implementation:

java
@Override
public int calculateEstimatedMinutes() {
    return durationInMinutes;
}

This one reuses the parent result and adds more information:

java
@Override
public String createSummary() {
    return super.createSummary()
            + ", Duration: " + durationInMinutes + " minutes";
}

When should you use super.method()? Use it when the child genuinely wants to preserve or extend the parent behavior. Do not call super.someMethod() mechanically. Ask:

Is the parent implementation a meaningful part of the child behavior?

Overriding vs. Overloading

These are different concepts. Overriding means a parent-child relationship, the same method signature, and runtime-selected behavior; calculateEstimatedMinutes() in VideoLesson is an example. Overloading, which we learned earlier, means the same method name with a different parameter list:

java
public void publish() {
}
java
public void publish(boolean notifyLearners) {
}

This is not runtime polymorphism.

OverridingOverloading
Parent-child relationshipParent-child relationship not required
Same parameter listDifferent parameter list
Runtime behavior selectionCompile-time method selection
Specializes inherited behaviorProvides multiple calling forms
Uses @OverrideDoes not use @Override

Same Name Does Not Always Mean Override

Parent:

java
public void publish() {
}

Child:

java
public void publish(boolean notify) {
}

This is not overriding. It is another overload, and the inherited publish() still exists.

Return Type and Parameter Rules

Return Type Rules

The same return type is always valid. For object return types, a child can return a more specific subtype. This is called a covariant return type. Parent:

java
public ContentItem copy() {
    return this;
}

Child:

java
@Override
public VideoLesson copy() {
    return this;
}

For now, remember:

An override return type must be compatible with the parent method.

A primitive return type must stay exactly the same. Parent:

java
public int calculateEstimatedMinutes() {
    return 10;
}

Child (does not compile):

java
@Override
public long calculateEstimatedMinutes() {
    return 10L;
}
text
error: calculateEstimatedMinutes() in VideoLesson cannot override calculateEstimatedMinutes() in ContentItem
  return type long is not compatible with int

Parameters Must Match

Parent:

java
public void updateTitle(String title) {
}

Child:

java
public void updateTitle(Object title) {
}

This is not an override, because the parameter type changed. Correct:

java
@Override
public void updateTitle(String title) {
}

Visibility Cannot Be Reduced

Parent:

java
public void publish() {
}

The child cannot write:

java
@Override
protected void publish() {
}

because the parent promised a public operation:

text
error: publish() in VideoLesson cannot override publish() in ContentItem
  attempting to assign weaker access privileges; was public

An override can keep or widen the parent's access level (for example, protected to public), but never narrow it. Why? The caller may have:

java
ContentItem content = new VideoLesson(...);
content.publish();

The parent contract says this operation is public. The child cannot make that contract less accessible.

What Is Not Overridden

Private Methods Are Not Overridden

Parent:

java
private void validate() {
}

Child:

java
private void validate() {
}

These are separate methods. The parent private method is not visible to the child, so no overriding occurs. Adding @Override to the child method would be a compile error.

final Methods Cannot Be Overridden

Parent:

java
public final long getId() {
    return id;
}

The child cannot redefine it:

java
@Override
public long getId() {
    return 100L;
}

Compile error:

text
error: getId() in VideoLesson cannot override getId() in ContentItem
  overridden method is final

Constructors Are Not Overridden

Even though the parameter lists of the parent constructor ContentItem(long id) and the child constructor VideoLesson(long id) look the same, constructors do not override one another. Constructors initialize their own classes.

Static Methods Are Not Runtime Polymorphic

Suppose the parent has:

java
public static String getCategory() {
    return "CONTENT";
}

Child:

java
public static String getCategory() {
    return "VIDEO";
}

This is not overriding. The child's static method hides the parent's static method.

A static method is class-level behavior. Java chooses it at compile time from the declared type, not from the runtime object. With ContentItem content = new VideoLesson(...), the call content.getCategory() runs the ContentItem version and returns "CONTENT". Putting @Override on a static method is a compile error (static methods cannot be annotated with @Override). Preferred usage:

java
ContentItem.getCategory();
VideoLesson.getCategory();

Do not depend on static methods for runtime polymorphic behavior.

Fields Are Not Runtime Polymorphic

Avoid designing polymorphic behavior with same-name fields. Parent:

java
public String type = "CONTENT";

Child:

java
public String type = "VIDEO";

With ContentItem content = new VideoLesson(...);, field access is based on the declared reference type, not runtime method dispatch: content.type reads "CONTENT", even though the object is a VideoLesson. For polymorphic behavior, use a method instead:

java
public String getContentType() {
    return "CONTENT";
}

Child:

java
@Override
public String getContentType() {
    return "VIDEO";
}

Parent Contract Must Still Hold

A method can be syntactically valid and still be a bad override. Suppose the parent contract is:

text
calculateEstimatedMinutes()
→ returns zero or a positive estimate

Bad child:

java
@Override
public int calculateEstimatedMinutes() {
    return -100;
}

The compiler accepts the signature, but the behavior breaks the parent contract.

The Compiler Checks Syntax, Not Domain Meaning

The compiler can check:

  • Method name
  • Parameters
  • Return type
  • Visibility
  • @Override

The compiler cannot fully decide:

  • Whether the result is meaningful
  • Whether domain invariants are preserved
  • Whether the method unexpectedly changes unrelated state

That is a design responsibility.

Avoid Surprising Side Effects

calculateEstimatedMinutes() sounds like a query. Bad override:

java
@Override
public int calculateEstimatedMinutes() {
    publish();
    return durationInMinutes;
}

The caller asked for an estimate. It did not ask to publish the content. An override should preserve the expected meaning of the parent operation.

If a Child Cannot Support Parent Behavior

Suppose the parent guarantees public boolean publish() for all content. A child that must do:

java
@Override
public boolean publish() {
    throw new UnsupportedOperationException();
}

may indicate that the hierarchy is wrong. Inheritance works best when every child can meaningfully satisfy the parent contract.

Complete Example

ContentItem.java

java
public class ContentItem {

    private final long id;
    private final String title;

    private boolean published;

    public ContentItem(long id, String title) {
        if (id <= 0) {
            throw new IllegalArgumentException("Content ID must be positive.");
        }

        if (title == null || title.isBlank()) {
            throw new IllegalArgumentException("Content title is required.");
        }

        this.id = id;
        this.title = title.strip();
        this.published = false;
    }

    public int calculateEstimatedMinutes() {
        return 0;
    }

    public String getContentType() {
        return "CONTENT";
    }

    public boolean publish() {
        if (published) {
            return false;
        }

        published = true;
        return true;
    }

    public final long getId() {
        return id;
    }

    public final String getTitle() {
        return title;
    }

    public final boolean isPublished() {
        return published;
    }
}

VideoLesson.java

java
public class VideoLesson extends ContentItem {

    private final String videoUrl;
    private final int durationInMinutes;

    public VideoLesson(
            long id, String title, String videoUrl, int durationInMinutes) {
        super(id, title);

        if (videoUrl == null || videoUrl.isBlank()) {
            throw new IllegalArgumentException("Video URL is required.");
        }

        if (durationInMinutes <= 0) {
            throw new IllegalArgumentException(
                    "Video duration must be positive.");
        }

        this.videoUrl = videoUrl.strip();
        this.durationInMinutes = durationInMinutes;
    }

    @Override
    public int calculateEstimatedMinutes() {
        return durationInMinutes;
    }

    @Override
    public String getContentType() {
        return "VIDEO";
    }

    public String getVideoUrl() {
        return videoUrl;
    }
}

ArticleLesson.java

java
public class ArticleLesson extends ContentItem {

    private static final int WORDS_PER_MINUTE = 200;

    private final int wordCount;

    public ArticleLesson(long id, String title, int wordCount) {
        super(id, title);

        if (wordCount <= 0) {
            throw new IllegalArgumentException("Word count must be positive.");
        }

        this.wordCount = wordCount;
    }

    @Override
    public int calculateEstimatedMinutes() {
        int estimatedMinutes = wordCount / WORDS_PER_MINUTE;
        return Math.max(1, estimatedMinutes);
    }

    @Override
    public String getContentType() {
        return "ARTICLE";
    }
}

QuizLesson.java

java
public class QuizLesson extends ContentItem {

    private static final int MINUTES_PER_QUESTION = 2;

    private final int questionCount;

    public QuizLesson(long id, String title, int questionCount) {
        super(id, title);

        if (questionCount <= 0) {
            throw new IllegalArgumentException(
                    "Question count must be positive.");
        }

        this.questionCount = questionCount;
    }

    @Override
    public int calculateEstimatedMinutes() {
        return questionCount * MINUTES_PER_QUESTION;
    }

    @Override
    public String getContentType() {
        return "QUIZ";
    }
}

Main.java

java
public class Main {

    public static void main(String[] args) {
        ContentItem video = new VideoLesson(1L, "Method Overriding",
                "https://cdn.tarkev.com/videos/overriding", 18);
        ContentItem article =
                new ArticleLesson(2L, "Runtime Polymorphism", 1_200);
        ContentItem quiz = new QuizLesson(3L, "Overriding Assessment", 10);

        printEstimate(video);
        printEstimate(article);
        printEstimate(quiz);
    }

    private static void printEstimate(ContentItem content) {
        System.out.println("Type: " + content.getContentType());
        System.out.println("Title: " + content.getTitle());
        System.out.println(
                "Estimated time: "
                + content.calculateEstimatedMinutes() + " minutes");
        System.out.println();
    }
}

Output:

text
Type: VIDEO
Title: Method Overriding
Estimated time: 18 minutes

Type: ARTICLE
Title: Runtime Polymorphism
Estimated time: 6 minutes

Type: QUIZ
Title: Overriding Assessment
Estimated time: 20 minutes

Notice that printEstimate(ContentItem content) does not know whether the object is a VideoLesson, an ArticleLesson or a QuizLesson. Runtime dispatch selects the appropriate child behavior.

Important Terms

Method Overriding

A child class providing a compatible implementation of a parent instance method.

@Override

A compiler annotation indicating an intended override.

Runtime Polymorphism

Different child behavior executing through the same parent contract.

Runtime Dispatch

Selecting an overridden instance method according to the actual runtime object.

super.method()

Explicitly calls the parent implementation.

Predict the Result

Question 1

java
ContentItem content = new VideoLesson(1L, "Overriding",
        "https://example.com/video", 15);
System.out.println(content.calculateEstimatedMinutes());

What prints?

Answer

text
15

The runtime object is VideoLesson.

Question 2

Parent:

java
public String getType() {
    return "CONTENT";
}

Child:

java
@Override
public String getType() {
    return "VIDEO";
}

Then:

java
ContentItem content = new VideoLesson(...);
System.out.println(content.getType());

What prints?

Answer

text
VIDEO

Instance-method runtime dispatch selects the child override.

Question 3

Can a child override public final void publish()?

Answer

No. A final method cannot be overridden.

Question 4

Parent:

java
private void validate() {
}

Child declares:

java
private void validate() {
}

Is this overriding?

Answer

No. The parent private method is not visible to the child.

Common Mistakes

Forgetting @Override

Without it, a misspelled name such as calculateEstimateMinutes() compiles as a new method, and the child silently keeps the parent's return 0 (see "The @Override Annotation"). With @Override, the compiler reports the mistake.

Changing the Parameters

calculateEstimatedMinutes(int speed) in the child does not override calculateEstimatedMinutes(); it is an overload. Calls through a ContentItem reference still run the parent's version.

Expecting Static Methods or Fields to Be Polymorphic

With ContentItem content = new VideoLesson(...), content.getCategory() (static) returns "CONTENT" and content.type reads the parent's field. Use an overridden instance method such as getContentType() when behavior should depend on the object.

Breaking the Parent's Contract

An override can compile and still be wrong: a negative estimate, or an estimate that also publishes the content. Callers rely on the parent's meaning, so keep it (see "Parent Contract Must Still Hold").

Practice Exercise 1: Quiz Duration

Solutions: lesson 4.10.

QuizLesson should override calculateEstimatedMinutes() with the rule: 3 minutes per question.

Practice Exercise 2: Content Type

Override the parent's getContentType(), which returns "CONTENT":

text
VideoLesson → VIDEO
ArticleLesson → ARTICLE
QuizLesson → QUIZ

Practice Exercise 3: Parent Reference

java
ContentItem content = new ArticleLesson(1L, "Polymorphism", 800);
content.calculateEstimatedMinutes();

Which implementation executes for the second line? Explain why.

Practice Exercise 4: Override or Overload?

Parent:

java
public void process(String value) {
}

Child A:

java
@Override
public void process(String value) {
}

Child B:

java
public void process(int value) {
}

Classify each.

Practice Exercise 5: Fix the Visibility

Fix the protected void publish() override from "Visibility Cannot Be Reduced" and explain why the fix is needed.

Practice Exercise 6: Method Instead of Field

Replace the field public String type = "CONTENT"; with a polymorphic getContentType() method.

Practice Exercise 7: Contract Review

The parent promises:

text
calculateEstimatedMinutes()
returns a non-negative value

Review:

java
@Override
public int calculateEstimatedMinutes() {
    return -1;
}

Why is the signature valid but the design wrong?

Knowledge Check

Question 1

What is method overriding?

Question 2

What must the parameter list of an overriding method look like?

Question 3

Why is @Override useful?

Question 4

What is runtime method dispatch?

Question 5

Can a parent reference execute a child override?

Question 6

What does super.method() do?

Question 7

What is the difference between overriding and overloading?

Question 8

Can a child reduce the visibility of a public parent method?

Question 9

Are private methods overridden?

Question 10

Can a final method be overridden?

Question 11

Are static methods runtime polymorphic?

Question 12

Are fields runtime polymorphic?

Question 13

Does a correct method signature guarantee correct domain behavior?

Question 14

Why is preserving the parent contract important?

Knowledge Check Answers

Answer 1

When a child provides a compatible new implementation of a parent instance method with the same parameter signature.

Answer 2

It must match the parent method's parameter list.

Answer 3

The compiler verifies that the intended method really overrides another method.

Answer 4

Selecting the overridden instance method according to the actual runtime object type.

Answer 5

Yes.

Answer 6

It explicitly calls the parent implementation.

Answer 7

Overriding specializes inherited behavior and is selected at runtime. Overloading creates method variations with different parameter lists and is selected at compile time.

Answer 8

No.

Answer 9

No.

Answer 10

No.

Answer 11

No. Static behavior is class-level. A child static method with the same signature hides the parent's method; it does not override it.

Answer 12

No. Runtime polymorphism applies to instance-method behavior.

Answer 13

No. The compiler does not verify domain meaning.

Answer 14

So that code expecting the parent type can still work meaningfully and predictably with child objects.

Lesson Summary

  • Method overriding lets a child replace or extend an inherited instance method with the same parameter list and a compatible return type; @Override makes the compiler check it.
  • Runtime dispatch runs the implementation of the actual object, so one call through a parent reference behaves differently per child. That is runtime polymorphism, and it frees callers from type checks.
  • super.method() reuses the parent implementation inside an override.
  • Overriding is selected at runtime; overloading (different parameters) at compile time.
  • An override cannot narrow visibility. Private, final and static methods, constructors and fields are not overridden.
  • A correct signature does not guarantee correct behavior: an override must keep the meaning of the parent operation.

Think first

Coming soon

Checkpoint questions that ask you to commit to an answer before the lesson shows it.