Java

Vererbung und Interfaces


Vererbung

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

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 { ... }

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 { ... }

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.

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

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.

  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.

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

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 {

    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 {

    private 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;

    private long ls = 0;

    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;
	}
    }

    public static String bitStringSign(byte l, long b) {
        String e = "";
        long x = 0;
        String s = "";
        long zwei = (long)2;

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

        for (int i = 0; i < l; i++) { 
            if ( (x % zwei) == 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 {

    private 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;

    private long ls = 0;

    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;
	}
    }

    public static String bitStringComplement(byte l, long b) {
        String e = "";
        long x = b;
        long zwei = (long)2;

        for (int i = 0; i < l+1; i++) { 
            if ( (x % zwei) == 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("a(     7) = " + a);
	BitStringInterface b = new BitStringComplement((int)7);
        System.out.println("b(     7) = " + b);

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

	AbstractBitString e = new BitStringSign((int)-32767);
        System.out.println("e(-32767) = " + e);
	AbstractBitString f = new BitStringComplement((int)-32767);
        System.out.println("f(-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("g( "+ 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("h( "+ x + ") = " + h);
	    x *= (short)2;
	}
        System.out.println();
    }

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

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

Ausgabe:

a(     7) = 00000000000000000000000000000111
b(     7) = 00000000000000000000000000000111
c( 32767) = 0111111111111111
d( 32767) = 0111111111111111
e(-32767) = 10000000000000000111111111111111
f(-32767) = 11111111111111111000000000000001

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

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

i( -1) = 11111111111111111111111111111111
i( -2) = 11111111111111111111111111111110
i( -4) = 11111111111111111111111111111100
i( -8) = 11111111111111111111111111111000
i( -16) = 11111111111111111111111111110000
i( -32) = 11111111111111111111111111100000
i( -64) = 11111111111111111111111111000000
i( -128) = 11111111111111111111111110000000
i( -256) = 11111111111111111111111100000000
i( -512) = 11111111111111111111111000000000
i( -1024) = 11111111111111111111110000000000
i( -2048) = 11111111111111111111100000000000
i( -4096) = 11111111111111111111000000000000
i( -8192) = 11111111111111111110000000000000
i( -16384) = 11111111111111111100000000000000
i( -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-2002.

Heinz Kredel
Last modified: Sun Dec 22 16:36:39 CET 2002