Object-oriented programming: inheritance, dynamic binding and vtables
The three features of OOP, inheritance and subtypes, finding which method runs with dynamic binding, the difference between override/overload/hiding, multiple inheritance and the implementation of dynamic binding with a vtable.
Object-oriented programming (OOP) is a combination of three ideas: the abstract data type (last week), inheritance and dynamic binding. The exam counterpart of this page is two kinds of questions: “Which method runs, what is printed?” (override, overload, hiding, constructor order) and “How is dynamic binding implemented?” (vtable). For the first kind, knowing the rule set is enough.
Golden rules:
- An overridden instance method is chosen according to the object’s actual class.
- Overload selection is done at compile time, according to the declared type of the argument.
- Fields and static methods are not dynamically bound; the type of the variable is used.
- The superclass’s constructor runs first.
- vtable: copy the superclass’s, change the overridden one, add the new one at the end.
Why inheritance?
ADTs can be reused but almost always a small change is needed; copying and modifying the code is a maintenance nightmare. Inheritance derives a new class from an existing class (the superclass, parent): the subclass (derived) takes the fields and methods of the superclass, adds new ones, and overrides some.
Terms:
- Class: an ADT. Object: an instance of the class.
- Method: a subprogram defined in a class. Message: sending a method call to an object.
- Instance variable: each object’s own copy. Class variable (Java
static): one per class. - If an object of the subclass can be used wherever the superclass is expected, the subclass is a subtype. In Java
publicinheritance provides this.
The disadvantage of inheritance: it creates dependency between classes. A change in the superclass can affect all subclasses.
Dynamic binding
A reference of the superclass type can point to an object of one of the subclasses. If the called method is overridden, which version runs? The one of the object’s actual class. This decision is made at run time: dynamic binding (polymorphism).
Animal a = new Dog();
System.out.println(a.speak() + " " + a.name + " " + a.kind() + " " + a.intro());
Woof animal Animal I say Woof
| Expression | Selection rule | Result |
|---|---|---|
a.speak() | Override, dynamic: the object is Dog | Woof |
a.name | Field, declared type Animal | animal |
a.kind() | Static method, declared type | Animal |
a.intro() | Written in Animal, speak() inside is dynamic | I say Woof |
The benefit: the code for (Animal an : zoo) an.speak(); works unchanged even with a Cat class added in the future.
When override and overload mix
class P { void m(Object o) {...P.Object} void m(String s) {...P.String} }
class Q extends P { @Override void m(Object o) {...Q.Object} }
P p = new Q(); Object o = "text";
p.m(o); p.m("text");
Q.Object P.String
Recipe: (1) compile time: the type of p is P, and one of the signatures in P is chosen according to the declared type of the argument. o is of type Object, so m(Object) is chosen even if there is a String inside. (2) run time: the version of the chosen signature in the object’s class (Q) runs.
Constructor order
class A { A() { System.out.print("A "); show(); } void show() { ... "A.show" } }
class B extends A { int v = 7; B() { super(); System.out.print("B "); }
@Override void show() { ... "B.show v=" + v } }
new B();
A B.show v=0 B
The superclass constructor runs first, the subclass’s field initializers after it. The show() in A’s constructor is dynamically bound and sees the not-yet-initialized v (0).
super and private
X z = new Z(); // Z.hi → "Z+" + super.hi() → Y.hi → "Y+" + super.hi() → X.hi
System.out.println(z.hi());
System.out.println(new Derived().call()); // private who() is not overridden
Z+Y+X
Base
super.m() always goes to the version of the direct superclass. private methods are not inherited; a method with the same name in the subclass is a separate method.
Language design questions
| Question | Java | C++ | Smalltalk | C# |
|---|---|---|---|---|
| Is everything an object? | Except primitives | No (hybrid) | Yes | Except value types (boxing exists) |
| Default binding | Dynamic (except private/static/final) | Static; dynamic with virtual | Always dynamic | Static; virtual + override |
| Multiple inheritance | Not for classes, via interface | Yes | No | Not for classes, via interface |
| Where is the object? | Heap | Stack or heap | Heap | Heap (class), stack (struct) |
| Memory reclamation | Garbage collection | Programmer (delete, destructor) | GC | GC |
In C++, if an object on the stack is assigned by value to a variable of the superclass type, the subclass’s fields are cut off (object slicing). In Java, since all objects are on the heap and accessed by reference, this problem doesn’t exist.
Abstract classes and multiple inheritance
abstract class Shape { abstract double area(); }
Shape s = new Shape(); // error: Shape is abstract; cannot be instantiated
In multiple inheritance, if two superclasses bring two methods with the same signature (the diamond problem), which is chosen? C++ requires the programmer to qualify with Walker::move. Java does not allow multiple inheritance for classes; if default methods in interfaces clash, the compiler requires an override:
interface Walker { default String move() { return "walk"; } }
interface Swimmer { default String move() { return "swim"; } }
class Duck implements Walker, Swimmer {
public String move() { return Walker.super.move() + "&" + Swimmer.super.move(); }
}
walk&swim
Implementation: CIR and vtable
The class instance record (CIR) is the structure of an object in memory; it is determined at compile time like a record. A subclass’s CIR copies the superclass’s CIR and adds its own fields at the end. This way the superclass’s fields are at the same offset in every subclass object.
For dynamically bound methods, every class has a virtual method table (vtable): the address of the code for each dynamically bound method in the class. Each CIR carries a pointer to its class’s vtable.
class Shape { area(); draw(); name(); }
class Circle extends Shape { override area(); new radius(); }
class Ring extends Circle { override draw(); override area(); }
slot Shape vtable Circle vtable Ring vtable
0 Shape.area Circle.area Ring.area
1 Shape.draw Shape.draw Ring.draw
2 Shape.name Shape.name Shape.name
3 - Circle.radius Circle.radius
for (Shape s : new Shape[]{ new Shape(), new Circle(), new Ring() })
System.out.println(s.area() + " " + s.draw() + " " + s.name());
Shape.area Shape.draw Shape.name
Circle.area Shape.draw Shape.name
Ring.area Ring.draw Shape.name
The compiler translates the call s.area() into code that says “go to the address in slot 0 of the object’s vtable”. Whatever the class, slot 0 is always area; at run time only which table is read changes. The cost: one indirection on every call. Statically bound methods (C++ non-virtual, Java private/final) don’t pay this cost and are not in the vtable.
In multiple inheritance things get complicated: the object has a separate vtable pointer and part for each superclass.
Common mistakes
| Wrong | Correct |
|---|---|
a.name according to the object’s class | Fields are by the declared type (hiding) |
| A static method is overridden | It is hidden (hiding), not dynamic |
| Overload is chosen at run time | At compile time, by the declared type |
| Subclass fields are ready in the constructor | Still at default values (0, null) |
super.hi() is dynamic | Static, directly to the superclass |
| A private method can be overridden | It is not inherited; a separate method |
| In C++ all methods are dynamic | Only the virtual ones |
| A non-overridden method is not in the vtable | The superclass’s version stays in the same slot |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
What is printed in Java?
class Animal {
String name = "animal";
String speak() { return "..."; }
static String kind() { return "Animal"; }
String intro() { return "I say " + speak(); }
}
class Dog extends Animal {
String name = "dog";
@Override String speak() { return "Woof"; }
static String kind() { return "Dog"; }
}
// in main:
Animal a = new Dog();
System.out.println(a.speak() + " " + a.name + " " + a.kind() + " " + a.intro());Show answer
An instance method is chosen with dynamic binding according to the object's actual class: Dog.speak → Woof. Fields and static methods are not dynamically bound, they are chosen according to the declared type of the variable (Animal): a.name = "animal", a.kind() = "Animal" (hiding). intro is written in Animal but the speak() call inside it is also dynamic: "I say Woof".
What is printed in Java?
class A { A() { System.out.print("A "); show(); }
void show() { System.out.print("A.show "); } }
class B extends A { int v = 7;
B() { super(); System.out.print("B "); }
@Override void show() { System.out.print("B.show v=" + v + " "); } }
// in main:
new B();Show answer
First the superclass constructor runs: "A ". A's constructor calls show(); since the object is actually a B, B.show runs. But B's field initializers (v = 7) have not run yet, v is the default 0. Then B's constructor: "B ". Calling an overridable method from a constructor is dangerous for this reason.
What is printed in Java?
class P { void m(Object o) { System.out.print("P.Object "); }
void m(String s) { System.out.print("P.String "); } }
class Q extends P { @Override void m(Object o) { System.out.print("Q.Object "); } }
// in main:
P p = new Q();
Object o = "text";
p.m(o);
p.m("text");Show answer
Two stages: (1) Overload selection is at compile time, according to the declared type of the argument: o is Object → the m(Object) signature; "text" is String → m(String). (2) Override selection is at run time, according to the object's actual class: m(Object) is overridden in Q → Q.Object; m(String) is not in Q → P.String.
Fill in the vtable of class Ring: which implementation does each slot point to?
class Shape { area(); draw(); name(); }
class Circle extends Shape { override area(); new radius(); }
class Ring extends Circle { override draw(); override area(); }Show answer
The subclass's vtable starts as a copy of the superclass's, the slot order is preserved: [area, draw, name] + the [radius] added by Circle. Overridden slots are replaced with the new version. Ring overrides area and draw; name is inherited from Shape, radius from Circle. The call s.area() is translated at compile time as "slot 0", and read from the object's vtable at run time.
What is printed in Java?
class Base { private String who() { return "Base"; }
String call() { return who(); } }
class Derived extends Base { private String who() { return "Derived"; } }
// in main:
System.out.println(new Derived().call());Show answer
private methods are not inherited and cannot be overridden; who in Derived is a completely separate method. The who() inside Base.call is statically bound to Base.who at compile time. In Java, private, static and final methods are statically bound; they don't need to enter the vtable.
What is printed in Java?
class X { String hi() { return "X"; } }
class Y extends X { @Override String hi() { return "Y+" + super.hi(); } }
class Z extends Y { @Override String hi() { return "Z+" + super.hi(); } }
// in main:
X z = new Z();
System.out.println(z.hi());Show answer
Dynamic binding picks Z.hi. super.hi() is not dynamic: it always calls the version in the class's direct superclass. The chain Z → Y.hi → X.hi: "Z+" + "Y+" + "X".
Does this Java code compile? Why?
abstract class Shape { abstract double area(); }
// in main:
Shape s = new Shape();Show answer
An abstract method is a protocol without a body; a class with at least one abstract method is abstract and no object of it can be created. Its purpose is to give subclasses a common interface. It is used with a subclass object, like Shape s = new Circle();. The C++ counterpart is the pure virtual function: virtual double area() = 0;.
Two Java interfaces Walker and Swimmer both declare default String move(). A class Duck implements Walker, Swimmer does not override move. What happens, and how is it fixed?
Show answer
The classic problem of multiple inheritance (the diamond problem): two implementations with the same signature come from two supertypes, which one? The Java compiler does not choose, it requires the class to override explicitly. Walker.super.move() + "&" + Swimmer.super.move() → "walk&swim". C++ allows multiple class inheritance and has you resolve the ambiguity by qualification like Walker::move.
Name the three characteristic features of an object-oriented language.
Show answer
An ADT hides the representation of data. Inheritance allows reusing and modifying an existing class; it solves the reuse problem of ADTs. Dynamic binding (polymorphism) allows calling the subclasses' own methods through a superclass reference; adding a new subclass does not change existing code.
In C++, method draw is declared in class Shape without the virtual keyword and redefined in Circle. Which version runs for Shape* p = new Circle(); p->draw();?
Show answer
In C++ the default is static binding; only methods declared virtual are dynamically bound and enter the vtable. Without virtual, the call is resolved at compile time according to the type of the pointer (Shape*). In Java it is the opposite: every method is dynamically bound unless it is private/static/final. C++ makes this choice for efficiency.
What is printed in Java?
Animal[] zoo = { new Animal(), new Dog(), new Dog() };
int woofs = 0;
for (Animal an : zoo) if (an.speak().equals("Woof")) woofs++;
System.out.println(woofs + " " + (zoo[1] instanceof Animal) + " " + (zoo[0] instanceof Dog));Show answer
The same an.speak() call runs a different method for each element according to the object's class: polymorphism. Two Dogs → 2. A Dog is an Animal (subtype): true. An Animal object is not a Dog: false. The subtype relationship is one-directional.
What does a class instance record (CIR) contain in a typical implementation, and where are the methods themselves stored?
Show answer
A CIR is the structure of an object, determined at compile time like a record. A subclass's CIR copies the superclass's CIR and adds its own fields at the end; that is why a subclass object can be used safely through a superclass reference. For dynamically bound methods, the CIR holds a pointer to the vtable. Statically bound ones have no place in the CIR.
Verification: All Java outputs were run with the JDK (java verify/SEN2001/W11.java); the abstract class instantiation and conflicting default method examples were attempted to compile with javac and gave the expected compile errors (verify/SEN2001/w11b/). The vtable was filled by actually calling each method in each class in Java to see which version runs. The C++ virtual/non-virtual and Smalltalk information is conceptual; their compilers are not installed.