CSC 347 - Concepts of Programming Languages

Inheritance and Dynamic Dispatch

Instructor: Stefan Mitsch

Learning Objectives

How do we support code sharing between classes?

What should happen when methods have the same name?

  • Understand inheritance
  • Understand how methods are chosen at runtime

Scala: Which toString?

  • x has static type Animal, dynamic type Bird/Cat
  • Dynamic dispatch, late binding: use dynamic/actual type (of object)
  1. class Animal { override def toString () = "Animal" }
  2. class Bird extends Animal { override def toString () = "Bird" }
  3. class Cat extends Animal { override def toString () = "Cat" }
  4. val xs = Array[Animal] (new Bird(), new Cat ())
  5. for (x <- xs) println (x.toString())
  • Output at runtime
    1. Bird
    2. Cat

Java: Which toString?

  • x has static type Animal, dynamic type Bird/Cat
  • Dynamic dispatch, late binding: use dynamic/actual type (of object)
  1. class Animal { public String toString () { return "Animal"; } }
  2. class Bird extends Animal { public String toString () { return "Bird"; } }
  3. class Cat extends Animal { public String toString () { return "Cat"; } }
  4. Animal[] xs = { new Bird(), new Cat () };
  5. for (Animal x : xs) System.out.println (x.toString());
  • Output at runtime
    1. $ javac AnimalTest.java && java AnimalTest
    2. Bird
    3. Cat

C++: Which toString?

  • x has static type Animal, dynamic type Bird/Cat
  • Static dispatch, early binding: use static/declared type (of variable)
  1. class Animal { public: string to_string() { return "Animal"; } };
  2. class Bird: public Animal { public: string to_string() { return "Bird"; } };
  3. class Cat: public Animal { public: string to_string() { return "Cat"; } };
  4. Animal *xs[] = { new Bird(), new Cat() };
  5. for (Animal *x : xs) cout << x->to_string() << endl;
  • Output at runtime
    1. $ g++ -std=c++11 -o animal1 animal1.cpp && ./animal1
    2. Animal
    3. Animal

C++: Which toString?

  • x has static type Animal, dynamic type Bird/Cat
  • C++ dynamically dispatches virtual methods only when object accessed with a pointer/reference
  1. class Animal { public: virtual string to_string() { return "Animal";
  2. class Bird: public Animal { public: virtual string to_string() { return "Bird"; }
  3. class Cat: public Animal { public: virtual string to_string() { return "Cat"; }
  4. Animal *xs[] = { new Bird(), new Cat() };
  5. for (Animal *x : xs) cout << x->to_string() << endl;
  • Output at runtime
    1. $ g++ -std=c++11 -o animal2 animal2.cpp && ./animal2
    2. Bird
    3. Cat

C++: Which toString?

  • x has static type Animal, dynamic type Animal
  • Truncated object, stored in place: C++ truncates the object, coercing to parent class
  1. class Animal { public: virtual string to_string() { return "Animal";
  2. class Bird: public Animal { public: virtual string to_string() { return "Bird"; }
  3. class Cat: public Animal { public: virtual string to_string() { return "Cat"; }
  4. Animal xs[] = { Bird(), Cat() };
  5. for (Animal x : xs) cout << x.to_string() << endl;
  • Output at runtime
    1. $ g++ -std=c++11 -o animal3 animal3.cpp && ./animal3
    2. Animal
    3. Animal

Dynamic Dispatch (Java)

  1. interface Fn { int apply (int x); }
  2. class F implements Fn { public int apply (int x) { return x + 1; } }
  3. class G implements Fn { public int apply (int x) { return x + 2; } }
  4. class H implements Fn { public int apply (int x) { return x + 3; } }
  1. Fn[] fs = { new F (), new G (), new H () };
  2. for (int i = 0; i < fs.length; i++) {
  3. System.out.format ("fs[%d].apply(20)=%d\n", i, fs[i].apply(20));
  4. }
  • Output at runtime
    1. fs[0].apply(20)=21
    2. fs[1].apply(20)=22
    3. fs[2].apply(20)=23

With Anonymous Classes

  1. interface Fn { int apply (int x); }
  1. Fn[] fs = new Fn[3];
  2. for (int i = 0; i < fs.length; i++) {
  3. int j = i + 1; // effectively final
  4. fs[i] = new Fn() { int apply (int x) { return j + x; } }
  5. }
  6. for (int i = 0; i < fs.length; i++) {
  7. System.out.format ("fs[%d].apply(20)=%d\n", i, fs[i].apply(20));
  8. }
  • Output at runtime
    1. fs[0].apply(20)=21
    2. fs[1].apply(20)=22
    3. fs[2].apply(20)=23

With Lambda Notation

  1. interface Fn { int apply (int x); }
  1. Fn[] fs = new Fn[3];
  2. for (int i = 0; i < fs.length; i++) {
  3. int j = i + 1; // effectively final
  4. fs[i] = x -> x + j;
  5. }
  6. for (int i = 0; i < fs.length; i++) {
  7. System.out.format ("fs[%d].apply(20)=%d\n", i, fs[i].apply(20));
  8. }
  • Output at runtime
    1. fs[0].apply(20)=21
    2. fs[1].apply(20)=22
    3. fs[2].apply(20)=23

Which Object?

  1. class A {
  2. int f (int x) {
  3. System.out.format ("A.f (%d)%n", x);
  4. return (x == 0) ? g () : f (x - 1);
  5. }
  6. int g () { System.out.println ("A.g ()"); return 0; }
  7. }
  • Instantiate object, call method f
    1. A x = new A ();
    2. x.f (2);
  • Output at runtime
    1. A.f (2)
    2. A.f (1)
    3. A.f (0)
    4. A.g ()
    5. $1 ==> 0
  • Which object are we accessing when invoking g () and f (x-1)?

Which Object: this

  • What are the static and dynamic types of this?
  1. class A {
  2. int f (int x) {
  3. System.out.format ("A.f (%d)%n", x);
  4. return (x == 0) ? this.g () : this.f (x - 1);
  5. }
  6. int g () { System.out.println ("A.g ()"); return 0; }
  7. }
  • Instantiate object, call method f
    1. A x = new A ();
    2. x.f (2);
  • Output at runtime
    1. A.f (2)
    2. A.f (1)
    3. A.f (0)
    4. A.g ()
    5. $1 ==> 0
  • Reference this has static type of class (here: A)
    and dynamic type of instantiated object (here: A)

Inheritance

  • B Inherits f, overrides g
  • What are the static and dynamic types of x and this?
  1. class A {
  2. int f (int x) {
  3. System.out.format ("A.f (%d)%n", x);
  4. return (x == 0) ? this.g () : this.f (x - 1);
  5. }
  6. int g () { System.out.println ("A.g ()"); return 0; }
  7. }
  8. class B extends A {
  9. int g () { System.out.println ("B.g ()"); return 0; }
  10. }
  • Instantiate object
    1. A x = new B ();
    2. x.f (2);
  • x has static type A, dynamic type B
  • Output at runtime
    1. A.f (2)
    2. A.f (1)
    3. A.f (0)
    4. B.g ()
    5. $1 ==> 0
  • Here, reference this has static type A and dynamic type B

Other Features

Let programmers specify how classes can be instantiated?
Let programmers decide which methods can/must/cannot be overridden?

Abstract Classes

  • Abstract classes cannot be instantiated
  1. abstract class A {
  2. int f (int x) {
  3. System.out.format ("A.f (%d)%n", x);
  4. return (x == 0) ? this.g () : this.f (x - 1);
  5. }
  6. int g () { System.out.println ("A.g ()"); return 0; }
  7. }
  8. class B extends A {
  9. int g () { System.out.println ("B.g ()"); return 0; }
  10. }
  • Instantiate object
    1. A x = new A ();
  • Compile error
    1. | Error:
    2. | A is abstract; cannot be instantiated
    3. | A x = new A ();
    4. | ^------^

Abstract Methods

  • Abstract methods must be overridden in concrete subclass
  1. abstract class A {
  2. int f (int x) {
  3. // ...
  4. }
  5. abstract int g ();
  6. }
  7. class B extends A { }
  • Compile error
    1. | Error:
    2. | B is not abstract and does not override abstract method g() in A
    3. | class B extends A {
    4. | ^------------------...

Final Methods

  • Final methods cannot be overridden
  1. abstract class A {
  2. final int f (int x) {
  3. // ...
  4. }
  5. abstract int g ();
  6. }
  7. class B extends A {
  8. int f (int x) { System.out.format ("B.f (%d)%n", x); return 0; }
  9. int g () { System.out.println ("B.g ()"); return 0; }
  10. }
  • Compile error

    1. | Error:
    2. | f(int) in B cannot override f(int) in A
    3. | overridden method is final
    4. | int f (int x) { System.out.format ("B.f (%d)%n", x); return 0; }
    5. | ^--------------------------------------------------------------^

Hook Methods

  • Nonfinal and abstract methods must be overridden
  1. abstract class A {
  2. int f (int x) {
  3. System.out.format ("A.f (%d)%n", x);
  4. return (x == 0) ? this.g () : this.f (x - 1);
  5. }
  6. abstract int g ();
  7. }
  8. class B extends A {
  9. int f (int x) { System.out.format ("B.f (%d)%n", x); return 0; }
  10. int g () { System.out.println ("B.g ()"); return 0; }
  11. }
  • Instantiate object
    1. A x = new B ();
    2. x.f (2);
  • Output at runtime
    1. B.f (2)
    2. $1 ==> 0

Hook Methods

  • Nonfinal and nonabstract methods (aka hooks) can provide default implementation
  1. class A {
  2. int f (int x) {
  3. System.out.format ("A.f (%d)%n", x);
  4. return (x == 0) ? this.g () : this.f (x - 1);
  5. }
  6. int g () { System.out.println ("A.g ()"); return 0; }
  7. }
  8. class B extends A {
  9. int f (int x) { System.out.format ("B.f (%d)%n", x); return 0; }
  10. int g () { System.out.println ("B.g ()"); return 0; }
  11. }
  • Instantiate object
    1. A x = new B ();
    2. x.f (2);
  • Output at runtime
    1. B.f (2)
    2. $1 ==> 0

Overriding f

  • Access A.f with this
  1. class A {
  2. int f (int x) {
  3. System.out.format ("A.f (%d)%n", x);
  4. return (x == 0) ? this.g () : this.f (x - 1);
  5. }
  6. int g () { System.out.println ("A.g ()"); return 0; }
  7. }
  8. class B extends A {
  9. int f (int x) { System.out.format ("B.f (%d)%n", x); return this.f(x); }
  10. int g () { System.out.println ("B.g ()"); return 0; }
  11. }
  • Instantiate object
    1. A x = new B ();
    2. x.f (2);
  • Output at runtime
    1. B.f (2)
    2. B.f (2)
    3. B.f (2)
    4. B.f (2)
    5. ... // infinite loop

How to Access Method in Super-Class?

  • Access A.f with super
  1. class A {
  2. int f (int x) {
  3. System.out.format ("A.f (%d)%n", x);
  4. return (x == 0) ? this.g () : this.f (x - 1);
  5. }
  6. int g () { System.out.println ("A.g ()"); return 0; }
  7. }
  8. class B extends A {
  9. int f (int x) { System.out.format ("B.f (%d)%n", x); return super.f(x); }
  10. int g () { System.out.println ("B.g ()"); return 0; }
  11. }
  • Instantiate object
    1. A x = new B ();
    2. x.f (2);
  • Output at runtime
    1. B.f (2) A.f (0)
    2. A.f (2) B.g ()
    3. B.f (1) $1 ==> 0
    4. A.f (1)
    5. B.f (0)

Delegation-based Languages

  • Create objects rather than instantiate classes
  • Use delegation instead of inheritance
  • Example: Javascript
  1. var A = {
  2. f (x) {
  3. console.log ("A.f (" + x + ")")
  4. return (x == 0) ? this.g () : this.f (x - 1);
  5. },
  6. g () { console.log ("A.g ()"); return 0; }
  7. }
  8. var B = {
  9. f (x) { console.log ("B.f (" + x + ")"); return super.f (x); },
  10. g () { console.log ("B.g ()"); return 0; }
  11. }
  • Link objects
    1. Object.setPrototypeOf(B, A);
    2. B.f (2);
  • Output at runtime
    1. B.f (2) A.f (0)
    2. A.f (2) B.g ()
    3. B.f (1) $1 ==> 0
    4. A.f (1)
    5. B.f (0)

Summary

  • Static vs. dynamic dispatch
    • Static dispatch: select method based on type of reference
    • Dynamic dispatch: select method based on type of object
  • Inheritance vs. delegation
    • Inheritance: links classes statically at compile time
    • Delegation: links objects dynamically at runtime

Examples: C++

  1. class Animal { public: virtual string to_string() { return "Animal"; } };
  2. class Bird: public Animal { public: virtual string to_string() { return "Bird"; } };
  3. class Cat: public Animal { public: virtual string to_string() { return "Cat"; } };
  • Pointers + virtual:
    1. Animal* a = new Bird ();
    2. cout << a->to_string() << endl;
    3. a = new Cat ();
    4. cout << a->to_string() << endl;
  • Reference + virtual:
    1. void print(Animal& a) {
    2. cout << a.to_string() << endl;
    3. }
    4. Bird b = Bird ();
    5. Cat c = Cat ();
    6. print(b);
    7. print(c);
  • Static types:
    1. Bird* b = new Bird ();
    2. cout << b->to_string() << endl;
    3. b = new Cat ();
    4. cout << b->to_string() << endl;

Examples: Scala

  1. class A:
  2. def f () = { println("A.f"); this.g () }
  3. def g () = { println("A.g"); this.h () }
  4. def h () = println("A.h")
  5. class B extends A:
  6. override def f () = { println("B.f"); super.f () }
  7. override def h () = println("B.h")
  8. def i () = println("B.i")
  • Static A, dynamic A
    1. val a:A = new A()
    2. a.f()
    3. a.g()
    4. a.h()
    5. a.i()
  • Static A, dynamic B
    1. val a:A = new B()
    2. a.f()
    3. a.g()
    4. a.h()
    5. a.i()
  • Static B, dynamic B
    1. val b:B = new B()
    2. b.f()
    3. b.g()
    4. b.h()
    5. b.i()