Java, Vererbung

Vererbung und Interfaces


Vererbung

Mit den Konzepten der Vererbung (inheritance) kommen wir zu höheren objektorientierten Konzepten.

Vererbung UML Diagramm
Vererbung UML Diagramm
public class Vererbung {

    public static void main(String[] args) {
	B1 b = new B1();
	System.out.println("b = " + b);

	B1.method();
	b.objmethod();

	A1 a = new A1();
	System.out.println("a = " + a);

	a.objmethod();
	a.andere();

	b = a;
	System.out.println("b = " + b);

	b.objmethod();
	//b.andere(); // Fehler

	C1 c = new C1();
	System.out.println("c = " + c);

	C1.method();
	c.objmethod();

	D1 d = new D1();
	System.out.println("d = " + d);

	d.andere(); 

        b = c;
	System.out.println("b = " + b);
	b = d;
	System.out.println("b = " + b);

    }

}

class B1 { 
    static void method() { }

    void objmethod() { }

    public String toString() {
       return "ich bin ein B1";
    }
}

class A1 extends B1 {

    void andere() { }

    public String toString() {
       return "ich bin ein A1";
    }
}

class C1 extends B1 {

   public String toString() {
       return "ich bin ein C1";
    }

}

class D1 extends A1 {

   public String toString() {
       return "ich bin ein D1";
    }

}

Überschreiben

Eine Methode in A implementiert eine Methode mit gleicher Schnittstelle von B, aber mit anderer Bedeutung (Semantik) (overriding).

Auch virtuelle Vererbung und virtuelle Methoden genannt im Gegensatz zu realer Vererbung (oben).

es gelte class A extends B { ... }

Ueberschreiben UML Diagramm
Ueberschreiben UML Diagramm
public class Ueberschreiben {

    public static void main(String[] args) {
	B2 b = new B2();
	System.out.println("b = " + b);

	B2.method();
	System.out.println("b.objmethod() = " + b.objmethod());

	A2 a = new A2();
	System.out.println("a = " + a);

	System.out.println("a.objmethod() = " + a.objmethod());

	b = a;
	System.out.println("b = " + b);
	System.out.println("b.objmethod() = " + b.objmethod());

    }

}

class B2 { 
    
    static void method() { }

    String objmethod() {
	return "von B2";
    }

    public String toString() {
       return "ich bin ein B2";
    }
}

class A2 extends B2 {

    static void method() { }

    String objmethod() {
	return "von A2";
    }

    void andere() { }

    public String toString() {
       return "ich bin ein A2";
    }
}

Typanpassungen

es gelte wieder class A extends B { ... }

Anpassung UML Diagramm
Anpassung UML Diagramm
public class Anpassung {

    public static void main(String[] args) {
	B3 b = new B3();
	System.out.println("b = " + b);

	A3 a = new A3();
	System.out.println("a = " + a);

        b = a;
	System.out.println("b = " + b);

	if (b instanceof A3) a = (A3) b;  
	System.out.println("a = " + a);

	((A3) b).andere(); 
    }

}

class B3 { 

    void objmethod() { }

    public String toString() {
       return "ich bin ein B3";
    }
}

class A3 extends B3 {

    void andere() { }

    public String toString() {
       return "ich bin ein A3";
    }
}

Beispiel Autos

Wir definieren eine einfache Klasse für Autos (Car). Man kann mit dieser Klasse ein Auto mit den Parametern Modell (model), Baujahr (year) und (Neu-)Preis (price) definieren.

Cars UML Diagramm
Cars UML Diagramm

Die Klassen-Methoden getModel(), getYear() und getPrice() dienen dem Zugriff auf die Objekt-Variablen.

public class Car {
  String model;
  int year;
  int price;

  public Car(String m, int y, int p) {
    model = m;
    year = y;
    price = p;
  }

  public String getName() {
    return model+" [year="+year+"]";
  }

  public int getPrice() {
    return price;
  }

  public static void main(String[] args) {
    Car ford = new Car("ford bronco", 1992, 35000);
    Car vw   = new Car("vw golf", 1984, 25000);

    System.out.println(
       ford.getName()+" costs "+ford.getPrice());
    System.out.println(
       vw.getName()+" costs "+vw.getPrice());
  }
}

Mit dieser Klasse kann man zum Beispiel die Objekte ford und vw erzeugen. Dies ergibt dann folgende Ausgabe.

    ford bronco [year=1992] costs 35000
    vw golf [year=1984] costs 25000

Mit dem Schlüsselwort extends kann man eine Klasse von einer anderen Klasse ableiten. Als Beispiel einer abgeleiteten Klasse von Car sei die Klasse Gebrauchtwagen (UsedCar) definiert, die den zusätzlichen Parameter `gefahrene Kilometer' (mileage) hat.

public class UsedCar extends Car {
  int mileage;

  public UsedCar(String m, int y, int p, int k) {
    super(m, y, p);
    mileage = k;
  }

  public String getName() {
    return super.getName()+" (mileage="+mileage+")";
  }

  public int getMileage() {
    return mileage;
  }

  public int getPrice() {
    return 10000*price/(mileage+10000);
  }
}

Die Methode getMilage() dient wieder dem Zugriff auf die Klassen-Parameter. Der Konstruktor für UsedCar ruft mit super(m, y, p) den Konstruktor der Basisklasse auf, dann wird der Parameter mileage gesetzt. Die Methode getPrice() überschreibt die Methode mit dem gleichen Namen und der gleichen Signatur (d.h. der gleichen Art und Anzahl der Parameter) von der Basisklasse. getPrice() berechnet in dieser Klasse den Gebrauchtwagenpreis in Abhängigkeit von der gefahrenen Kilometerzahl.

Mit diesen Klassen kann man dann zum Beispiel in dem Hauptprogramm main die folgenden Beziehungen definieren und ausdrucken.

  public static void main(String[] args) {
    Car cars[] = new Car[4];
    cars[0] = new Car("ford bronco", 1992, 35000);
    cars[1] = new UsedCar("ford bronco", 1992, 
                          35000, 8000);
    cars[2] = new Car("vw golf", 1984, 25000);
    cars[3] = new UsedCar("vw golf", 1984, 
                          25000, 20000);

    for (int i=0; i<4; i++)
      System.out.println(cars[i].getName()
             +" costs "+cars[i].getPrice());
  }

Als Ausgabe erhält man zum Beispiel.

   java UsedCar

   ford bronco [year=1992] costs 35000
   ford bronco [year=1992] (mileage=8000) costs 19444
   vw golf [year=1984] costs 25000
   vw golf [year=1984] (mileage=20000) costs 8333

Interface

Eine Java-Klasse ist nur von einer anderen Klasse ableitbar. Java kennt keine Mehrfachvererbung (multiple inheritance). Aber mit dem Sprachkonstrukt interface kann eine Klasse mehrere Methodensignaturen implementieren (erben). Ein Interface deklariert nur abstrakte Methoden.

Zum Beispiel kann man ein Interface Rank definieren, das das Vergleichen zweier Objekte erlaubt. compare() gibt eine positive ganze Zahl zurück, falls dieses Objekt (this) in der implementierten Ordnung vor dem anderen Objekt (obj) steht.

Ranks UML Diagramm
Ranks UML Diagramm
  public interface Rank {
    public int compare(Rank obj) 
           throws RankException;
  }

Ist die implementierte Ordnung nicht total, so wird für unvergleichbare Objekte eine RankException ausgelöst.

  public class RankException 
         extends RuntimeException {
  }

Eine Subklasse von Rectangle (aus java.awt), die dieses Interface unterstützt, kann man wie folgt definieren.

  public class RankedRect extends Rect implements Rank {
    public RankedRect(int w, int h) {
      super(w,h);
    }

    public int compare(Rank obj) 
           throws RankException {
      try {
        return area() - ((Rect)obj).area();
      }
      catch (ClassCastException e) {
        throw new RankException();
      }
    }
  }

In einer ähnlichen Weise kann man eine Subklasse von Car definieren.

  public class RankedCar extends Car implements Rank {
    public RankedCar(String m, int y, int p) {
      super(m,y,p);
    }

    public int compare(Rank obj) {
      try {
        return getPrice()-((Car)obj).getPrice();
      }
      catch (ClassCastException e) {
        throw new RankException();
      }
    }
  }

Diese beiden Klassen können behandelt werden, als ob sie vom Typ Rank wären. Zum Beispiel als Argument in einer Methode foo(Rank obj). Eine Klasse kann mehrere Interfaces implementieren. Bei der Erzeugung von Objekten von Klassen, die ein Interface implementieren, kann man die Struktur, die nicht von dem Interface stammt, ignorieren.

   Rank r = new RankedCar(model, year, price);

Dann ist aber nur noch die Verwendung der Methode r.compare(.) möglich und nicht mehr r.getModel(). Um dennoch getModel() zu verwenden, muß erst ein Cast (eine Typenanpassung)   auf RankedCar gemacht werden:

   ( (RankedCar) r ).getModel()

Abstrakte Klassen

Man kann eine Klasse definieren, die keine vollständige Implementierung der deklarierten Methoden enthält.

Diese kann man sich als Mischform zwischen einem Interface und einer richtigen Klasse vorstellen. Ein Teil der Methoden wird implementiert, und für einen anderen Teil der Methoden werden nur die Spezifikationen (das Interface) festgelegt.

Cell UML Diagramm
Cell UML Diagramm

Eine solche Klasse wird als abstrakte Klasse bezeichnet und muß mit dem Schlüsselwort abstract gekennzeichnet werden.

  public abstract class Cell {
      int size;
     
      public int getSize() {
             return size;
      }
     
      public abstract void interact(Cell neighbor);
  }

Aus abstrakten Klassen können können keine Objekte erzeugt werden. Aus einer Subklasse, die die fehlenden Implementationen definiert, können Objekte erzeugt werden.

Beispiel: BitString

BitString UML Diagramm
BitString UML Diagramm

Interface: BitStringInterface

Methoden: toString() und toBitString() liefern die entsprechende Darstellung als Binärfolge.

public interface BitStringInterface {

    public String toString(); 

    public String toBitString();

}

Abstrakte Klasse: AbstractBitString

Methoden: toString() wird mit Hilfe von toBitString() implementiert.

public abstract class AbstractBitString 
                implements BitStringInterface {

    protected int art;
    final static int BYTE  = 1;
    final static int SHORT = 2;
    final static int INT   = 3;
    final static int LONG  = 4;

    final static byte BYTEsize  =  7;
    final static byte SHORTsize = 15;
    final static byte INTsize   = 31;
    final static byte LONGsize  = 63;

    protected long ls = 0;

    public String toString() {
	return toBitString();
    }
 
    public abstract String toBitString();

}

Klasse: BitStringSign

Darstellung von ganzen Zahlen als Binärfolge. Die Zahlen werden als Paare von Vorzeichen und positiven Zahlen dargestellt.

Konstruktoren: für byte, short, int und long,

Methoden: toBitString() liefert Zeichenkette entsprechend dem Datentyp.
bitStringSign() liefert Zeichenkette entsprechend der 'Vorzeichen, Grösse'-Darstellung.

public class BitStringSign extends AbstractBitString {

    public BitStringSign(byte b) {
        art = BYTE;
	ls = b;
    }

    public BitStringSign(short s) {
        art = SHORT;
	ls = s;
    }

    public BitStringSign(int s) {
        art = INT;
	ls = s;
    }

    public BitStringSign(long s) {
        art = LONG;
	ls = s;
    }

    public String toBitString() {
        switch (art) {
          case  BYTE: return bitStringSign(BYTEsize,ls);
          case SHORT: return bitStringSign(SHORTsize,ls);
          case   INT: return bitStringSign(INTsize,ls);
          case  LONG: return bitStringSign(LONGsize,ls);
	     default: return ""+ls;
	}
    }

    protected static String bitStringSign(byte l, long b) {
        String e = "";
        long x = 0;
        String s = "";
	byte einz = (byte)1;

        if (b < 0L ) {
	    x = (long)-b; s = "1";
	} else {
            x = (long)b; s = "0";
	}

        for (int i = 0; i < l; i++) { 
	    // (x % 2) == 0 
            if ( ( ((byte)x) & einz) == 0 ) { 
               e = "0" + e; 
            } else { 
               e = "1" + e; 
            }
            x = (long)(x >> 1);
	}
        return s+e;
    }
}

Klasse: BitStringComplement

Darstellung von ganzen Zahlen als Binärfolge. Die Zahlen werden im Zweier-Komplement dargestellt.

Konstruktoren: für byte, short, int und long,

Methoden: toBitString() liefert Zeichenkette entsprechend dem Datentyp.
bitStringComplement() liefert Zeichenkette entsprechend der Komplement-Darstellung.

public class BitStringComplement extends AbstractBitString {

    public BitStringComplement(byte b) {
        art = BYTE;
	ls = b;
    }

    public BitStringComplement(short s) {
        art = SHORT;
	ls = s;
    }

    public BitStringComplement(int s) {
        art = INT;
	ls = s;
    }

    public BitStringComplement(long s) {
        art = LONG;
	ls = s;
    }

    public String toBitString() {
        switch (art) {
          case  BYTE: return bitStringComplement(BYTEsize,ls);
          case SHORT: return bitStringComplement(SHORTsize,ls);
          case   INT: return bitStringComplement(INTsize,ls);
          case  LONG: return bitStringComplement(LONGsize,ls);
	     default: return ""+ls;
	}
    }

    protected static String bitStringComplement(byte l, long b) {
        String e = "";
        long x = b;
	byte einz = (byte)1;

        for (int i = 0; i < l+1; i++) { 
	    // (x % 2) == 0 
            if ( ( ((byte)x) & einz) == 0 ) { 
               e = "0" + e; 
            } else { 
               e = "1" + e; 
            }
            x = (long)(x >> 1);
	}
        return e;
    }
}

Klasse: BitTest

Testen der zwei Darstellungen. Es werden Objekte von BitStringSign und BitStringComplement erzeugt. Diese Objekte können in Variablen der entsprechenden Klassen abgespeichert werden, aber auch in Variablen vom Typ BitStringInterface als auch von Typ AbstractBitString.

public class BitTest {

    public static void main(String[] args) {
        test1();
        test2();
        test3();
        test4();
    }

    static void test1() {
        BitStringInterface a = new BitStringSign((int)7);
        System.out.println("sign(     7) = " + a);
        BitStringInterface b = new BitStringComplement((int)7);
        System.out.println("cmpl(     7) = " + b);

        BitStringInterface g = new BitStringSign((int)-7);
        System.out.println("sign(    -7) = " + g);
        BitStringInterface h = new BitStringComplement((int)-7);
        System.out.println("cmpl(    -7) = " + h);

        BitStringInterface d = new BitStringSign((short)32767);
        System.out.println("sign( 32767) = " + d);
        BitStringInterface c = new BitStringComplement((short)32767);
        System.out.println("cmpl( 32767) = " + c);

        AbstractBitString e = new BitStringSign((int)-32767);
        System.out.println("sign(-32767) = " + e);
        AbstractBitString f = new BitStringComplement((int)-32767);
        System.out.println("cmpl(-32767) = " + f);

        System.out.println();
    }


    static void test2() {
        short x = -1;
        for (byte l = 0; l <= 15; l++ ) {
            BitStringInterface g = new BitStringSign((short)x);
            System.out.println("sign( "+ x + ") = " + g);
            x *= (short)2;
        }
        System.out.println();
    }


    static void test3() {
        short x = -1;
        for (byte l = 0; l <= 15; l++ ) {
            AbstractBitString h = new BitStringComplement((short)x);
            System.out.println("cmpl( "+ x + ") = " + h);
            x *= (short)2;
        }
        System.out.println();
    }

    static void test4() {
        short x = -1;
        for (byte l = 0; l <= 15; l++ ) {
            System.out.println("Integer( "+ x + ") = " 
                         + Integer.toBinaryString(x));
            x *= (short)2;
        }
        System.out.println();
    }
}

Im letzten Test erfolgt ein Vergleich mit der 'eingebauten'  Funktion Integer.toBinaryString(x))

Ausgabe:

sign(     7) = 00000000000000000000000000000111
cmpl(     7) = 00000000000000000000000000000111
sign(    -7) = 10000000000000000000000000000111
cmpl(    -7) = 11111111111111111111111111111001
sign( 32767) = 0111111111111111
cmpl( 32767) = 0111111111111111
sign(-32767) = 10000000000000000111111111111111
cmpl(-32767) = 11111111111111111000000000000001

sign( -1) = 1000000000000001
sign( -2) = 1000000000000010
sign( -4) = 1000000000000100
sign( -8) = 1000000000001000
sign( -16) = 1000000000010000
sign( -32) = 1000000000100000
sign( -64) = 1000000001000000
sign( -128) = 1000000010000000
sign( -256) = 1000000100000000
sign( -512) = 1000001000000000
sign( -1024) = 1000010000000000
sign( -2048) = 1000100000000000
sign( -4096) = 1001000000000000
sign( -8192) = 1010000000000000
sign( -16384) = 1100000000000000
sign( -32768) = 1000000000000000

cmpl( -1) = 1111111111111111
cmpl( -2) = 1111111111111110
cmpl( -4) = 1111111111111100
cmpl( -8) = 1111111111111000
cmpl( -16) = 1111111111110000
cmpl( -32) = 1111111111100000
cmpl( -64) = 1111111111000000
cmpl( -128) = 1111111110000000
cmpl( -256) = 1111111100000000
cmpl( -512) = 1111111000000000
cmpl( -1024) = 1111110000000000
cmpl( -2048) = 1111100000000000
cmpl( -4096) = 1111000000000000
cmpl( -8192) = 1110000000000000
cmpl( -16384) = 1100000000000000
cmpl( -32768) = 1000000000000000

Integer( -1) = 11111111111111111111111111111111
Integer( -2) = 11111111111111111111111111111110
Integer( -4) = 11111111111111111111111111111100
Integer( -8) = 11111111111111111111111111111000
Integer( -16) = 11111111111111111111111111110000
Integer( -32) = 11111111111111111111111111100000
Integer( -64) = 11111111111111111111111111000000
Integer( -128) = 11111111111111111111111110000000
Integer( -256) = 11111111111111111111111100000000
Integer( -512) = 11111111111111111111111000000000
Integer( -1024) = 11111111111111111111110000000000
Integer( -2048) = 11111111111111111111100000000000
Integer( -4096) = 11111111111111111111000000000000
Integer( -8192) = 11111111111111111110000000000000
Integer( -16384) = 11111111111111111100000000000000
Integer( -32768) = 11111111111111111000000000000000

Vergleiche auch die Hüll-Klassen java.lang.Integer und java.lang.Long. Für die Klassen java.lang.Byte und java.lang.Short existieren keine vergleichbaren Methoden.


© Universität Mannheim, Rechenzentrum, 1998-2005.

Heinz Kredel
Last modified: Mon Jan 23 22:52:52 CET 2006