CSC447

Concepts of Programming Languages

Dynamic Dispatch and Inheritance

Instructor: James Riely

Which toString?

  • x has static type Animal, dynamic type Bird/Cat

class Animal              { public  String toString () { return "Animal"; } }
class Bird extends Animal { public  String toString () { return "Bird"; } }
class Cat  extends Animal { public  String toString () { return "Cat"; } }

Animal[]  xs = { new Bird(), new Cat () };
for (Animal  x : xs) System.out.println (x.toString());
          

$ javac AnimalTest.java && java AnimalTest
Bird
Cat
          
  • Dynamic Dispatch == Late Binding
    • use dynamic/actual type (of object)

Which toString?

  • x has static type Animal, dynamic type Bird/Cat

class Animal              { public: string to_string() { return "Animal"; } };
class Bird: public Animal { public: string to_string() { return "Bird"; } };
class Cat:  public Animal { public: string to_string() { return "Cat"; } };

Animal *xs[] = { new Bird(), new Cat() };
for (Animal *x : xs) cout << x->to_string() << endl; 
          

$ g++ -std=c++11 -o animal1 animal1.cpp && ./animal1
Animal
Animal
          
  • Static Dispatch == Early Binding
    • use static/declared type (of variable)

Which toString?

  • x has static type Animal, dynamic type Bird/Cat

class Animal              { public: virtual string to_string() { return "Animal";
class Bird: public Animal { public: virtual string to_string() { return "Bird"; }
class Cat:  public Animal { public: virtual string to_string() { return "Cat"; } 

Animal *xs[] = { new Bird(), new Cat() };
for (Animal *x : xs) cout << x->to_string() << endl; 
          

$ g++ -std=c++11 -o animal2 animal2.cpp && ./animal2
Bird
Cat
          
  • C++ has dynamically dispatches virtual methods
    • when object accessed with a pointer/reference

Which toString?

  • x has static type Animal, dynamic type Animal

class Animal              { public: virtual string to_string() { return "Animal";
class Bird: public Animal { public: virtual string to_string() { return "Bird"; }
class Cat:  public Animal { public: virtual string to_string() { return "Cat"; } 

Animal  xs[] = {     Bird(),     Cat() };
for (Animal  x : xs) cout <<  x.to_string() << endl; 
          

$ g++ -std=c++11 -o animal3 animal3.cpp && ./animal3
Animal
Animal
          
  • Unboxed object, stored in place
    • C++ truncates the object, coercing to parent class

Dynamic Dispatch (Java)


interface Fn {                 int apply (int x);                  }
class F implements Fn { public int apply (int x) { return x + 1; } }
class G implements Fn { public int apply (int x) { return x + 2; } }
class H implements Fn { public int apply (int x) { return x + 3; } }
          

Fn[] fs = { new F (), new G (), new H () };
for (int i = 0; i < fs.length; i++) {
  System.out.format ("fs[%d].apply(20)=%d\n", i, fs[i].apply(20));
}
          

fs[0].apply(20)=21
fs[1].apply(20)=22
fs[2].apply(20)=23
          

With anonymous classes


interface Fn { int apply (int x); }
          

Fn[] fs = new Fn[3];
for (int i = 0; i < fs.length; i++) {
  int j = i + 1;      // effectively final
  fs[i] = new Fn() { int apply (int x) { return j + x; } }
}
for (int i = 0; i < fs.length; i++) {
  System.out.format ("fs[%d].apply(20)=%d\n", i, fs[i].apply(20));
}
          

fs[0].apply(20)=21
fs[1].apply(20)=22
fs[2].apply(20)=23
          

With lambda notation


interface Fn { int apply (int x); }
          

Fn[] fs = new Fn[3];
for (int i = 0; i < fs.length; i++) {
  int j = i + 1;      // effectively final
  fs[i] = x -> x + j; 
}
for (int i = 0; i < fs.length; i++) {
  System.out.format ("fs[%d].apply(20)=%d\n", i, fs[i].apply(20));
}
          

fs[0].apply(20)=21
fs[1].apply(20)=22
fs[2].apply(20)=23
          

Delegation and Inheritance

  • Delegation (aka Composition)
    • Object references another object
    • Dynamic relationship between objects
  • Inheritance
    • Define class in layers
    • Static relationship between classes

Example

  • this has static type A, dynamic type A

class A {
  int f (int x) {
    System.out.format ("A.f (%d)%n", x);
    return (x == 0) ? this.g () : this.f (x - 1); 
  }
  int g () { System.out.println ("A.g ()"); return 0; }
}



A x = new A ();
x.f (2);
          

A.f (2)
A.f (1)
A.f (0)
A.g ()
$1 ==> 0
          

B Inherits f, Overrides g

  • this has static type A, dynamic type B

class A {
  int f (int x) {
    System.out.format ("A.f (%d)%n", x);
    return (x == 0) ? this.g () : this.f (x - 1); 
  }
  int g () { System.out.println ("A.g ()"); return 0; }
}
class B extends A {

  int g () { System.out.println ("B.g ()"); return 0; }
}
A x = new B ();
x.f (2);
          

A.f (2)
A.f (1)
A.f (0)
B.g ()
$1 ==> 0
          

Abstract classes

  • Abstract classes cannot be instantiated

abstract class A {
  int f (int x) {
    System.out.format ("A.f (%d)%n", x);
    return (x == 0) ? this.g () : this.f (x - 1); 
  }
  int g () { System.out.println ("A.g ()"); return 0; }
}
class B extends A {

  int g () { System.out.println ("B.g ()"); return 0; }
}
A x = new A (); 
//
          

|  Error:
|  A is abstract; cannot be instantiated
|  A x = new A ();
|        ^------^
|
          

Abstract methods

  • Abstract methods must be overridden in concrete subclass

abstract class A {
  int f (int x) {
    System.out.format ("A.f (%d)%n", x);
    return (x == 0) ? this.g () : this.f (x - 1); 
  }
  abstract int g ();
}
class B extends A { 


}

//
          

|  Error:
|  B is not abstract and does not override abstract method g() in A
|  class B extends A {
|  ^------------------...
|
          

Final methods

  • Final methods cannot be overridden

abstract class A {
  final int f (int x) {
    System.out.format ("A.f (%d)%n", x);
    return (x == 0) ? this.g () : this.f (x - 1); 
  }
  abstract int g ();
}
class B extends A {
  int f (int x) { System.out.format ("B.f (%d)%n", x); return 0; }
  int g () { System.out.println ("B.g ()"); return 0; }  
}

//
          

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

Hook methods

  • Nonfinal and abstract methods (aka hooks) can be overridden

abstract class A {
  int f (int x) {
    System.out.format ("A.f (%d)%n", x);
    return (x == 0) ? this.g () : this.f (x - 1); 
  }
  abstract int g ();
}
class B extends A {
  int f (int x) { System.out.format ("B.f (%d)%n", x); return 0; }
  int g () { System.out.println ("B.g ()"); return 0; }  
}
A x = new B (); 
x.f (2);
          

B.f (2)
$1 ==> 0


//
          

Hook methods

  • Nonfinal and abstract methods (aka hooks) can be overridden

class A {
  int f (int x) {
    System.out.format ("A.f (%d)%n", x);
    return (x == 0) ? this.g () : this.f (x - 1); 
  }
  int g () { System.out.println ("A.g ()"); return 0; }
}
class B extends A {
  int f (int x) { System.out.format ("B.f (%d)%n", x); return 0; }
  int g () { System.out.println ("B.g ()"); return 0; }  
}
A x = new B (); 
x.f (2);
          

B.f (2)
$1 ==> 0


//
          

Overriding f

  • Access A.f with this?

class A {
  int f (int x) {
    System.out.format ("A.f (%d)%n", x);
    return (x == 0) ? this.g () : this.f (x - 1); 
  }
  int g () { System.out.println ("A.g ()"); return 0; }
}
class B extends A {
  int f (int x) { System.out.format ("B.f (%d)%n", x); return  this.f(x); }
  int g () { System.out.println ("B.g ()"); return 0; }  
}
A x = new B (); 
x.f (2);
          

B.f (2)
B.f (2)
B.f (2)
B.f (2)
... // infinite loop
          

Super

  • Access A.f with super?

class A {
  int f (int x) {
    System.out.format ("A.f (%d)%n", x);
    return (x == 0) ? this.g () : this.f (x - 1); 
  }
  int g () { System.out.println ("A.g ()"); return 0; }
}
class B extends A {
  int f (int x) { System.out.format ("B.f (%d)%n", x); return super.f(x); }
  int g () { System.out.println ("B.g ()"); return 0; }  
}
A x = new B (); 
x.f (2);
          

B.f (2)               A.f (0) 
A.f (2)               B.g ()  
B.f (1)               $1 ==> 0
A.f (1)
B.f (0)
          

Delegation

  • Recursion behaves differently with delegation

class A {
  int f (int x) {
    System.out.format ("A.f (%d)%n", x);
    return (x == 0) ? this.g () : this.f (x - 1); 
  }
  int g () { System.out.println ("A.g ()"); return 0; }
}
class B {  A that = new A ();
  int f (int x) { System.out.format ("B.f (%d)%n", x); return that.f (x); }
  int g () { System.out.println ("B.g ()"); return 0; }
}
B x = new B (); 
x.f (2);
          

B.f (2)               $1 ==> 0
A.f (2)
A.f (1)
A.f (0)
A.g () 
          

Fixing this is painful

  • Too much plumbing

interface I { int f (int x); int g (); }
class A implements I { I back = this; public   
  int f (int x) {
    System.out.format ("A.f (%d)%n", x);
    return (x == 0) ? back.g () : back.f (x - 1); 
  } public
  int g () { System.out.println ("A.g ()"); return 0; }
}
class B implements I { A a; B () { a = new A (); a.back = this; } public 
  int f (int x) { System.out.format ("B.f (%d)%n", x);return a.f (x);}public
  int g () { System.out.println ("B.g ()"); return 0; }
}
B x = new B (); 
x.f (2);
          

B.f (2)               A.f (0) 
A.f (2)               B.g ()  
B.f (1)               $1 ==> 0
A.f (1)
B.f (0)
          

Delegation-based languages

  • Javascript makes this easy

var A = {
    f (x) {
        console.log ("A.f (" + x + ")")
        return (x == 0) ? this.g () : this.f (x - 1); 
    },
    g ()  { console.log ("A.g ()"); return 0; }
}
var B = {
    f (x) { console.log ("B.f (" + x + ")"); return super.f (x); },
    g ()  { console.log ("B.g ()"); return 0; }
}
Object.setPrototypeOf(B, A);
B.f (2);
          

B.f (2)               A.f (0) 
A.f (2)               B.g ()  
B.f (1)               $1 ==> 0
A.f (1)
B.f (0)
          

Delegation versus Inheritance

Example: InputStream

  • java.io.InputStream represents stream of bytes
    • implement read() in subclass
    • inherit other methods

public abstract class InputStream implements Closeable {
  public abstract int read() throws IOException;
  public int read (byte b[], int off, int len) throws IOException {
    ... // calls read()
  }
  public long skip(long n) throws IOException {
    ... // calls read(byte b[], int off, int len) 
  }
  ...
}
          

Example: InputStream

Applications that need to define a subclass of InputStream must always provide a method that returns the next byte of input.
The read(b, off, len) method for class InputStream simply calls the method read() repeatedly.
The skip(n) method of this class creates a byte array and then repeatedly reads into it until n bytes have been read or the end of the stream has been reached.
Subclasses are encouraged to provide a more efficient implementation of [these methods].

Example: Subclass


public class DemoStream extends InputStream {
  int next = 0;
  public int read () throws IOException {
    int result = 32 + next;
    next = (next + 1) % (127 - 32);
    return result;
  }
  public static void main (String[] args) {
    byte[] buffer = new byte[50];
    InputStream is = new DemoStream ();
    for (int i = 0; i < 3; i++) {
      try { int numread = is.read (buffer, 0, buffer.length);
            System.out.println (new String (buffer, 0, numread));
      } catch (IOException e) {}
} } }
          

$ javac DemoStream.java && java DemoStream
 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQ
RSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ !"#$
%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUV