Dynamische Polymorphie (Objektorientierung)

ein Bezeichner mit mehreren Bedeutungen

poly-morph = viel-gestaltig.     Formen der Polymorphie:

Motivation: Objekt = Daten + Verhalten.

Einfachste Implementierung:

typedef struct {
   int x; int y; // Daten
   void (*print) (FILE *fp); // Verhalten
} point;
point *p; ... ; (*(p->print))(stdout);

Anwendung: Datei-Objekte in UNIX (seit 1970)

(Merksatz 1: all the world is a file) (Merksatz 2: those who do not know UNIX are doomed to re-invent it, poorly)

(d. h. objektorientiert, aber ohne Klassen)

Objekte, Attribute, Methoden:

var o = { a : 3, 
  m : function (x) { return x + this.a; } };
Vererbung zwischen Objekten:
var p = { __proto__ : o };
Attribut (/Methode) im Objekt nicht gefunden weitersuchen im Prototyp ...Prototyp des Prototyps ...

Übung: Überschreiben

p.m = function (x) { return x + 2*this.a }
var q = { __proto__ : p }
q.a = 4
alert (q.m(5))

gemeinsame Datenform und Verhalten von Objekten

typedef struct { int (*method[5])(); } cls;
typedef struct {
    cls * c;
} obj;
obj *o; ... (*(o->c->method[3]))();

allgemein: Klasse:

Objekt:

Motivation: Methode soll wissen, für welches Argument sie gerufen wurde

typedef struct { int (*method[5])(obj *o); 
} cls;
typedef struct {
    int data [3]; // Daten des Objekts
    cls *c; // Zeiger auf Klasse
} obj;
obj *o; ... (*(o->c->method[3]))(o);
int sum (obj *this) {
    return this->data[0] + this->data[1]; }
jede Methode bekommt this als erstes Argument

(in Java, C# geschieht das implizit)

Def: Klasse D ist abgeleitet von Klasses C :

Anwendung: dynamische Polymorphie

class C { 
  int x = 2; int p () { return this.x + 3; } 
}
C x = new C() ; int y = x.p ();
Überschreiben:
class E extends C { 
  int p () { return this.x + 4; } 
}
C x =           // statischer  Typ: C
      new E() ; // dynamischer Typ: E
int y = x.p ();

class C { 
   void p () { ... q(); ...  }; 
   void q () { .. };
}
Jetzt wird q überschrieben (evtl. auch unabsichtlich--in Java), dadurch ändert sich das Verhalten von p.
class D extends C {
   void q () { ... }
}
Korrektheit von D abhängig von Implementierung von C

object-orientation is, by its very nature, anti-modular ...

http://existentialtype.wordpress.com/2011/03/15/teaching-fp-to-freshmen/


class C { 
  final int x; final int y;
  C (int x, int y) { this.x = x; this.y = y; }
  int hashCode () { return this.x + 31 * this.y; }
}
nicht so:
  
  public boolean equals (C that) {
    return this.x == that.x && this.y == that.y;
  }

...sondern so:

public boolean equals (Object o) {
  if (! (o instanceof C)) return false;
  C that = (C) o;
  return this.x == that.x && this.y == that.y;
}
Die Methode boolean equals(Object o) wird aus HashSet aufgerufen.

Sie muß deswegen überschrieben werden.

Das boolean equals (C that) hat den Methodenamen nur überladen.

für diese findet kein dynmischer Dispatch statt. (Beispiele--Puzzle 48, 54)

Damit das klar ist, wird dieser Schreibstil empfohlen:

Durch extends/implements entsteht eine Halbordnung auf Typen

Bsp. class C; class D extends C; class E extends C definiert Relation (≤) = {(C, C),(D, C),(D, D),(E, C),(E, E)} auf T = {C, D, E}

Relation 2 auf T2 :

(t1, t2)≤2(t1', t2') : $ \iff$t1t1'∧t2t2'

es gilt (D, D)≤2(C, C);(D, D)≤2(C, D);(C, D)≤2(C, C);(E, C)≤2(C, C) .

Auflösung von p (new D(), new D()) bzgl.

static void p (C x, D y);
static void p (C x, C y);
static void p (E x, C y);

interface I { void P (); }
static void Q (IList<I> xs) 
    { foreach (I x in xs) { x.P(); } }
static void R<C> (Action<C> S, IList<C> xs)
    { foreach (C x in xs) { S(x); } }
für gleichzeitige Behandlung mehrerer Objekte
ist Vererbungspolymorphie meist ungeeignet

(z. B. Object.equals(Object o) falsch, Comparable<T>.compareTo(T o) richtig)

Warum geht das nicht:

class C { } 

class E extends C { void m () { } }
 
List<E> x = new LinkedList<E>();

List<C> y = x; // Typfehler

Antwort: wenn das erlaubt wäre, dann:

List<? extends Number> z = 
    Arrays.asList(new Double[]{1.0, 2.0});
z.add(new Double(3.0));

Kovarianz (in P), Kontravarianz (out P)

class C {} class E : C {}

interface I<in P> { }
class K<P> : I<P> { }

I<C> x = new K<C>();
I<E> y = x;

Unterscheidung: Schranken/Varianz:

bei Schranken geht es um die Instantiierung (Wahl der Typargument)

bei Varianz um den erzeugten Typ (seine Zuweisungskompatibilität)

das gibt keinen Typfehler:

class C { }
class E extends C { void m () { } }

E [] x = { new E (), new E () };
C [] y = x;

y [0] = new C ();
x [0].m();
aber ...(Übung)

warum ist die Typprüfung für Arrays schwächer als für Collections?

Historische Gründe. Das sollte gehen:

void fill (Object[] a, Object x) { .. }
String [] a = new String [3];
fill (a, "foo");

Das sieht aber mit Generics besser so aus: ...

2015-08-17