Dynamic Binding: Class-Appropriate behavior
Steven Zeil
To be considered “object-oriented”, a language must support inheritance, subtyping, and dynamic binding. We have seen the first two. Now it’s time to look at the third.
If you want to claim that a program is written in an object-oriented style, the code must be designed to take advantage of these three features. Otherwise, it’s just a traditionally styled program written in an object-oriented programming language (OOPL).
1 Dynamic Binding
Dynamic binding is a key factor in allowing different classes to respond to the same message with different methods.
- Arguably, no language without dynamic binding is an OOPL
1.1 What is dynamic binding?
Binding is a term used in programming languages to denote the association of information with a symbol. It’s a very general term with many applications.
-
a = 2;
is a binding of a value to a variable. -
String s;
is a binding of a type (String) to the variable name (s).
Binding Function Calls to Bodies
In OOP, we are particularly interested in the binding of a function body (method) to a function call.
Given the following:
a = foo(b);
When is the decision made as to what code will be executed for this call to foo?
1.1.1 Compile-Time Binding
a = foo(b);
In traditionally compiled languages (FORTRAN, PASCAL, C, etc), the decision is made at compile-time.
-
Decision is immutable
-
If this statement is inside a loop, the same code will be invoked for foo each time.
-
-
Compile-time binding is cheap – there’s very little execution-time overhead.
-
That’s because a simple, constant address is associated with each function and the compiler can generate code to jump to that address.
-
1.1.2 Run-Time Binding
a = foo(b);
In traditionally interpreted languages (LISP, BASIC, etc), the decision is made at run-time.
-
Decision is often mutable
-
If this statement is inside a loop, different code may be invoked for foo each time.
-
-
Run-time binding can be expensive (high execution-time overhead) because it suggests that some sort of decision or lookup is done at each call.
1.1.3 Dynamic Binding = the Happy Medium?
OOPLs typically feature dynamic binding, an “intermediate” choice in which
-
the choice of method is made from a relatively small list of options.
- That list is determined at compile time.
- The final choice made at run-time.
-
The options that make up that list are organized according to the inheritance hierarchy.
Dynamic Binding and Programming Languages
-
In Java, all function calls are resolved by dynamic binding.
-
In C++, we can choose between compile-time and dynamic binding.
-
This is one of the reasons that C++ is often called a “hybrid” OOPL, as opposed to “pure” OOPLs like SmallTalk.
-
Actually, both the class designer and the application programmer have a say in this. Dynamic binding happens only if the class designer says it’s OK for a given function, and then only if the application programmer makes calls to that function in a way that permits dynamic binding.
-
1.2 Dynamic Binding in C++
1.2.1 Virtual functions
-
A non-inherited function member is subject to dynamic binding if its declaration is preceded by the word
virtual
. -
An inherited function member is subject to dynamic binding if that member in the base class is subject to dynamic binding.
-
Using the word
virtual
on inherited members in subclasses is optional (but recommended).
-
1.2.2 Calling via Pointers or References
-
Declaring a function as virtual gives programmers permission to call it via dynamic binding.
-
But not all calls will be resolved that way.
-
-
Let foo be a virtual function member.
-
x.foo()
, where x is an object, is bound at compile time -
x.foo()
, where x is a reference, is bound at run-time (dynamic). -
x->foo()
, where x is a pointer, is bound at run-time (dynamic).
-
2 Dynamic Binding in Java
Dynamic binding is more pervasive in Java, because
-
Almost all functions in Java are, implicitly, virtual.
-
The only exceptions are constructors and functions that are explicitly marked as
static
.(In C++, it would not be legal to mark constructors or static functions as
virtual
.)
-
-
All non-primitive variables in Java are references to objects on the heap.
We don’t have the distinction made in C++ between functions invoked directly on an object as opposed to functions invoked via a pointer/reference.
3 An Example of Dynamic Binding
3.1 C++: An Animal Inheritance Hierarchy
For this example, we will introduce a simple hierarchy.
3.1.1 The Base Class
class Animal {
public:
virtual String eats() {return "???";}
String name() {return "Animal";}
};
We begin with the base class, Animal, which has two functions.
- One of those functions has been marked
virtual
, and so is eligible for dynamic binding.
3.1.2 Plant Eaters
Now we introduce a subclass of Animal that overrides both those functions.
class Herbivore: public Animal {
public:
virtual String eats() {return "plants";}
String name() {return "Herbivore";}
};
- The “
virtual
” on function eats is optional.- That function is already virtual because it was declared that way in the base class.
But listing the
virtual
in the subclass is a nice reminder to the reader.
- That function is already virtual because it was declared that way in the base class.
3.1.3 Cud-Chewers
Now we introduce a subclass of that class.
class Ruminants: public Herbivore {
public:
virtual String eats() {return "grass";}
String name() {return "Ruminant";}
};
3.1.4 Meat Eaters
And another subclass of the original base class.
class Carnivore: public Animal {
public:
virtual String eats() {return "meat";}
String name() {return "Carnivore";}
};
3.1.5 The Application Code
Output Function
It will also be useful in this example to have a simple utility function to print a pair of strings.
void show (String s1, String s2) {
cout << s1 << " " << s2 << endl;
}
Finally, we introduce some application code that makes calls on the member functions of our inheritance hierarchy.
Let’s Make Some Calls
Animal a, *paa, *pah, *par;
Herbivore h, *phh;
Ruminant r;
paa = &a; phh = &h; pah = &h; par = &r;
show(a.name(), a.eats()); // AHRC ?pgm
show(paa->name(), paa->eats()); // AHRC ?pgm
show(h.name(), h.eats); // AHRC ?pgm
show(phh->name(), phh->eats()); // AHRC ?pgm
show(pah->name(), pah->eats()); // AHRC ?pgm
show(par->name(), par->eats()); //AHRC ?pgm
- Note that some pointers are initialized to point to objects of a subtype of the pointer’s own declared type.
Note the variety of variables we are using.
-
We have three actual objects, a, h, and r, which are of type Animal, Herbivore, and Ruminant, respectively.
-
We also have a number of pointers, all of which have names beginning with “p”.
-
The second letter of each pointer variable name indicates its data type.
Thus, pa, pah, and par are all of type Animal
*
. ph has type Herbivore*
. -
These pointer variables are all assigned the address of one of our three actual objects. (The unary prefix operator
&
in C++ is the “address-of” operator.) -
The third letter in each pointer variable’s name indicates what type of object it actually points to. Thus paa is of type Animal
*
and actually points to an Animal object, a.pah, on the other hand, is of type Animal
*
but actually points to an Herbivore object, h. (This is possible because of subtyping - we can substitute a subtype object into a context where the supertype object is expected.)
-
3.1.6 What’s the output?
3.2 The Animal Example in Java
Same example, this time in Java. Start with the inheritance hierarchy.
3.2.1 The Classes
public class Animal {
public String eats() {return "???";}
public name() {return "Animal";}
}
public class Herbivore extends Animal {
public String eats() {return "plants";}
public String name() {return "Herbivore";}
}
public class Ruminants extends Herbivore {
public String eats() {return "grass";}
public String name() {return "Ruminant";}
}
public class Carnivore extends Animal {
public String eats() {return "meat";}
public String name() {return "Carnivore";}
}
3.2.2 The Application
public class AnimalTest {
private static void show (String s1, String s2) {
System.out.println (s1 + " " + s2);
}
public static void main (String[]) {
Animal a = new Animal();
Herbivore h = new Herbivore();
Ruminant r = new Ruminant();
Animal paa = a;
Animal pah = h;
Animal par = r;
show(a.name(), a.eats());
show(paa.name(), paa.eats());
show(h.name(), h.eats);
show(pah.name(), pah.eats());
show(par.name(), par.eats());
}
}
The application is structured a bit differently. Java does not allow standalone functions, so the show
function needs to be inside a class. We assume the rest of the application code is in that same class.
Note the absence of the *
and &
operators.
- Because almost everything is a pointer, special operators for dereferencing pointers and fetching addresses are not needed.
3.2.3 The Output
The overall output here is slightly different from that in the earlier C++ example because, in Java, the name()
function is “virtual” and so is handled by dynamic binding.
4 Why is Dynamic Binding Important?
Dynamic binding lets us write application code for the superclass that can be applied to the subclasses, taking advantage of the subclasses’ different methods.
We will explore this idea in the next lesson.