Inheritance, Interfaces, and Polymorphism
Method Overriding and Runtime Polymorphism
You are viewing a free preview lesson.
Lesson Overview
In previous lessons, we learned:
- How a child class extends a parent class
- How to call a parent constructor using
super(...) - How a child object inherits parent behavior
Now we will learn one of the most important behaviors of inheritance: method overriding.
Suppose we need to know the estimated learning time for every type of lesson content.
VideoLesson
→ video duration
ArticleLesson
→ reading time
QuizLesson
→ question count
The operation is the same for every type of content:
calculateEstimatedMinutes()
But the implementation is different.
ContentItem content =
new VideoLesson(
1L,
"Method Overriding",
"https://cdn.liveklass.io/videos/overriding",
18
);
System.out.println(
content.calculateEstimatedMinutes()
);
Even though the reference type is ContentItem, the actual VideoLesson implementation can execute at runtime.
This is runtime polymorphism.
In this lesson, we will learn:
- Method overriding
@Override- Runtime method dispatch
- Parent reference and child object
super.method()- Overriding vs. overloading
- Return-type and visibility rules
- Private, static, and
finalmethods - Preserving the parent contract
Learning Objectives
After completing this lesson, you will be able to:
- Override a parent method in a child class
- Use
@Overridecorrectly - 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:
public class ContentItem {
public int calculateEstimatedMinutes() {
return 0;
}
}
Child:
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.
Basic Requirements for Overriding
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
Parent:
public int calculateEstimatedMinutes() {
return 0;
}
Child:
@Override
public int calculateEstimatedMinutes() {
return durationInMinutes;
}
This is a valid override.
The @Override Annotation
Use:
@Override
before an overriding method.
@Override
public int calculateEstimatedMinutes() {
return durationInMinutes;
}
This tells the compiler:
This method is intended to override an inherited method.
Why @Override Matters
Suppose the parent method is:
public int calculateEstimatedMinutes() {
return 0;
}
The child accidentally writes:
public int calculateEstimateMinutes() {
return durationInMinutes;
}
The names are different:
calculateEstimatedMinutes
calculateEstimateMinutes
Without @Override, this can become a new method.
With:
@Override
public int calculateEstimateMinutes() {
return durationInMinutes;
}
the compiler will report an error.
Useful rule:
Use
@Overridewith every method that is intended to override another method.
A Common Parent Type
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 boolean publish() {
if (published) {
return false;
}
published =
true;
return true;
}
public long getId() {
return id;
}
public String getTitle() {
return title;
}
public boolean isPublished() {
return published;
}
}
For now:
calculateEstimatedMinutes()
returns 0 by default.
Later, after learning abstract methods, this design can be improved further.
Overriding in VideoLesson
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;
}
}
For a video lesson, the estimate is simply its duration.
Overriding in ArticleLesson
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
);
}
}
Overriding in QuizLesson
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;
}
}
The same operation:
calculateEstimatedMinutes()
provides different behavior for different types.
Parent Reference, Child Object
Consider:
ContentItem content =
new VideoLesson(
1L,
"Runtime Polymorphism",
"https://cdn.liveklass.io/videos/runtime",
18
);
There are two types:
Compile-time type
→ ContentItem
Runtime type
→ VideoLesson
Now:
int minutes =
content.calculateEstimatedMinutes();
Result:
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:
VideoLesson
and executes the most specific overridden implementation.
Conceptually:
content.calculateEstimatedMinutes()
↓
actual object = VideoLesson
↓
VideoLesson.calculateEstimatedMinutes()
Runtime Polymorphism
Multiple child objects can be used through parent-type references.
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
);
Calls:
System.out.println(
video.calculateEstimatedMinutes()
);
System.out.println(
article.calculateEstimatedMinutes()
);
System.out.println(
quiz.calculateEstimatedMinutes()
);
Output:
20
5
20
Same method call.
Different runtime behavior.
Why Is Polymorphism Useful?
Without polymorphism, the caller might write conditions based on type:
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:
int minutes =
content.calculateEstimatedMinutes();
each object knows how to perform its own calculation.
The caller only needs to know the common contract.
One Method, Multiple Implementations
private static void printEstimate(
ContentItem content
) {
System.out.println(
content.calculateEstimatedMinutes()
);
}
Call:
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:
public String createSummary() {
return "Title: "
+ getTitle();
}
Child:
@Override
public String createSummary() {
return super.createSummary()
+ ", Type: Video";
}
Here:
super.createSummary()
explicitly calls the parent implementation.
Replacing Parent Behavior
@Override
public int calculateEstimatedMinutes() {
return durationInMinutes;
}
Here, the child completely replaces the parent implementation.
Extending Parent Behavior
@Override
public String createSummary() {
return super.createSummary()
+ ", Duration: "
+ durationInMinutes
+ " minutes";
}
Here, the child reuses the parent result and adds more information.
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
- Parent-child relationship
- Same method signature
- Runtime-selected behavior
Parent:
public int calculateEstimatedMinutes() {
return 0;
}
Child:
@Override
public int calculateEstimatedMinutes() {
return durationInMinutes;
}
Overloading
We learned method overloading earlier.
Same method name, different parameter list:
public void publish() {
}
public void publish(
boolean notifyLearners
) {
}
This is not runtime polymorphism.
Overriding vs. Overloading Summary
| Overriding | Overloading |
|---|---|
| Parent-child relationship | Parent-child relationship not required |
| Same parameter list | Different parameter list |
| Runtime behavior selection | Compile-time method selection |
| Specializes inherited behavior | Provides multiple calling forms |
Uses @Override | Does not use @Override |
Same Name Does Not Always Mean Override
Parent:
public void publish() {
}
Child:
public void publish(
boolean notify
) {
}
This is not overriding.
It is another overload.
The inherited:
publish()
still exists.
Return Type Rules
The same return type is valid:
@Override
public int calculateEstimatedMinutes() {
return durationInMinutes;
}
For object return types, a child can sometimes return a more specific subtype.
Example:
Parent:
public ContentItem copy() {
return this;
}
Child:
@Override
public VideoLesson copy() {
return this;
}
For now, remember:
An override return type must be compatible with the parent method.
Primitive return types cannot arbitrarily change.
Invalid parent:
public int calculateEstimatedMinutes() {
return 10;
}
Child:
@Override
public long calculateEstimatedMinutes() {
return 10L;
}
This will not compile.
Parameters Must Match
Parent:
public void updateTitle(
String title
) {
}
Child:
public void updateTitle(
Object title
) {
}
This is not an override.
The parameter type changed.
Correct:
@Override
public void updateTitle(
String title
) {
}
Visibility Cannot Be Reduced
Parent:
public void publish() {
}
The child cannot write:
@Override
protected void publish() {
}
because the parent promised a public operation.
A valid child override must remain:
public
or otherwise not reduce accessibility.
Why Visibility Cannot Be Reduced
The caller may have:
ContentItem content =
new VideoLesson(...);
and call:
content.publish();
The parent contract says this operation is public.
The child cannot make that contract less accessible.
Private Methods Are Not Overridden
Parent:
private void validate() {
}
Child:
private void validate() {
}
These are separate methods.
The parent private method is not visible to the child, so normal overriding does not occur.
final Methods Cannot Be Overridden
Parent:
public final long getId() {
return id;
}
The child cannot redefine it:
@Override
public long getId() {
return 100L;
}
Compile error.
Constructors Are Not Overridden
Parent constructor:
ContentItem(
long id
)
Child constructor:
VideoLesson(
long id
)
Even though the parameter lists look the same, constructors do not override one another.
Constructors initialize their own classes.
Static Methods Are Not Runtime Polymorphic
Suppose the parent has:
public static String getCategory() {
return "CONTENT";
}
Child:
public static String getCategory() {
return "VIDEO";
}
This is not normal instance-method overriding.
A static method is class-level behavior.
Preferred usage:
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:
public String type =
"CONTENT";
Child:
public String type =
"VIDEO";
With:
ContentItem content =
new VideoLesson(...);
field access is based on the declared reference type, not runtime method dispatch.
For polymorphic behavior, use a method instead:
public String getContentType() {
return "CONTENT";
}
Child:
@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:
calculateEstimatedMinutes()
→ returns zero or a positive estimate
Bad child:
@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
Suppose:
calculateEstimatedMinutes()
sounds like a query.
Bad override:
@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:
@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
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
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
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
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
public class Main {
public static void main(
String[] args
) {
ContentItem video =
new VideoLesson(
1L,
"Method Overriding",
"https://cdn.liveklass.io/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:
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:
printEstimate(
ContentItem content
)
does not know whether the object is a:
VideoLesson
ArticleLesson
QuizLesson
Runtime dispatch selects the appropriate child behavior.
Common Mistakes
Forgetting @Override
Signature mistakes become harder to catch.
Changing the Parameters
Parent:
calculateEstimatedMinutes()
Child:
calculateEstimatedMinutes(
int speed
)
This is not overriding.
Reducing Visibility
A public parent method cannot become protected or private in the child.
Expecting Static Runtime Polymorphism
Static methods are class-level behavior.
Using Fields for Polymorphic Behavior
Same-name fields do not behave like overridden instance methods.
Calling Child-Specific API Through a Parent Type
ContentItem content =
new VideoLesson(...);
This will not compile:
content.getVideoUrl();
because ContentItem does not declare that method.
Breaking Parent Behavior
Correct syntax alone does not make an override semantically correct.
Overriding When Parent Behavior Is Already Correct
If the inherited implementation already fits the child, do not override it unnecessarily.
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.
Overloading
Using the same method name with different parameter lists.
Polymorphic Method
An instance method that can execute different implementations depending on the runtime object type.
Practice Exercise 1: Quiz Duration
QuizLesson should override:
calculateEstimatedMinutes()
Rule:
3 minutes per question
Practice Exercise 2: Content Type
Parent:
public String getContentType() {
return "CONTENT";
}
Override:
VideoLesson → VIDEO
ArticleLesson → ARTICLE
QuizLesson → QUIZ
Practice Exercise 3: Parent Reference
ContentItem content =
new ArticleLesson(
1L,
"Polymorphism",
800
);
Which implementation executes?
content.calculateEstimatedMinutes();
Explain why.
Practice Exercise 4: Override or Overload?
Parent:
public void process(
String value
) {
}
Child A:
@Override
public void process(
String value
) {
}
Child B:
public void process(
int value
) {
}
Classify each.
Practice Exercise 5: Fix the Visibility
Parent:
public void publish() {
}
Child:
@Override
protected void publish() {
}
Fix it and explain why.
Practice Exercise 6: Method Instead of Field
Replace:
public String type =
"CONTENT";
with a polymorphic method:
getContentType()
Practice Exercise 7: Contract Review
The parent promises:
calculateEstimatedMinutes()
returns a non-negative value
Review:
@Override
public int calculateEstimatedMinutes() {
return -1;
}
Why is the signature valid but the design wrong?
Predict the Result
Question 1
ContentItem content =
new VideoLesson(
1L,
"Overriding",
"https://example.com/video",
15
);
System.out.println(
content.calculateEstimatedMinutes()
);
What prints?
Question 2
Parent:
public String getType() {
return "CONTENT";
}
Child:
@Override
public String getType() {
return "VIDEO";
}
Then:
ContentItem content =
new VideoLesson(...);
System.out.println(
content.getType()
);
What prints?
Question 3
Can a child override:
public final void publish() {
}
Question 4
Parent:
private void validate() {
}
Child declares:
private void validate() {
}
Is this overriding?
Predict the Result Answers
Answer 1
15
The runtime object is VideoLesson.
Answer 2
VIDEO
Instance-method runtime dispatch selects the child override.
Answer 3
No.
A final method cannot be overridden.
Answer 4
No.
The parent private method is not visible to the child.
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.
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
In this lesson, we learned:
- Method overriding allows a child to specialize inherited instance behavior
- An overriding method uses the same parameter signature
@Overridehelps catch signature mistakes- A parent reference can hold a child object
- Runtime object type determines which overridden method executes
- This is runtime polymorphism
- The same parent method can execute different child implementations
super.method()calls the parent implementation- A child can replace or extend parent behavior
- Overriding and overloading are different concepts
- Overriding is runtime-selected
- Overloading is compile-time-selected
- Override return types must be compatible
- Child-method visibility cannot be narrower than the parent's
- Private methods are not overridden
finalmethods cannot be overridden- Constructors are not overridden
- Static methods do not provide runtime polymorphism
- Fields are not a substitute for polymorphic behavior
- Polymorphic differences should be modeled with methods
- A correct signature alone does not guarantee a correct behavioral contract
- Child behavior should preserve the meaning of the parent operation
- Runtime polymorphism lets callers use common behavior without knowing the concrete child type