Dynamic binding in Java can feel like magic—but understanding how it works under the hood is key to mastering object-oriented programming.
Inheritance and Specialization
With regard to inheritance, in a form of is-a-type-of relationship, subclasses are always also of the same type as their superclasses. The visible methods that superclasses have thus also exist in the subclasses. The advantage of specialization is that the superclass specifies a simple implementation, and a subclass can override it. We saw this specialization earlier with toString().
However, not only is specialization interesting from a design perspective but also in terms of inheritance. If a superclass provides a visible method, you always know that all subclasses will have that method, whether they override the method or not. As you’ll see in a moment, this inheritance leads to one of the most important constructs in OOP languages.
What Happens When You Call to toString()?
Since each class inherits members from java.lang.Object, the toString() method can be called on any object. In our examples, the Object, Event, and Workout classes all have their own toString() methods. Event overrides the toString() of Object, while Workout overrides the toString() method of Event.
Now, let’s look at an interesting scenario where the toString() method is called but the reference type and the object type are different.
Workout ww = new Workout();
ww.about = "Running";
ww.duration = 100;
ww.caloriesBurned = 300;
System.out.println( ww.toString() );
Event ew = new Workout();
ew.about = "Running";
ew.duration = 100;
System.out.println( ew.toString() );
Object ow = new Workout();
System.out.println( ow.toString() );
The toString() method is called three times, where the object type always remains the same (Workout), but the reference type is always different (Workout, Event, or Object).
Now is time for the most exciting question in OOP: What happens in the toString() method call?
The answer is the following output:
Workout[about=Running, duration=100][caloriesBurned=300]
Workout[about=Running, duration=100][caloriesBurned=0]
Workout[about=null, duration=0][caloriesBurned=0]
Explaining the Output: Dynamic vs. Static Typing
This output is easy to understand if you consider that two type systems are at work, and the compiler doesn’t have the same knowledge as the runtime environment. The crucial thing is that the runtime environment looks at the object type when calling a method, not at the reference type—the same behavior as instanceof.
What Is Dynamic Binding?
Since the variable type agreed to in the program doesn’t indicate which implementation of the toString() method is called, we call this scenario late dynamic binding (or dynamic binding for short). Only at runtime (which is late in comparison to translation time) does the runtime environment dynamically select the appropriate object method and match the actual type of the calling object. The JVM knows that a Workout object exists behind each of the three variables, so it calls the toString() method from Workout.
Comparing Java and C++ Binding Behavior
Dynamic binding is automatic in Java and can’t be controlled or switched off by a modifier. In C++, functions—the term “method” is not used in C++—are not dynamically bound without explicit indication. If you want to bind functions dynamically in C++, you must explicitly place the keyword virtual in front of the function; the result is a virtual function.
Why Superclasses Matter in Overriding Methords
What’s important is that a method is overridden. Let’s assume that there’s no toString() in Object, but only an implementation in the Nap and Workout subclasses. We wouldn’t benefit from that in any way! We therefore explicitly use the commonality that Event, Workout, and other subclasses inherit toString() from Object.
Without the superclass, no connective link would exist, and consequently, the superclass always provides a method that subclasses can override. If we were to create a new subclass of Object and not override toString(), the runtime environment would find toString() in Object, but the method would exist in any case—either the original method or the overridden variant.
Dynamic Binding vs. Polymorphism: Terminology Tips
Dynamic binding is often also referred to as polymorphism, and a dynamically bound call is then referred to as a polymorphic call. This meaning is fine in the context of Java; however, in the world of programming languages “polymorphism” can designate many different things, such as parametric polymorphism (which in Java is called generics).
How System.out.println(Object) Demonstrates Dynamic Binding
Let’s look at a program that makes dynamic binding even more obvious. The print*(...) methods are overloaded to accept any object and then output the string representation.
public void println( Object x ) {
String s = String.valueOf( x );
// String s = (obj == null) ? "null" : obj.toString();
synchronized ( this ) {
print( s );
newLine();
}
}
The println(Object) method consists of three parts: First, the string representation of an object is requested, which is where the dynamically bound call can be found. Then, this string is passed to print(String), and finally, newLine() produces the line break.
The compiler has no idea at all what x is, and x can be anything because everything is a java.lang.Object. Statically, nothing can be read from the argument x, and so the runtime environment must decide which class the method call will go to—the miracle of dynamic binding.
Final Tip: Explore Type Hierarchies in IntelliJ
IntelliJ displays a type hierarchy when you press (Ctrl)+(H), by default showing superclasses and known subclasses.
Editor’s note: This post has been adapted from a section of the book Java: The Comprehensive Guide by Christian Ullenboom. Christian is an Oracle-certified Java programmer and has been a trainer and consultant for Java technologies and object-oriented analysis and design since 1997.
This post was originally published 5/2024.
Comments