Loading content...
In our exploration of inheritance, we learned that method overriding allows subclasses to replace inherited behavior with specialized implementations. But that was only half the story. Overriding isn't just about customization—it's the fundamental mechanism that makes runtime polymorphism possible.
When you call a method on an object reference, and the actual object might be of any subtype, how does the system know which implementation to execute? The answer lies in the sophisticated interplay between overriding and dynamic method dispatch. This connection transforms simple code reuse into something far more powerful: behavior that adapts based on actual object types at runtime.
By the end of this page, you'll understand why method overriding is inseparable from polymorphism, how overriding enables substitutability, the mechanics of how overridden methods get selected at runtime, and why this mechanism is central to flexible object-oriented design.
Before connecting overriding to polymorphism, let's ensure we have a precise understanding of what method overriding actually means:
Method Overriding occurs when a subclass provides a specific implementation for a method that is already defined in its parent class. The method in the subclass must have:
When these conditions are met, the subclass method replaces the parent's implementation for instances of the subclass.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
// Parent class with a method to be overriddenpublic class Document { private String content; public Document(String content) { this.content = content; } public String getContent() { return content; } // Method that subclasses will override public String render() { return content; // Default: return raw content }} // Child class that OVERRIDES the render methodpublic class MarkdownDocument extends Document { public MarkdownDocument(String content) { super(content); } @Override // Annotation indicates intentional override public String render() { // Convert Markdown to HTML (simplified) return convertMarkdownToHtml(getContent()); } private String convertMarkdownToHtml(String markdown) { // Process **bold**, *italic*, etc. return markdown .replaceAll("\*\*(.+?)\*\*", "<strong>$1</strong>") .replaceAll("\*(.+?)\*", "<em>$1</em>"); }} // Another child class with its own overridepublic class RichTextDocument extends Document { public RichTextDocument(String content) { super(content); } @Override public String render() { // Apply rich text styling return applyRichTextFormatting(getContent()); } private String applyRichTextFormatting(String text) { // RTF-specific processing return "<div class='rich-text'>" + text + "</div>"; }}In this example, both MarkdownDocument and RichTextDocument override the render() method from Document. Each provides a specialized implementation appropriate to its document type. So far, this is basic inheritance—customization of behavior in subclasses.
But here's where things get interesting: What happens when we store these objects in a variable typed as the parent class?
The true power of overriding emerges when we work with objects through parent-class references. This is the essence of runtime polymorphism:
The declared type determines what you can call. The actual type determines which implementation runs.
Let's see this in action:
123456789101112131415161718192021222324252627282930313233343536373839404142
public class DocumentProcessor { public void processAndDisplay(Document document) { // The reference type is Document (parent class) // But the actual object could be ANY subclass String rendered = document.render(); // Which render() runs? System.out.println("=== Rendered Output ==="); System.out.println(rendered); System.out.println("======================="); } public static void main(String[] args) { DocumentProcessor processor = new DocumentProcessor(); // Three different document types Document plain = new Document("Just plain text"); Document markdown = new MarkdownDocument("This is **bold** and *italic*"); Document richText = new RichTextDocument("Formatted content here"); // ALL stored as Document references // But each calls its OWN render() implementation! processor.processAndDisplay(plain); // Output: Just plain text processor.processAndDisplay(markdown); // Output: This is <strong>bold</strong> and <em>italic</em> processor.processAndDisplay(richText); // Output: <div class='rich-text'>Formatted content here</div> // We can even collect them: Document[] documents = { plain, markdown, richText }; for (Document doc : documents) { // Same method call, THREE different behaviors! processor.processAndDisplay(doc); } }}Notice how processAndDisplay() doesn't know or care which specific Document type it receives. It calls render() on the Document reference, but the ACTUAL behavior depends on what TYPE of object is behind that reference. This is polymorphism—the same interface (the render() method) produces different behavior (many forms) based on the actual object type.
Why is this powerful?
Without polymorphism through overriding, the processAndDisplay method would need to know every possible document type and have conditional logic to handle each one:
// WITHOUT polymorphism - fragile and unmaintainable
public void processAndDisplay(Document document) {
String rendered;
if (document instanceof MarkdownDocument) {
rendered = ((MarkdownDocument) document).renderMarkdown();
} else if (document instanceof RichTextDocument) {
rendered = ((RichTextDocument) document).renderRichText();
} else {
rendered = document.getContent();
}
// Every new document type requires modifying this method!
}
With polymorphism through overriding:
processAndDisplayrender() appropriatelyPolymorphism through overriding isn't just a convenient trick—it embodies a fundamental principle of object-oriented design: substitutability.
Substitutability means that wherever code expects an object of a parent type, any subtype object can be used in its place without breaking correctness. This is formalized as the Liskov Substitution Principle (LSP), which we'll explore in depth later.
Overriding is what makes substitutability work:
Key insight: The client code (processAndDisplay) is decoupled from specific implementations. It depends only on the abstraction (the Document class and its render() contract). This means:
render() for predictable testingThis decoupling is the practical benefit of substitutability—and it's only possible because overriding exists.
For substitutability to work correctly, overriding methods must honor the behavioral contract of the parent—not just the signature. A MarkdownDocument that overrides render() to delete the document content instead of rendering it would break substitutability. The signature would match, but the behavior would violate expectations. This is why LSP matters.
A critical question emerges: Why can't the compiler figure out which overridden method to call?
The answer reveals why runtime polymorphism requires dynamic dispatch:
Compile Time vs. Runtime Type Information:
At compile time, the compiler only knows the declared (static) type of a variable. At runtime, the JVM/interpreter knows the actual (dynamic) type of the object.
1234567891011121314151617181920212223242526272829303132333435363738394041424344
public class DocumentFactory { public Document createDocument(String type, String content) { // The return type is Document (parent class) // The compiler cannot know which ACTUAL type will be created if (type.equals("markdown")) { return new MarkdownDocument(content); } else if (type.equals("richtext")) { return new RichTextDocument(content); } else if (type.equals("pdf")) { return new PdfDocument(content); } else { return new Document(content); // Plain document } }} public class Application { public static void main(String[] args) { DocumentFactory factory = new DocumentFactory(); // The "type" comes from user input, config file, database, etc. String docType = getUserInput(); // Could be ANYTHING // Compiler knows this is a Document // But cannot know if it's Markdown, RichText, PDF, or plain Document doc = factory.createDocument(docType, "Hello world"); // Which render() implementation should run? // The compiler CANNOT know—the value of docType is only // known at RUNTIME when the user provides input! String result = doc.render(); System.out.println(result); } private static String getUserInput() { // Simulating runtime input java.util.Scanner scanner = new java.util.Scanner(System.in); return scanner.nextLine(); }}The critical insight: In this example, the actual type of doc depends on user input. There's no way for static analysis to predict it. The decision about which render() to call must wait until execution time.
This is true for many real-world scenarios:
In all these cases, compile-time resolution is impossible. The system must delay method selection until runtime—which is exactly what virtual method dispatch provides.
Understanding that overriding enables runtime polymorphism fundamentally changes how we design systems. Here are the key implications:
Real-World Design Pattern: Template Method
One of the clearest illustrations of overriding for polymorphism is the Template Method pattern. The parent class defines the structure of an algorithm, while subclasses override specific steps:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
public abstract class ReportGenerator { // Template method — defines the algorithm structure // Marked final so subclasses can't change the structure public final String generateReport(List<DataPoint> data) { String header = createHeader(); // May be overridden String body = formatData(data); // May be overridden String footer = createFooter(); // May be overridden String styled = applyTheme(header + body + footer); // May be overridden return styled; } // Hooks — meant to be overridden protected String createHeader() { return "=== Report ===\n"; // Default implementation } // Abstract — MUST be overridden protected abstract String formatData(List<DataPoint> data); // Hooks — meant to be overridden protected String createFooter() { return "\n=== End ==="; // Default implementation } protected String applyTheme(String content) { return content; // No styling by default }} // Subclass for HTML reportspublic class HtmlReportGenerator extends ReportGenerator { @Override protected String createHeader() { return "<html><head><title>Report</title></head><body>\n"; } @Override protected String formatData(List<DataPoint> data) { StringBuilder html = new StringBuilder("<table>"); for (DataPoint point : data) { html.append("<tr><td>").append(point.getLabel()) .append("</td><td>").append(point.getValue()) .append("</td></tr>"); } html.append("</table>"); return html.toString(); } @Override protected String createFooter() { return "</body></html>"; } @Override protected String applyTheme(String content) { // Add CSS styling return content.replace("<head>", "<head><style>table { border-collapse: collapse; }</style>"); }} // Subclass for CSV exportpublic class CsvReportGenerator extends ReportGenerator { @Override protected String createHeader() { return "label,value\n"; // CSV header row } @Override protected String formatData(List<DataPoint> data) { StringBuilder csv = new StringBuilder(); for (DataPoint point : data) { csv.append(point.getLabel()).append(",") .append(point.getValue()).append("\n"); } return csv.toString(); } @Override protected String createFooter() { return ""; // No footer for CSV } // applyTheme not overridden — CSV doesn't support styling}In this pattern:
generateReport() method defines the common algorithm structurecreateHeader, formatData, etc.) are overridden for customizationReportGenerator references—it doesn't know if it's getting HTML, CSV, PDF, or any other formatWe've established the critical connection between method overriding and runtime polymorphism. Let's consolidate the key insights:
What's Next:
Now that we understand why overriding enables polymorphism, we need to understand how the runtime actually selects the right override to execute. In the next page, we'll dive deep into Virtual Method Dispatch—the mechanism that makes this magic happen under the hood.
You now understand how method overriding serves as the mechanism enabling runtime polymorphism. Overriding isn't just about customization—it's what allows the same method call to exhibit different behaviors based on actual object types. This is the heart of flexible, substitutable object-oriented design.