Back to Blog
Java

Java Compile Time vs Runtime Polymorphism

java compile time vs runtime polymorphism: Understand the difference between compile-time and runtime polymorphism in Java, including method overloading, overriding, b...

JavaPolymorphismMethod OverloadingMethod OverridingDynamic BindingJVM
Diagram comparing compile-time method resolution with runtime dynamic dispatch in Java

java compile time vs runtime polymorphism requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

In Java, polymorphism appears in two distinct forms: compile-time polymorphism, achieved through method overloading, and runtime polymorphism, achieved through method overriding. The difference determines which method the compiler and JVM select, and when that selection happens. This article explains the mechanics, the binding differences, and how to choose between them in real code.

What Compile-Time Polymorphism Means in Java

Compile-time polymorphism in Java is realized through method overloading. Overloading allows a class to have multiple methods with the same name but different parameter lists. The compiler decides which method to call based on the number, types, and order of arguments at compile time. This is also known as static binding because the method resolution is fixed when the code is compiled.

public class Calculator { public int add(int a, int b) { return a + b; } public double add(double a, double b) { return a + b; } public int add(int a, int b, int c) { return a + b + c; } }

When you call calc.add(2, 3), the compiler picks the first method. If you pass 2.0, 3.0, it picks the second. This resolution happens entirely during compilation; the generated bytecode contains a direct reference to the chosen method. There is no runtime decision involved.

How Runtime Polymorphism Works

Runtime polymorphism is achieved through method overriding. A subclass provides its own implementation of a method inherited from a superclass. The method signature must match exactly. At runtime, the JVM determines which implementation to execute based on the actual object type, not the reference type. This is called dynamic binding or late binding.

class Animal { public void sound() { System.out.println("Some sound"); } } class Dog extends Animal { @Override public void sound() { System.out.println("Bark"); } } Animal a = new Dog(); a.sound(); // prints "Bark"

Here, the reference variable a is of type Animal, but the object it points to is a Dog. When sound() is invoked, the JVM looks up the actual class of the object and calls Dog.sound(). This decision is made at runtime, enabling polymorphic behavior where the same reference type can behave differently depending on the concrete object.

The Binding Difference: Static vs Dynamic Dispatch

The core distinction between compile-time and runtime polymorphism is when the method call is bound to a specific method implementation.

Static binding (compile-time) uses the declared type of the reference and the argument types to resolve the method. It is efficient because the compiler can emit a direct invocation. Overloaded methods are resolved this way.

Dynamic binding (runtime) uses the actual object type to resolve the method. The JVM maintains a virtual method table (vtable) for each class, mapping method signatures to their concrete implementations. When a virtual method is called, the JVM looks up the vtable entry for the object's class and dispatches to that implementation. This adds a small overhead compared to static binding, but it enables inheritance and interface-based polymorphism.

Performance and Runtime Cost of Dynamic Dispatch

Dynamic dispatch is not free, but modern JVMs mitigate the overhead. Each virtual method call requires an extra indirection through the vtable. However, the JIT compiler often applies inline caching or even devirtualization when it can determine the concrete type at runtime. For most applications, the performance difference between static and dynamic dispatch is negligible. The real cost appears in extremely hot paths where millions of calls per second are made and the JIT cannot optimize due to polymorphic call sites.

If you are designing a high-frequency library and need to minimize dispatch overhead, you might consider using overloaded methods or final methods, which can be statically bound. But in normal business logic, runtime polymorphism is the standard tool for extensibility and should not be avoided for micro-optimization reasons.

Practical Implications for API Design

Choosing between overloading and overriding is not just a syntactic preference. It affects how your API can evolve and how clients use it.

Overloading is useful when you want to provide multiple ways to invoke the same operation with different input types or arities. It is resolved at compile time, so the caller must know the exact types at compile time. Overriding is essential when you want to allow subclasses to extend or replace behavior. It is resolved at runtime, so the caller only needs a reference to the superclass or interface.

A common pattern is to combine both: a base class defines an overridable method, and overloaded convenience methods delegate to it. For example:

public class FileParser { public void parse(String path) { parse(new File(path)); } public void parse(File file) { // default implementation } }

Subclasses can override parse(File) to change behavior, while callers can still use the parse(String) overload. This gives flexibility without breaking the API.

Common Pitfalls and Edge Cases

One subtle issue is that overloading is resolved at compile time, not runtime. If you have a method that accepts a superclass and another that accepts a subclass, the compiler picks the most specific method based on the declared type of the argument, not the actual object type.

void print(Object obj) { ... } void print(String str) { ... } Object o = "hello"; print(o); // calls print(Object), not print(String)

This surprises many developers. To get runtime polymorphism, you must use overriding, not overloading.

Another edge case involves variable hiding. Fields are not polymorphic; they are resolved based on the reference type. If a subclass declares a field with the same name as a superclass field, the reference type determines which field is accessed. This is unrelated to method polymorphism but can cause confusion.

Choosing the Right Polymorphism for Your Code

Use compile-time polymorphism (overloading) when:

  • The set of parameter types is known and fixed at compile time.
  • You want to provide convenience methods without affecting subclass behavior.
  • You need to support different argument combinations that do not require runtime type checks.

Use runtime polymorphism (overriding) when:

  • You need to allow subclasses to provide their own implementation of a common operation.
  • You are designing a framework where the caller only knows the interface or abstract class.
  • You want to apply the Open/Closed Principle: open for extension, closed for modification.

In many real-world classes, both are used together. The key is to understand that overloading is a compile-time mechanism that does not participate in dynamic dispatch, while overriding is the foundation of Java's polymorphic behavior. By choosing the right mechanism for each method, you can build APIs that are both flexible and predictable.

java compile time vs runtime polymorphism: Practical Usage a | RYUSLOG DEV