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;
++ -++ -. .
-
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;
++ -++ -. .
-
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; |
| 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; |
| 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
A.f
A.f
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)
| ^--------------------------------------------------------------^
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
| 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
B.f
B.f
B.f
...
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
| 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
-
GoF: Favor 'object composition' over 'class inheritance'
-
Delegate to methods of components
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 { |
| ... |
| } |
| public long skip(long n) throws IOException { |
| ... |
| } |
| ... |
| } |
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
CSC447 Concepts of Programming Languages Dynamic Dispatch and Inheritance Instructor: James Riely