8. Vererbung

Vererbung ist ein wichtiger Bestandteil der objektorientierten Programmierung. Vererbung ergänzt die bisher vorgestellten Konzepte der

um ein weiteres Strukturierungsmittel.

Der erste Abschnitt beschäftigt sich mit dern Konzepten der Vererbung, die folgenden beschreiben die Implementierung in Java.

8.1 Das Konzept der Vererbung

Die Ziele die mit Vererbung in der objektorientierten Programmierung verfolgt werden sind:

  • Wiederverwendung
    • eine Klasse vererbt Eigenschaften an eine andere.
    • Dies bedeutet: Man nutzt Daten und Methoden einer Klasse 
  • Modellierung in Hierarchien
    • Hierarchien sind gängige Konzepte des Alltagsleben und finden sich in der Begrifflichkeit des Alltagslebens wieder. Eine Modellierung die Hierarchien unterstützt und vereinfacht das Verständnis und die Kommunikation.
    • Hierarchien sind Konzepte die stark bei der Abgrenzung zwischen ähnlichen Dingen helfen

Beispiel einer Vererbungshierarchie:

Klassenhierarchie

Die gleiche Begriffshierarchie lässt sich auch als Mengendiagramm darstellen:

Mengendiagramm

Alle Mitglieder einer Teilmenge haben die gleichen Eigenschaften.

Das Mengendiagramm verdeutlicht, dass es sich bei der Vererbungshierarchie um eine "Ist-ein" Relation handelt.

 Vererbung und Klassen

Die Konzepte der Vererbungshierarchien lässt sich leicht auf Klassen abbilden

  • Unterklassen können von Oberklassen Methoden und Attribute erben und direkt in Ihrer Implementierung benutzen
  • Typischerweise haben Unterklassen nur direkten Zugriff auf öffentliche Methoden und Attribute
  • Unterklassen können selbst zusätzliche Attribute und Methoden implementieren und so spezialisiertere Aufgaben übernehmen
  • Instanzen von Unterklassen müssen sich wie Instanzen der Oberklassen verhalten. Da die Instanzen der Unterklasse immer eine Teilmenge der Instanzen der Oberklasse sind. Sie dürfen jedoch zusätzliche Eigenschaften implementieren

Das Diagramm rechts verdeutlicht die Beziehungen zwischen den Klassen, ihrer Bedeutung und der Typisierung. 

Sub- Supertypbeziehung

Ein- und Mehrfachvererbung

Programmiersprachen wie C++ erlauben die Benutzung von Mehrfachvererbung. Die Sprache Java erlaubt jedoch nur eine einfache Vererbung.

  • Einfachvererbung: Eine Unterklasse erbt von genau einer Oberklasse
  • Mehrfachvererbung: Eine Unterklasse erbt von mehreren Oberklassen

Das Konzept der Mehrfachvererbung durchbricht zu einem gewissen Grad das Konzept der Spezialisierung-Generalisierung. Bei der Einfachvererbung sind Personen Mitarbeiter oder Berater aber nicht beides gleichzeitig. Bei der Mehrfachvererbung mit einer einzelnen Basisklasse werden Unterklassen zuerst als disjunkte Klassen definiert. Später sollen Unter-Unterklassen jedoch die gleichen Eigenschaften über verschieden Vererbungspfade erben.

Anmerkung

Mehrfachvererbung erhöht die Mächtigkeit sowie die Komplexität einer Sprache erheblich. Es kann hier leicht zu Effekten kommen die ein Entwickler nicht als intuitiv empfindet. Das Risiko des Fehlverhaltens einer Anwendung steigt hierdurch.

Beim Entwurf der Sprache Java wurde bewusst auf Mehrfachvererbung verzichtet um die Sprache einfach zu halten und um die Implementierung eines effizienten Laufzeitsystems zu vereinfachen.

Die Java Interface (Schnittstellen) Technologie bietet eine "Hintertür" zur Implementierung von Fällen in denen eine Mehrfachvererbung wünschenswert ist.

8.2 Vererbung in Java

Die Vererbungsbeziehung wird in Java mit dem Schlüsselwort extends beschrieben. Beispiel:

class Mitarbeiter extends Person {
   /* Klassenimplementierung */
   }

Die Klasse Mitarbeiter ist eine Spezialisierung der Klasse Person. Sie erbt die öffentlichen Attribute und Methoden der Klasse Person.

Alle Klassen in Java erben direkt oder indirekt von der Java Basisklasse Object. Wird bei einer Klassendeklaration keine extends Klausel angegeben so wird die Klasse automatisch von der Klasse Object abgeleitet.

Die Klasse Object

... stellt für alle Javaklassen eine minimale Infrastruktur zur Verfügung:

  • die Möglichkeit dynamisch Objekte auf dem Heap anzulegen
  • eine Identität und die damit verbundene Möglichkeit zum Vergleich von Objekten
  • die Möglichkeit des automatischen Löschens wenn keine Referenz mehr auf eine Instanz zeigt.
  • Möglichkeit der Instrospektion (Dynamisches Erkunden der Struktur der Klasse)

Zugriff auf geerbte Methoden und Attribute

Oberklassen vererben öffentliche (public, protected) Methoden und Attribute an Unterklassen. Das heißt, dass sie wie eigene Attribute verwendet werden können.

Die Beispielklasse Person verfügt über:

  • private Attribute für Vor- und Nachnamen
  • öffentliche Methode zum Auslesen des Gesamtnamens
  • öffentliche Methode zum Setzen des Namens
  • ein öffentliches Attribut zum Verwalten des Alters
  • Einen Konstruktor zum Setzen des Namens

Die Oberklasse Person

public class Person {
    private String name;
    private String firstName;
    public int age;
    
    public Person(String ln, String fn) {
        name = ln; firstName = fn; }
    
    public Person() { this("Doe","John");}
    
    public void setName(String ln, String fn) {
        name = ln; firstName = fn; }
    public String fullName() {return (name + " " + firstName);}    
}

Die Klasse Employee soll jetzt die öffentlichen Eigenschaften der Oberklasse Person nutzen. So kann eine redundante Programmierung der schon existierenden Funktionalität vermieden werden.

Die Unterklasse Employee

public class Employee extends Person {
    private String employeeId;
    public Employee(String ln, String fn, String EmpId, int a) {
        super(ln,fn); // Java ruft hier den Konstruktor der Oberklasse auf
        employeeId = EmpId;
        age = a;
    }
    public String getEmployeeId() {return employeeId;}
    public String printAll() {
        return (
                fullName() + " " +
                employeeId + " " +
                age);}
    }

Benutzung der Klasse

Die so implementierte Klasse kann wie folgt verwendet werden:

public class MainEmployee {
    public static void main(String[] args) {
        Employee ceo = new Employee("Doe","Jane", "1", 25);
        Employee cto = new Employee("Miller","John","2", 30);

        cto.age++;
        System.out.println(ceo.printAll());
        System.out.println(cto.printAll());
    }
}

Dies ergibt die Konsolenausgabe:

Doe Jane 1 25
Miller John 2 31 

Alle öffentlichen Attribute und Methoden (Beispiel age) der Klasse Person können in der Klasse Employee genutzt werden als ob es eigene/lokale sind.

Die Vererbung funktioniert jedoch nicht rückwärts. Man kann einer Instanz von Person keine Angestelltennummer (employeeId) zuweisen. Sie existiert in der Klasse Person nicht.

"Vertragsrecht": Alle Instanzen von Employee müssen sich auch genau wie Instanzen von Person verhalten. Sie sind auch Instanzen der Klasse Person. Andere Konsumenten im Programm verlassen sich auf dieses Verhalten, diesen "Vertrag". Dies bedeutet:

  • Die Unterklasse muss eventuell erzwungene Intialisierungen der Oberklasse achten in dem Sie rekursiv deren Konstruktoren aufruft.
  • Veröffentlichte Attribute und Methoden können/sollen nicht mehr blockiert werden. Beispiel: In einem Unternehmen darf das Alter aufgrund von rechtlichen Bestimmungen nicht veröffentlicht werden

Vererbung in UML

Implementierung ohne Vererbung Implementierung mit Vererbung

Hier eine Implementierung der Klasse Employee ohne Vererbung. Die Implementierung entspricht der rechten Implementierung mit Vererbung. Die Methoden der Klasse haben hier aber direkten Zugriff auf die privaten Attribute name, firstName.

 

Eine Implementierung der Klasse Employee mit Vererbung der Klasse Person Vererbung

Vererbung privater Attribute und Methoden

Private Attribute werden nicht vererbt!

Sie sind nur innerhalb der Klasse sichtbar. Eine Unterklasse hat keinen priviligierten Zugriff auf private Methoden. Sie werden nicht vererbt. Eine Unterklasse muss wie jede andere Klasse über vorhandene öffentliche Zugriffsmethoden auf die privaten Attribute zugreifen.

Vererbung und Konstruktoren

Konstruktoren werden in Java nicht vererbt. Man Sie nur indirekt über den new() Operator für eine gegebene Klasse aufrufen.

Konstruktoren müssen für jede Unterklasse implementiert werden. Die Konstruktoren der Unterklasse müssen jedoch in der Lage sein existierende Konstruktoren der Oberklasse aufzurufen, da sie für die Initialisierung der Oberklasse notwendig sind. Hierzu wird das Schlüsselwort super() verwendet. 

Hiermit kann man jetzt die Klasse Employee() eleganter und robuster Initialisieren

public class Employee extends Person {
    private String employeeId;
    public Employee(String ln, String fn, String EmpId, int a) {
        super(fn,ln); // Java ruft hier den Konstruktor der Oberklasse auf
        employeeId = EmpId;
        age = a;
    }
    public void printAll() {
        System.out.println (
                fullName() + " " +
                employeeId + " " +
                age);
    }

Wichtig: Der Aufruf des Konstruktors der Oberklasse muss als erster Befehl im Konstruktor der Unterklasse stehen. Die Attribute der Oberklasse müssen als erstes initialisiert werden, bevor die Attribute der Unterklasse initialisiert werden. Nur so kann gewährleistet werden dass sich Instanzen der Oberklasse korrekt verhalten. Würde der Aufruf des Konstruktors der Oberklasse nicht als erstes geschehen, könnte die Unterklasse die Oberklasse inkorrekt manipulieren.

Unterklassenkonstruktoren ohne super() Aufruf

Konstruktoren müssen nicht einen bestimmten Konstruktor der Oberklasse explizit aufrufen.

Wird kein super() Aufruf implementiert, so generiert der Übersetzer automatisch einen super() Aufruf der den Standard(default)konstruktor aufruft.

Hiermit wird immer ein vollständiges Durchlaufen der Klassenhierachie beim Initialisieren gewährleistet.

Besitzt die Oberklasse nur Konstruktoren mit Übergabeparameter so muss einer dieser Konstruktoren über super() aufgerufen werden.

Im folgenden Beispiel kann man die Initialisierungskette einer Klasse C sehen die aus B abgeleitet wird. B wird aus A abgeleitet:

public class A {
    public A() {System.out.println("Constructor A called");}
}

public class B extends A{
    public B() {System.out.println("Constructor B called");}
}

public class C extends B {
    public C() {System.out.println("Constructor C called");}
}

Das erzeugen einer Instanz von C:

C c = new C();

Triggert die folgenden Ausgaben:

Constructor A called
Constructor B called
Constructor C called

 

8.3 Überschreiben (Overriding)

Das im vorgehenden Abschnitt vorgestellte Konzept des Vererbens ging immer von einer Erweiterung der Klasse aus. Das heißt mehr und mehr Attribute und Methoden kommen hinzu.

Es kommt jedoch vor, das eine spezialisierte Klasse eine existierende Methode der Oberklasse "verfeinern" möchte. Dieses Konzept der Implementierung einer Methode oder Attributs welches eine Methode oder Attribut aus der Oberklasse neu implementiert, wird in der objektorientierten Programmierung Überschreiben (Overriding) genannt.

Überschreiben von Methoden

Java erlaubt die Methode einer Oberklasse zu überschreiben, das bedeutet sie zu verdecken, wenn die folgenden Bedingungen für die Deklaration der Methode erfüllt sind

  • Namensgleicher Methodenname
  • Eingabeparameter sind von Anzahl, Reihenfolge und Typ identisch
  • Rückgabeparameter ist identisch
  • Zugriffsrechte der Methode der Oberklasse (public, protected, private) werden nicht eingeschränkt

Der Methodenrumpf kann mit einer beliebigen Implementierung ausgeführt werden. Mit dieser Technik kann eine Unterklasse eine eigene Implementierung für eine allgemeinere Methode einer Überklasse zur Verfügung stellen.

Im vorhergehenden Fall der Klasse Employee kann man eine spezialisierte Klasse Manager implementieren, die ein zusätzliches Budget verwalten kann. Das Budget soll ebenfalls mit der Methode printAll() ausgeben werden.

public class Manager extends Employee {
    public int budget;

    public Manager(String ln, String fn, String EmpId, int a, int b) {
        super(ln, fn,EmpId,a);
        budget = b;
    }
    public String printAll()
        { return (
                fullName() + " " +
                getEmployeeId() + " " +
                age + " " +
                budget); }
}

Das Hauptprogramm:

public class Main {
    public static void main(String[] args) {
        Employee ceo = new Employee("Doe","Jane", "1", 25);
        Employee cto = new Employee("Miller","John","2", 30);
        Employee man1 = new Manager("Johnson","Susan","3", 29, 30000);

        cto.age++;
        System.out.println(ceo.printAll());
        System.out.println(cto.printAll());
        System.out.println(man1.printAll());;
    }
}

...erzeugt die folgenden Ausgaben:

Doe Jane 1 25
Miller John 2 31
Johnson Susan 3 29 30000

Überschriebene Methoden sind miteinander verwandt und eine Benutzung der Methode der Oberklasse ist gewünscht um Codereplikation und Redundanz zu vermeiden. Hierfür existiert das Schlüsselwort super. Es erlaubt das Aufrufen der überschriebenen Methode mit der folgenden Syntax:

super.MethodenName(para_1, ..,para_n)

Im Fall der Klasse Manager kann man die printAll() Methode mit dem Schlüsselwort super vereinfachen:

public class Manager extends Employee {
    public int budget;

    public Manager(String ln, String fn, String EmpId, int a, int b) {
        super(ln, fn,EmpId,a);
        budget = b;
    }
    public String printAll()
        { return ( super.printAll() + " " + budget); }
}

Hinweis: Die Syntax  super.super.methodenname() ist nicht möglich. Man kann nicht die Methode einer Ober-Oberklasse unter Auslassung der Oberklasse aufrufen.

Suchalgorithmus der Laufzeitumgebung

Da in Java alle Klassen einzeln übersetzt werden können, kann man erst zur Laufzeit entscheiden welche Methode aufgerufen werden muss (dynamic invocation). Die Laufzeitumgebung geht bei jedem Aufruf wie folgt vor

  • Bestimme Typ des Objekts
  • Versuche Methode zum passenden Typ (Klasse) auszuführen
  • Versuche rekursiv in der Oberklasse die Methode auszuführen

Überschreiben von Attributen

Für das Überschreiben von Attributen gelten die gleichen Regeln wie für das Überschreiben von Methoden:

  • Namensgleicher Attributname
  • Zugriffsrechte des Attributs der Oberklasse (public, protected) werden nicht eingeschränkt

Private Attribute werden nicht vererbt. Sie sind der Unterklasse weder bekannt noch zugänglich!

Der Zugriff auf ein überschriebenes Attribut der Oberklasse geht symmetrisch zu den Methoden mit dem super Schlüsselwort. Beispiel:

a = super.a * 2;

Die @Overriding Annotation

Java kennt seit JDK 1.5 Annotationen. Annotationen sind Hinweise für den Übersetzer.

Es ist guter Stil wenn man einer Methode die Überschrieben wird die Annotation

@Overriding

voranstellt. @Overriding ist eine fest eingebaute Annotation, die den Übersetzer zu einer Fehlermeldung zwingt wenn die aufgewählte Methode nicht die Methode der Oberklasse überschreibt (Siehe Oracle Tutorial Annotationen).

Javaentwicklungsumgebungen tendieren eine Warnung zu Erzeugen, falls eine überschriebene Methode nicht mit dieser Annotation erzeugt wurde.

 

8.4 Abstrakte und finale Klassen

Finale Klassen und Methoden

Java bietet die Möglichkeit Klassen und Methoden zu implementieren die nicht mehr abgeleitet oder überschrieben werden dürfen. Hierzu dient das Schlüsselwort final.

Finale Methoden

Mit dem Schlüsselwort final man das Überschreiben der Methode in einer Unterklasse unterbinden. Ein typsiches Beispiel hierfür ist:

public final void print() { /* Methodenrumpf */}

Unterklassen mit überschriebenen Methoden sind ein sehr mächtiges Konzept für den Entwickler einer Unterklasse. Eine Unterklasse kann inhaltlich eine Oberklasse in weiten Bereichen neu implementieren, solange die Signaturen der Methoden gewahrt bleiben und ein Konstruktor der Oberklasse benutzt. Die Kontrolle liegt fast vollständig in den Händen der Unterklasse. das Schlüsselwort final erlaubt dem Entwickler der Oberklasse

  • das Überschreiben zu verbieten
  • und damit die Sichtbarkeit der Methode der Oberklasse für beliebige Konsumenten der Unterklasse zu garantieren

Dem Entwickler der Unterklasse steht es nach wie vor frei Methoden mit anderen Signaturen zu verwenden.

Finale Klassen

Finale Klassen verbieten die Spezialisierung einer Klasse durch eine Unterklasse.

Das Schlüsselwort final kommt im folgenden Beispiel zum Einsatz:

final class chiefExecutiveOfficer extends Manager { /* Klassen Implementierung */} 

Beispiel

  • Systemklasse String: Die Klasse String ist final, da Zeichenketten hochoptimiert und nicht veränderbar sind. Das Javalaufzeitsystem verwendet spezielle Optimierungen wie Stringpools und es nutzt die Unveränderbarkeit von Zeichenketten um dem Anwender Zugriff auf spezielle Zeichenketten des Laufzeitsystems zu geben. Die Möglichkeit diese Klasse zu spezialisieren wurde unterbunden um Sicherheit und Leistung zu garantieren.

Abstrakte Klassen und Methoden

Abstrakte Klassen

Abstrakte Klassen und Java-Schnittstellen (Stoff des zweiten Semester) dienen in Java zur Modularisierung und damit oft zur Arbeitsteilung zwischen Entwicklern. Abstrakte Klassen erlauben es dem Entwickler eine Oberklasse zu implementieren die nicht selbst instanziiert werden darf. Dieses Konzept erlaubt es Klassen als Vorlagen zu implementieren und gleichzeitig zu erzwingen, dass die Entwickler der Unterklasse fehlende oder eigene Methoden und Attribute zur Verfügung stellen. Hiermit lassen sich allgemeine Konzepte implementieren die zu allgemein sind um Instanziierungen zu erlauben, Dies erfolgt mit dem Schlüsselwort abstract bei der Spezifikation der Klasse.

Die Klasse Person im vorhergehenden Beispiel ist eine Klasse von der man nicht erlauben möchte, dass sie instanziiert wird. Dies geschieht zum Beispiel so:

public abstract class Person {
    private String name;
    private String firstName;
    public int age;

    public Person(String ln, String fn) {
        name = ln; firstName = fn; }

    public Person() { this("Doe","John");}

    public void setName(String ln, String fn) {
        name = ln; firstName = fn; }
    public String fullName() {return (name + " " + firstName);}
}

Versucht man nun eine Person zu instanziieren:

Person p1 = new Person("persona","nongrata");

...führt dies beim Übersetzen zu folgendem Fehler:

javac Main.java 
Main.java:8: Person is abstract; cannot be instantiated
        Person p1 = new Person("persona","nongrata");
                    ^
1 error

Abstrakte Klassen haben die typischen Eigenschaften von normalen Javaklassen. Sie haben Attribute und Methoden die vererbt werden können.

Wichtige Eigenschaften abstrakter Klassen
  • Sie können nicht instanziiert werden.
  • Sie können nicht final sein.

Beispiel

Die Systemklasse Number: Die Klasse Number enthält gemeinsame Methoden für zahlenartige Typen. Man kann sie jedoch nicht direkt instanziieren. Die daraus abgeleiteten Klassen Byte, Float etc. sind final. Sie sind hochoptimiert und sollen aus Performance- und Sicherheitsgründen nicht spezialisiert werden.

Abstrake und finale Klassen

Abstrakte Methoden

Normale Methoden und Attribute werden an die Unterklasse vererbt und können auch bei Bedarf überschrieben werden.

Wird jedoch eine Methode mit dem Schlüsselwort abstract gekennzeichnet so muss sie von einer nicht abstrakten Unterklasse implementiert und überschrieben werden.

Beispiel

Die Klasse Person kann so erzwingen, dass eine printAll() Methode für alle Unterklassen implementiert muss, ohne sie selbst zu implementieren.

public abstract class Person {
    private String name;
    private String firstName;
    public int age;

    public Person(String ln, String fn) {
        name = ln; firstName = fn; }

    public Person() { this("Doe","John");}

    public void setName(String ln, String fn) {
        name = ln; firstName = fn; }
    public String fullName() {return (name + " " + firstName);}
    public abstract String printAll();
}

Regel: Eine Unterklasse einer abstrakten Klasse muss:

  • alle abstrakten Methoden implementieren um nicht "abstrakt" zu sein

oder

  • selbst eine abstrakte Klasse sein, falls sie nicht alle oder keine der abstrakten Methoden implementiert.

 

8.5 Entwurfsüberlegungen zur Vererbung

Die Möglichkeiten der Vererbung sind sehr ausdrucksstark, haben viele Vorteile, bergen aber auch Risiken. Hierbei muss man mindestens drei verschieden Entwicklerrollen beachten:

  • Implementierer der Oberklasse
    • entwirft ein semantisches Konzept für eine Klassenhierarchie
    • implementiert allgemeingültige Methoden und Attribute
    • gibt Rahmenwerk durch Zugriffsmethoden, Zugriffskontrolle, explizite Möglichkeit zum Vererben und Überschreiben vor
  • Implementierer der Unterklasse
    • nutzt Infrastruktur
    • muss sich an Syntax und Zugriffskontrollen halten
    • macht Annahmen über Semantik der Oberklasse
    • verfeinert und verändert Funktionalität
  • Benutzer der Klassenhierarchie
    • macht Annahmen zur gesamten Klassenhierarchie
      • nutzt Syntax
      • erschliesst Semantik aus Klassendeklaration, Dokumentation und experimenteller Benutzung
    • muss sich an Syntax und Zugriffskontrollen aller Klassen halten

Semantische Integrität von Unterklassen

Unterklassen müssen Spezialisierungen der Oberklassen sein. Sie müssen sich in allen semantischen Belangen wie die Oberklasse verhalten.

Vorteile

  • Existierende Methoden können
    • benutzt werden
    • komplett überschrieben werden
    • überschrieben und die Methode der Oberklasse rekursiv genutzt werden
  • Existierende Funktionalität von Oberklassen können sehr einfach genutzt werden

Risiken

  • Überschriebene Methoden haben eine Semantik die nicht vom Entwickler der Oberklasse oder dem Benutzer der Unterklasse verstanden werden
    • Das heißt, dass die Methoden eventuell unter falschen Annahmen benutzt werden und entsprechenden Schaden anrichten
  • Unterklassen erben unnötige Infrastruktur (Methoden) die den Pflegeaufwand in die Höhe treiben oder externe Benutzer verwirren

Ändern von Oberklassen

Änderungen an Oberklassen sind heikel, da sie die Implementierungen der Unterklassen invalidieren können:

  • Entfernen von öffentlichen Methoden oder Attributen wird die Unterklassen invalidieren. Überschriebene Methoden werden plötzlich zu regulären. Der Polymorphismus gilt nicht für identische Methoden zweier Unterklassen wenn die Methode nicht in der Oberklasse als abstrakte Methode implementiert ist
  • Nicht abstrakte Methoden abstrakt machen invalidiert die Unterklassen. Alle Unterklassen müssen die abstrakte Methode implementieren
  • Hinzufügen von Methoden oder Attributen birgt das Risiko, dass sie Aufgrund zufälliger Namensgleichheit in den Unterklassen zu überschriebenen Methoden führen

Wichtig: Die Modellierung einer Klassenhierarchie muss sehr sorgfältig geschehen. Das möglichst exakte Abbilden realer Hierarchien ist für eine langlebige und wartbare Implementierung sehr wichtig.

Alternativen zur Vererbung

Vererbung birgt auch Risiken da eine Klassenhierarchie relativ starr ist und individuelle Änderungen weitreichende Auswirkungen haben können.

Vererbung sollte immer dann eingesetzt werden wenn man eine "ist-ein" Beziehung modellieren möchte.

In vielen Fällen liegt aber auch eine "hat-ein" Beziehung vor, eine Assoziation. Hier sollte man erwägen zwei Objekte zu erzeugen und sie über eine Referenz zu verwalten.

Im vorhergehen Beispiel ist es eine Überlegung wert, das Budget der Klasse Manager als Referenz von der Klasse Employee auf eine Klasse Budget zu implementieren. Hierbei sind die folgenden Dinge abzuwägen:

  • Nachteile der Assoziation
    • Höherer initialer Implementierungsaufwand zur Datenkaspelung
      • Referenz + Zugriffsmethoden + Belegung im Konstruktor mit Objekterzeugung des Budgets
    • Jeder zusätzliche Aspekt der Klasse muss als Assoziation implementiert werden
    • Zusätzliche Aspekte der Klasse sind nicht in der Klassenhierarchie sichtbar
    • Zusätzlicher Speicherverbrauch durch zusätzliche Objekte
  • Vorteile der Assoziation
    • Zukünftige Änderungen der assoziierten Klasse haben weniger Einfluss auf die gesamte Klassenhierarchie
    • Der Implementierungs-Overhead einer eigenen Klasse entfällt (Konstruktoren, überschriebene Methoden etc.)

 

8.6 Übungen

Duke als Boxer

8.6.1 Aufruf von überschriebenen Methoden

Implementieren sie die Klasse TopClass mit den folgenden Eigenschaften

  • statische, geschütztes (protected) Attribut zaehlerTop zum Zählen der erzeugten Instanzen der Klasse TopClass
  • parameterlosen Konstruktor der den Instanzenzähler inkrementiert.
  • eine statische geschützte Methode getZaehler() die das Attribut zaehlerTop ausgibt.

Implementieren Sie die Klasse LowClass welche aus TopClass abgeleitet mit den folgenden Eigenschaften 

  • statische, geschütztes (protected) Attribut zaehler zum Zählen der erzeugten Instanzen der Klasse LowClass
  • parameterlosen Konstruktor der den Instanzenzähler inkrementiert
  • eine statische geschützte Methode getZaehler() die das Attribut zaehler ausgibt.
  • eine Methode test() die
    • die lokale Methode getZaehler() aufruft und das Ergebnis auf der Konsole ausgibt
    • die Methode getZaehler() der Oberklasse aufruft und das Ergebnis auf der Konsole ausgibt

Implementieren Sie ein Hauptprogramm, dass folgende Befehle ausführt

  • Anlegen eines Feldes der Klasse TopClass mit n Elementen
  • Anlegen eines Feldes der Klasse LowClass mit n Elementen
  • Erzeugen von jeweils n Elementen der Klasse TopClass und Einfügen in das zugehörige Feld
  • Aufruf der getZaehler() Methode der Klasse TopClass und Ausgabe des Wertes auf der Konsole
  • Erzeugen von jeweils n Elementen der Klasse LowClass und Einfügen in das zugehörige Feld
  • Aufruf der getZaehler() Methode der Klasse LowClass und Ausgabe des Wertes auf der Konsole
  • Aufruf der getZaehler() Methode der Klasse TopClass und Ausgabe des Wertes auf der Konsole
UML Diagramm TopClass, LowClass

Zu beobachten:

  • Wieviel Instanzen welcher Klasse (TopClass, LowClass) wurden erzeugt?
  • Wieviel Instanzen wurden insgesamt erzeugt?
  • Was geschieht wenn das Attribut zaehler der Klasse LowClass gelöscht wird?

Klasse MainTop zum Testen

package block9;
/**
*
* @author sschneid
*/
public class MainTop {
private static int size = 3;
public static void main(String[] args) {
TopClass[] feldT = new TopClass[size];
LowClass[] feldL = new LowClass[size]; for ( int i=0; i<size; i++) feldT[i]= new TopClass(); for ( int i=0; i<size; i++) {

feldL[i]= new LowClass();
feldL[i].test();
}
System.out.println(TopClass.getZaehler() + " Instanzen TopClass generiert, "+
LowClass.getZaehler() + " Instanzen LowClass generiert");
}
}

8.6.2 Vererbung und Assoziation

Implementieren Sie eine Klasse Point mit den folgenden Eigenschaften:

  • private Fliesskommaattribute für x und y Koordinate
  • öffentlicher Konstruktor mit x und x als Parameter
  • öffentliche setXY(x,y) Methode
  • Auslesemethoden getX(), getY()
  • print() Methode die eine Zeichenkette mit allen Werten liefert
  • überschreiben die die toString() Methode von Object so das sie alle Werte für Point ausgibt.

Implementieren Sie eine Klasse CircleIsPoint die die Eigenschaften der Klasse Point hat und zusätzlich einen Radius verwalten kann mit folgenden Eigenschaften:

  • erbt von Point
  • das des Zentrum des Kreises sind die (x,y) Koordinaten von Point 
  • privates Fliesskomma Attribut für den Radius radius
  • getRadius() und setRadius(r) Methoden zum Pflegen des Attributs
  • öffentlicher Konstruktor für (x,y) Zentrum und Radius
  • print() Methode die eine Zeichenkette mit allen Werten liefert
  • überschreiben die die toString() Methode von Object so das sie alle Werte für Point ausgibt.

Implementieren Sie eine Klasse CircleHasPoint die die Eigenschaften der Klasse Point hat und zusätzlich einen Radius verwalten kann mit folgenden Eigenschaften:

  • erbt nicht von Point
  • benutzt privates Attribut vom Referenztyp auf Point
  • das des Zentrum des Kreises sind die (x,y) Koordinaten von Point 
  • alle öffentlichen Methoden sind identisch zu CircleIsPoint!

 Was unterscheidet beide Implementierungen? Welche ist die bessere Implementierung?

Point und Circle, Delegation versus Assoziation

Klasse Main

Die drei obigen Klassen sollten mit der folgenden main() Methode funktionieren:

package block9;
public class Main {

    public static void main(String[] args) {
        Point p1 = new Point (2.2, 3.3);
        Point p2 = new Point (2.22, 3.33);
        CircleIsPoint cip1 = new CircleIsPoint(4.4,5.5,6.6);

        p1.print();
        cip1.print();

        CircleHasPoint chp1 = new CircleHasPoint(44.4,55.5,66.6);
        chp1.print();
    }
}

8.6.3 Flughafenbeispiel

UML Diagramm Flughafenbeispiel mit Vererbung

Klasse Flughafen

package airline.block8;

 

/**
*
* @author Otto Lilienthal
*/
public class Flughafen {
String name;
Passagierflugzeug[] gate;
Flugzeug[] parkposition;
double treibstoffLager;

/**
* Anlegen eines Flughafens
* @param anzahlGates Anzahl der Gates eines Flugzeugs
* @param anzahlParkposition Anzahl der Parkpositionen
*/
public Flughafen(int anzahlGates, int anzahlParkposition) {
gate = new Passagierflugzeug[anzahlGates];
parkposition = new Flugzeug[anzahlParkposition];
treibstoffLager = 10000000D;
}

/**
* Betanken eines Flugzeugs aus dem Treibstofflage
* Flugzeug wird nicht betankt wenn das Volumen im Tank nicht ausreicht!
* @param kg zu tankendes Volumen
* @param f Flugzeug welches betankt werden soll
*/
public void tanken(double liter, Flugzeug f) {
if ((liter <= treibstoffLager) && (liter >=0)) {
treibstoffLager -= liter;
f.tanken(liter);
}
else
System.out.println("Nicht genug Treibstoff im Depot zum Tanken");
}

/**
* Drucke den Zustand des Flughafens auf die Konsole
*/
public void drucken() {
System.out.println("*** Unser Flughafen ***");
System.out.println("Flughafen " + name);
for (int i =0;i<gate.length;i++)
System.out.println("Am Gate " + i + ": " + gate[i]);
for (int i =0;i<parkposition.length;i++)
System.out.println("Auf Parkposition " + i + ": " + parkposition[i]);
System.out.println("Treibstoff: " + treibstoffLager);
System.out.println("***********************");
}

/**
* Baue einen Flughafen und teste alle Klassen
* @param args nicht benoetigt
*/
public static void main(String[] args) {

Flughafen pad = new Flughafen(6, 10);
pad.name="Paderborn";
pad.treibstoffLager = 1000000;

pad.drucken();

// Boeing 747, https://de.wikipedia.org/wiki/Boeing_747#747-400
Passagierflugzeug lh1 = new Passagierflugzeug(40000, 400000);
lh1.kennzeichen ="D-ABTL";
lh1.einsteigen(3);
System.out.println("Unsere Gesellschaft" + Flugzeug.meineLG());

double aktuellesGewicht = lh1.gewicht();
System.out.println("aktuelle Gewicht von lh1: " + aktuellesGewicht);

pad.gate[1] = lh1;

// Airbus A380 https://de.wikipedia.org/wiki/Airbus_A380#A380-800
Passagierflugzeug lh2 = new Passagierflugzeug(40000, 400000, "D-AIMA");

// 11 +1 +4 Passagiere steigen ein
lh2.einsteigen(11);
lh2.einsteigen();
lh2.einsteigen(4);
// 5 Passagiere wollen wieder austteigen
lh2.aussteigen(5);

// lh2 wird an Gate 2 angedockt
pad.gate[2] = lh2;

Frachtflugzeug meinFrachter = new Frachtflugzeug(10000D,300000D,"N-1997A");
meinFrachter.laden(1000D);
meinFrachter.entladen(500D);
// Betanke mein Flugzug
pad.tanken(200D,meinFrachter);
pad.parkposition[2] = meinFrachter;
pad.parkposition[3] = new Passagierflugzeug(100000D,500000D,"D-AAAA");

// Hier steht ein Frachtflugzeug. Das ist bekannt!!
// Durch den "Cast" kann man man die Methode laden() für f aufrufen
Frachtflugzeug f = (Frachtflugzeug) pad.parkposition[2];
f.laden(500D);
System.out.println(pad.parkposition[2].kennzeichen + " "
+ pad.parkposition[2].gewicht());


// Hier wird die überladene Methode gewicht() aufgerufen
// Das Programm ruft hier zwei verschiedene Methoden auf
// welche Methode aufgerufen wird wird erst zur Laufzeit entschieden!
System.out.println("Gewicht Parkposition 2 " + pad.parkposition[2].gewicht());
System.out.println("Gewicht Parkposition 3 " + pad.parkposition[3].gewicht());

System.out.println("Flughafen " + pad.name);
System.out.println("Am Gate 1: " + pad.gate[1].kennzeichen +
" Passagiere: " + pad.gate[1].getPassagiere() +
" akt. Gewicht: " + pad.gate[1].gewicht());
System.out.println("Am Gate 2: " + pad.gate[2].kennzeichen +
" Passagiere: " + pad.gate[2].getPassagiere() +
" akt. Gewicht: " + pad.gate[2].gewicht());
pad.drucken();

}
}

 

Klasse Flugzeug

package airline.block8;

/**
* Das ist ein Flugzeug des 8.ten Blocks
* @author Otto Lilienthal
*/
public class Flugzeug {

/**
* Kennzeichen des Flugzeugs
*/
String kennzeichen;
final double maximalesGewicht;
final double leerGewicht;
private double tankinhalt;

/**
* Initialiseren eines Flugzeugs mit Leer- und Maximalgewicht
* @param minGewicht Minimalgewicht in kg
* @param maxGewicht Maximalgewicht in kg
*/
public Flugzeug(double minGewicht, double maxGewicht) {
System.out.println("Ich baue jetzt ein Flugzeug");
if (minGewicht > 0) leerGewicht = minGewicht;
else leerGewicht = 0;
if ((maxGewicht > 0) && (maxGewicht>=leerGewicht))
maximalesGewicht = maxGewicht;
else maximalesGewicht = leerGewicht;
tankinhalt = 0;
}

/**
* Initialiseren eines Flugzeugs mit Leer- und Maximalgewicht, Kennzeichen
* @param minGewicht Minimalgewicht in kg
* @param maxGewicht Maximalgewicht in kg
* @param kennz Kennzeichen
*/
public Flugzeug(double minGewicht, double maxGewicht, String kennz) {
this(minGewicht,maxGewicht);
kennzeichen = kennz;
}

/**
* berechnet das aktuelle Gewicht des Flugzeugs
* @return aktuelle Gewicht
*/
public double gewicht() {
//double g = leerGewicht+passagiere*durchschnittsgewicht;
return leerGewicht+ tankinhalt;
}

/**
* betanken eines Flugzeugs. Vorsicht: Hier wird das Gewicht eingegeben
* @param t kg Treibstoff
*/
public void tanken(double t) {
if (t >= 0)
tankinhalt += t;
else
System.out.println("Versuch mit negativer Literanzahl: " +t);
}

/**
* Aktuelle Tankfüllung in kg
* Vorsicht: Es geht hier um das Gewicht des Treibsstoffs
* @return Gewicht des aktuellen Treibstoffs
*/
public double tankfuellung() {return tankinhalt;}

/**
* Die Fluggesellschaft der Flugzeuge. Dies ist eine static Variable
* D.h. alle Flugzeuge gehören zu einer Fliggesellschaft
* @return Name der Fluggesellschaft
*/
public static String meineLG() { return "Lufthansa";}

}

 

 

Klasse Frachtflugzeug


package airline.block8;

/**
*
* @author Otto Lilienthal
*/
public class Frachtflugzeug extends Flugzeug {

private double ladung;

/**
* Initialisieren eines Frachtflugzeugs mit Initial- und Maximalgewicht,
* Kennzeichen
* @param leer Leergewicht
* @param max Maximalgewicht
* @param kennz Kennzeichen
*/
public Frachtflugzeug(double leer, double max, String kennz) {
super(leer, max, kennz);
ladung = 0;
}

/**
* Laden von Fracht in kg
* @param l zu ladende Fracht in kg
*/
public void laden(double l) {
if (gewicht() + ladung <= maximalesGewicht) {
ladung += l;
} else {
System.out.println("Kann nicht " + l + " kg laden...");
}
}

/**
* Entladen von Fracht in kg
* @param l zu entladende Fracht in kg
*/
public void entladen(double l) {
if (ladung >= l) {
ladung -= l;
} else {
System.out.println("Kann nicht " + l + " kg entladen (Ist zuviel)");
}
}

/**
* Gewicht eines Frachtflugzeugs in kg
* @return Gewicht in kg
*/
@Override
public double gewicht() {
return super.gewicht() + ladung;
}

}

 

Klasse Passagierflugzeug 

package airline.block8;

/**
*
* @author stsch
*/
public class Passagierflugzeug extends Flugzeug {

final static double durchschnittsgewicht = 75;
private int passagiere;
int maxPassagiere = 100;

/**
* gibt die aktuelle Anzahl der Passagiere zurück
* @return Anzahl der Passagiere
*/
public int getPassagiere() {
return passagiere;
}

/**
* Initialisert ein Flugzeug mit Minimal- und Maximalgewicht
* @param minGewicht Minimalgewicht in kg
* @param maxGewicht Maximalgewicht in kg
*/
public Passagierflugzeug(double minGewicht, double maxGewicht) {
super(minGewicht,maxGewicht);
passagiere = 0;

}

/**
* Initialisiert ein Flugzeug mit Minimal- und Maximalgewicht und Kennzeichen
* @param minGewicht Minimalgewicht in kg
* @param maxGewicht Maximalgewicht in kg
* @param kennz Kennzeichen des Flugzeugs
*/
public Passagierflugzeug(double minGewicht, double maxGewicht, String kennz) {
super(minGewicht,maxGewicht,kennz);
passagiere = 0;
}

/**
* Einsteigen eines einzelnen Passagiers
*/
public void einsteigen() {
if (passagiere >= maxPassagiere) { // Zuviele Einsteiger
System.out.println("Fehler: Einsteigen verweigert ");
} else { // passt schon
passagiere++;
}
}

/**
* Einsteigen einer vorgegebenen Anzahl Passagiere.
* und sie funktioniert auch!
* Wenn zuviele einsteigen möchten, darf keiner einsteigen!
* @param einsteiger Anzahl der Passagiere die einsteigen.
*/
public void einsteigen(int einsteiger) {
if (passagiere+einsteiger > maxPassagiere) {
System.out.println("Fehler: Einsteigen verweigert ");
} else {
System.out.println("Ich lasse " + einsteiger + " Passagiere eingestiegen");
einsteigen(); // Einer steigt ein
if (einsteiger > 1)
einsteigen(einsteiger-1); //Der Rest steigt ein
}
}

/**
* Aussteigen einer Anzahl von Pasagieren
* @param aussteiger ANzahl der aussteigenden Passagiere
*/
public void aussteigen(final int aussteiger) {
if (passagiere >= aussteiger) {
passagiere -= aussteiger;
} else {
System.out.println("Fehler: Flugzeug ist leer... ");
}
} // Ende der Methode aussteigen

/**
* berechnet das aktuelle Gewicht des Flugzeugs
* @return aktuelles Gewicht
*/
@Override
public double gewicht() {
return super.gewicht()+passagiere*durchschnittsgewicht;
}

}


 

 

8.7 Lösungen

8.7.1 Vererbung und Überschreiben

Klasse TopClass

public class TopClass {
    protected static int zaehler;

    public TopClass() {zaehler++;}

    protected static int getZaehler() {return zaehler;}

}

Klasse LowClass

public class LowClass extends TopClass {
    protected static int zaehler;

    public LowClass() {zaehler++;}

    protected static int getZaehler() {return zaehler;}

    protected void test (){
        System.out.println( getZaehler() + " Instanzen der Klasse LowClass erzeugt");
        System.out.println( TopClass.getZaehler() + " Instanzen der Klasse TopClass erzeugt");
    }
}

Klasse Main

/**
*
* @author sschneid
*/
public class Main {
private static int size = 3;
public static void main(String[] args) {
TopClass[] feldT = new TopClass[size];
LowClass[] feldL = new LowClass[size];

for ( int i=0; i<size; i++) feldT[i]= new TopClass();

for ( int i=0; i<size; i++) {
feldL[i]= new LowClass();
feldL[i].test();
}
System.out.println(TopClass.getZaehler() + " Instanzen TopClass generiert, "+
LowClass.getZaehler() + " Instanzen LowClass generiert");
}
}

Ausgaben (mit Attribut zaehler in Klasse LowClass)

1 Instanzen der Klasse LowClass erzeugt
4 Instanzen der Klasse TopClass erzeugt
2 Instanzen der Klasse LowClass erzeugt
5 Instanzen der Klasse TopClass erzeugt
3 Instanzen der Klasse LowClass erzeugt
6 Instanzen der Klasse TopClass erzeugt
6 Instanzen TopClass generiert, 3 Instanzen LowClass generiert

Ausgaben (ohne Attribut zaehler in Klasse LowClass)

5 Instanzen der Klasse LowClass erzeugt
5 Instanzen der Klasse TopClass erzeugt
7 Instanzen der Klasse LowClass erzeugt
7 Instanzen der Klasse TopClass erzeugt
9 Instanzen der Klasse LowClass erzeugt
9 Instanzen der Klasse TopClass erzeugt
9 Instanzen TopClass generiert, 9 Instanzen LowClass generiert

8.7.2 Vererbung und Assoziation

Klasse Point

package block9;
public class Point {
private double x;
private double y;
public Point(double xx, double yy) {
x = xx;
y = yy;
}
public void setXY(double xx, double yy) {
x = xx;
y = yy;
}
public double getX() { return x;}
public double getY() { return y;}
public void print() {System.out.println(toString());}
public String toString() {return ("x: " + x + " y: " + y);}
}

Klasse CircleIsPoint

package block9;
public class CircleIsPoint extends Point{
private double radius;
public CircleIsPoint(double xx, double yy, double r) {
super(xx,yy);
radius=r;
}
public double getRadius() {return radius;}
public void setRadius(double r) {radius=r;}
public String toString() {return (super.toString() + " , r: " + radius);}
}

Klasse CircleHasPoint

package block9;
public class CircleHasPoint {
private double radius;
private Point p;
public CircleHasPoint(double xx, double yy, double r) {
p = new Point(xx,yy);
radius=r;
}
public double getRadius() {return radius;}
public void setRadius(double r) {radius=r;}
public void setXY(double xx, double yy) { p.setXY(xx,yy);}
public double getX() { return p.getX();}
public double getY() { return p.getY();}
public void print() {System.out.println(toString());}
public String toString() {return ("x: " + p.getX() + " y: " +
p.getY() + " r: "+ radius);}
}

Klasse Main

package block9;
public class Main {

    public static void main(String[] args) {
        Point p1 = new Point (2.2, 3.3);
        Point p2 = new Point (2.22, 3.33);
        CircleIsPoint cip1 = new CircleIsPoint(4.4,5.5,6.6);

        p1.print();
        cip1.print();

        CircleHasPoint chp1 = new CircleHasPoint(44.4,55.5,66.6);
        chp1.print();
    }
}

Ausgabe:

x: 2.2 y: 3.3
x: 4.4 y: 5.5 radius: 6.6
x: 44.4 y: 55.5 radius: 66.6

8.7.3 Beispiel der Vorlesung

Schritt 1: Klonen der Klasse Ware zu Buch und Anpassen

Ware, Lager, Buch, ohne Vererbung

Klasse Buch (nicht von Ware abgeleitet!)

package block9;
/**
* Dies ist die Dokumentation der Klasse Ware. Ware dient zum Verwalten von Gütern
* mit Preisen und Namen in einem Lager.
* @author Stefan Schneider
* @version 1.2
* @see Lager
*/
public class Buch {
/*
* 1. Erzeugen Sie eine Klasse Buch in dem Sie die Klasse
* Ware mit Ihrem Inhalt kopieren
* 1.1 Anpassen Name
* 1.2 Anpassen Konstruktoren
* 1.3 Anpassen equals Methode
* 1.4 Anlegen einer ISBN Nummer
* 1.5 Anlegen einer hardcodierten halben Mehrwertsteuer
* 1.6 Referenz auf eine private, optionale (ältere) Auflage
* Zugriffsmethoden anlegen
* 1.7 Anpassen der Druckenmethode
* 4. Ableiten der Klasse aus der Klasse aus der Klasse Ware
* 4.1 extends Schlüsselwort benutzen
* 4.2 Löschen von allem redundanten Code
* 6. Erzeugen er Klasse MusikCD durch Kopieren der Klasse Ware
* 6.1 Mehrwertsteuer auf vollen Mehrwertsteuer hart kodieren
* 7. Die Klasse Ware soll nicht mehr instanziiert werden. Sie soll
* abstrakt werden
*/
/**
* Der aktuelle Mehrwertsteuersatz 2010.
* Er liegt zur Zeit bei {@value}.
*
* @since 1.0
*/
public static final double mws = 0.19;
private double nettoPreis; //Deklaration
private boolean halbeMws;
private String name;
public Buch empfehlung;
private String isbn;
private Buch alteAuflage;

/**
* Referenz aus alte Auflage
* @return
*/
public Buch getAlteAuflage() { return alteAuflage; }
/**
* zuweisen einer alten Auflage
* @param alteAuflage
*/
public void setAlteAuflage(Buch alteAuflage) {
this.alteAuflage = alteAuflage;
}
/**
* Auslesen der ISBN-Nummer (eine Zeichenkette)
* @return
*/
public String getISBN() { return isbn; }
/**
* Konstruktor fuer die Klasse Ware
* @param n der Name der Ware
* @param np der Nettorpreis
* @param hmws halber Mehrwertsteuersatz für Ware gueltig
*/

public Buch(String n, double np, String myISBN) {
name = n;
nettoPreis = np;
halbeMws = true; //Bei Büchern immer der Fall
isbn=myISBN;
// zusätzliche Initialiserungen....
}
/**
* tiefes Vergleichen. Berücksichtigt beim Namen nicht
* die Groß/kleinschreinung. Berücksichtigt rekursiv die
* Empfehlungen
* @param dieAndereWare
* @return wahr wenn beide Waren dem Vergleich entsprechen.
*/
public boolean equals(Buch dieAndereWare) {
boolean result;
result = getName().equalsIgnoreCase(dieAndereWare.getName())
&& (getNettoPreis() == dieAndereWare.getNettoPreis())
&& (getHalbeMws() == dieAndereWare.getHalbeMws())
&& (((getEmpfehlung() == null) && (dieAndereWare.getEmpfehlung() == null))
|| ((getEmpfehlung() != null)
&& (dieAndereWare.getEmpfehlung() != null)
&& (getEmpfehlung().equals(dieAndereWare.getEmpfehlung()))
)
&& (isbn.equals(dieAndereWare.isbn))
&&
(((alteAuflage == null) && (dieAndereWare.alteAuflage == null))
|| ((alteAuflage != null)
&& (dieAndereWare.alteAuflage != null)
&& (alteAuflage.equals(dieAndereWare.alteAuflage))
)
)

);
return result;
}
/**
* Liefert den Namen einer Ware zurueck.
* @return Name der Ware
*/
public String getName() {
return name;
}
/**
* Setzen eines neuen Nettopreis
* @param npr der neue Nettopreis
*/
public void setNettoPreis(double npr) {
nettoPreis = npr;
}

/**
* liefere wahr zurück wenn Mwssatz reduziert ist
* @return
*/
public boolean getHalbeMws() {return halbeMws;}

public Buch getEmpfehlung() { return empfehlung; }

public void setEmpfehlung(Buch empfehlung) {
this.empfehlung = empfehlung;
}
/**
* Ausdrucken aller Werte auf der Konsole
*/
public void drucken() { drucken(0); }
/**
* Ausdrucken aller Werte auf der Konsole mit vorgebener Einrueckung
* für Empfehlungen
* @param einruecken eingerueckte Stellen für Empfehlungen
*/
private void drucken(int einruecken) {
String leerStellen = "";
for (int i = 0; i < einruecken; i++) {
leerStellen = leerStellen + " ";
}
System.out.print(leerStellen + "Name: " + getName());
System.out.print(" ;Netto: " + getNettoPreis());
System.out.print(" ;Brutto: " + getBruttoPreis());
if (halbeMws)
System.out.println(" (Halbe Mws)");
else
System.out.println(" (Volle Mws)");
if (empfehlung != null) {
System.out.println(leerStellen + "Empfehlung:");
empfehlung.drucken(einruecken + 2);
}
System.out.println(leerStellen + "ISBN:" +isbn);
if (alteAuflage != null) {
System.out.println(leerStellen + "Alte Auflage:");
alteAuflage.drucken(einruecken + 2);
}

}
/**
* Ausgabe des Nettopreis
* @return der Nettopreis
*/
public double getNettoPreis() {
return nettoPreis;
}
/**
* Ausgabe des Bruttopreis
* @return der Bruttopreis
*/
public double getBruttoPreis() {
double bp; //temporaere Variable; keine Klassenvariable
if (halbeMws) {
bp = Math.round(nettoPreis * (mws / 2 + 1) * 100) / 100;
} else {
bp = Math.round(nettoPreis * (mws + 1) * 100) / 100;
}
return bp;
}
/**
* Erzeugt verkette Liste einer Ware
* @param Anzahl der zu verkettenden Waren
*/
public void generiereEmpfehlungen(int empf) {
Buch ware1 = this;
Buch ware2;
for (int i = 1; i <= empf; i++) {
ware2 =
new Buch(ware1.getName() + " besser" + i, 31.12, isbn+"-1");
ware1.setEmpfehlung(ware2);
ware1 = ware2;
}
}
}

 Klasse Main

package block9;
/**
* Eine Hilfsklasse zur Implementierung eines Hauptprogramms
* @author sschneid
* @version 1.1
*/
public class Main {
/**
* Die Methode wareTesten testet die Implementierung der
* von Waren mit tiefem Kopieren, tiefem Vergleichen.
* Sie nutzt nicht die Vererbung aus.
*/
public static void warenTesten() {
Ware ware1, ware2;
Lager dasLager;
// Testen der Klasse Ware
ware1 = new Ware("Zeitung", 12.12, true);
ware1.drucken();
double p = ware1.getNettoPreis();
// Generieren von Empfehlungen
ware2 = new Ware("Potter Band 1", 31.12, false);
ware2.generiereEmpfehlungen(7);
// Abfrage des Lagers
dasLager = Lager.getLager();
dasLager.einlagern(ware1, 0);
dasLager.einlagern(ware2, 1);
// Prüfen der Lagerbestände mit tiefem Vergleichen
Ware testWare = dasLager.holen(0);
if (testWare == ware2)
System.out.println("testware und ware2 sind identisch (gut)");
if (testWare.equals(ware2))
System.out.println("testware und ware2 sind inhaltsgleich (gut)");
// vollständiges Befüllen des Lager
for (int i = 0; i < 1000; i++) {
ware2 = new Ware("Band" + i + "B", 12.12, true);
ware2.generiereEmpfehlungen(7);
dasLager.einlagern(ware2);
}
System.out.println("Lager vollständig gefüllt mit "
+ dasLager.lagerGroesse() + " Waren.");
for (int i = 0; i < 1000; i++) {
ware2 = new Ware("Volume" + i + "B", 12.12, true);
ware2.generiereEmpfehlungen(7);
dasLager.einlagern(ware2,i);
}
System.out.println("Lager erneut vollständig gefüllt mit "
+ dasLager.lagerGroesse() + " Waren.");
}
/**
* Diese Methode dient zum Testen der Klasse Buch.
* Sie nutzt die Veererbung in Java aus.
*/
public static void buecherTesten() {
/*
* 2. Testen: Anlegen einer Testmethode für Bücher
* Erzeugen von Büchern
* Anlegen einer Referenz auf eine alte Auflage
* Drucken zweier Bücher
* 3. Frage: Wie kann man Bücher in das Lager einfügen?
* 5. Einfügen der Bücher in das Lager
* 8. Anpassen der Hauptroutine
* 8.1 Alle Instanzen vom Typ Ware sollen MusikCDs werden da die Klasse
* Ware jetzt abstrakt ist.
*/
Lager dasLager;
dasLager = Lager.getLager();
Buch buch1 = new Buch("Das Grauen",22.22,"9876543");
Buch buch2 = new Buch("Das Kapital",33.33,"9876543");
buch1.setAlteAuflage(buch2);
buch1.drucken();

//dasLager.einlagern(buch1);
//dasLager.einlagern(buch2);
dasLager.drucken();
}
/**
* Das Hauptprogramm
*
* @param args
*/
public static void main(String[] args) {
//warenTesten();
buecherTesten();
}
}

 Schritt 2: Die Klasse Buch wird aus der Klasse Ware abgeleitet

Klasse Buch ist von der Klasse Ware abgeleitet

Klasse Buch

package block9;
/**
* Dies ist die Dokumentation der Klasse Ware. Ware dient zum Verwalten von Gütern
* mit Preisen und Namen in einem Lager.
* @author Stefan Schneider
* @version 1.2
* @see Lager
*/
public class Buch extends Ware {
/*
* 1. Erzeugen Sie eine Klasse Buch in dem Sie die Klasse
* Ware mit Ihrem Inhalt kopieren
* 1.1 Anpassen Name
* 1.2 Anpassen Konstruktoren
* 1.3 Anpassen equals Methode
* 1.4 Anlegen einer ISBN Nummer
* 1.5 Anlegen einer hardcodierten halben Mehrwertsteuer
* 1.6 Referenz auf eine private, optionale (ältere) Auflage
* Zugriffsmethoden anlegen
* 1.7 Anpassen der Druckenmethode
* 4. Ableiten der Klasse aus der Klasse aus der Klasse Ware
* 4.1 extends Schlüsselwort benutzen
* 4.2 Löschen von allem redundanten Code
* 6. Erzeugen er Klasse MusikCD durch Kopieren der Klasse Ware
* 6.1 Mehrwertsteuer auf vollen Mehrwertsteuer hart kodieren
* 7. Die Klasse Ware soll nicht mehr instanziiert werden. Sie soll
* abstrakt werden
*/ /** * Der aktuelle Mehrwertsteuersatz 2010. * Er liegt zur Zeit bei {@value}. * * @since 1.0 */

private String isbn;
private Buch alteAuflage;
/**
* Referenz aus alte Auflage
* @return
*/
public Buch getAlteAuflage() { return alteAuflage; }
/**
* zuweisen einer alten Auflage
* @param alteAuflage
*/
public void setAlteAuflage(Buch alteAuflage) {
this.alteAuflage = alteAuflage;
}
/**
* Auslesen der ISBN-Nummer (eine Zeichenkette)
* @return
*/
public String getISBN() { return isbn; }
/**
* Konstruktor fuer die Klasse Ware
* @param n der Name der Ware
* @param np der Nettorpreis
* @param hmws halber Mehrwertsteuersatz für Ware gueltig
*/
public Buch(String n, double np, String myISBN) {
super(n,np,true);
isbn=myISBN;
// zusätzliche Initialiserungen....
}
/**
* tiefes Vergleichen. Berücksichtigt beim Namen nicht
* die Groß/kleinschreinung. Berücksichtigt rekursiv die
* Empfehlungen
* @param dieAndereWare
* @return wahr wenn beide Waren dem Vergleich entsprechen.
*/
public boolean equals(Buch dieAndereWare) {
boolean result;
result = super.equals((Ware)dieAndereWare)
&& (isbn.equals(dieAndereWare.isbn))
&& (((alteAuflage == null) && (dieAndereWare.alteAuflage == null))
|| ((alteAuflage != null)
&& (dieAndereWare.alteAuflage != null)
&& (alteAuflage.equals(dieAndereWare.alteAuflage))
)
);
return result;
}
/**
* Ausdrucken aller Werte auf der Konsole mit vorgebener Einrueckung
* für Empfehlungen
* @param einruecken eingerueckte Stellen für Empfehlungen
*/
protected void drucken(int einruecken) {
super.drucken(einruecken);
String leerStellen = "";
for (int i = 0; i < einruecken; i++) {
leerStellen = leerStellen + " ";
}
System.out.println(leerStellen + "ISBN:" +isbn);
if (alteAuflage != null) {
System.out.println(leerStellen + "Alte Auflage:");
alteAuflage.drucken(einruecken + 2);
}
}
}

Klasse Main

Die Instanzen der Klasse Buch können jetzt in das Lager eingefügt werden

package block9;
/**
* Eine Hilfsklasse zur Implementierung eines Hauptprogramms
* @author sschneid
*/
public class Main { /** * Die Methode wareTesten testet die Implementierung der * von Waren mit tiefem Kopieren, tiefem Vergleichen. * Sie nutzt nicht die Vererbung aus. */

public static void warenTesten() {
Ware ware1, ware2;
Lager dasLager;
// Testen der Klasse Ware
ware1 = new Ware("Zeitung", 12.12, true);
ware1.drucken();
double p = ware1.getNettoPreis();
// Generieren von Empfehlungen
ware2 = new Ware("Potter Band 1", 31.12, false);
ware2.generiereEmpfehlungen(7);
// Abfrage des Lagers
dasLager = Lager.getLager();
dasLager.einlagern(ware1, 0);
dasLager.einlagern(ware2, 1);
// Prüfen der Lagerbestände mit tiefem Vergleichen
Ware testWare = dasLager.holen(0);
if (testWare == ware2)
System.out.println("testware und ware2 sind identisch (gut)");
if (testWare.equals(ware2))
System.out.println("testware und ware2 sind inhaltsgleich (gut)");
// vollständiges Befüllen des Lager
for (int i = 0; i < 1000; i++) {
ware2 = new Ware("Band" + i + "B", 12.12, true);
ware2.generiereEmpfehlungen(7);
dasLager.einlagern(ware2);
}
System.out.println("Lager vollständig gefüllt mit "
+ dasLager.lagerGroesse() + " Waren.");
for (int i = 0; i < 1000; i++) {
ware2 = new Ware("Volume" + i + "B", 12.12, true);
ware2.generiereEmpfehlungen(7);
dasLager.einlagern(ware2,i);
}
System.out.println("Lager erneut vollständig gefüllt mit "
+ dasLager.lagerGroesse() + " Waren.");
}
/**
* Diese Methode dient zum Testen der Klasse Buch.
* Sie nutzt die Veererbung in Java aus.
*/
public static void buecherTesten() {
/*
* 2. Testen: Anlegen einer Testmethode für Bücher
* Erzeugen von Büchern
* Anlegen einer Referenz auf eine alte Auflage
* Drucken zweier Bücher
* 3. Frage: Wie kann man Bücher in das Lager einfügen?
* 5. Einfügen der Bücher in das Lager
* 8. Anpassen der Hauptroutine
* 8.1 Alle Instanzen vom Typ Ware sollen MusikCDs werden da die Klasse
* Ware jetzt abstrakt ist.
*/
Lager dasLager;
dasLager = Lager.getLager();
Buch buch1 = new Buch("Das Grauen",22.22,"9876543");
Buch buch2 = new Buch("Das Kapital",33.33,"9876543");
buch1.setAlteAuflage(buch2);
buch1.drucken();
dasLager.einlagern(buch1);
dasLager.einlagern(buch2);
dasLager.drucken();

}

/**
* Das Hauptprogramm
*
* @param args
*/
public static void main(String[] args) {

//warenTesten();
buecherTesten();
}
}

Stufe 3: Klasse MusikCD

Klasse MusikCD

package block9;
/**
* Dies ist die Dokumentation der Klasse Ware. Ware dient zum Verwalten von Gütern
* mit Preisen und Namen in einem Lager.
* @author Stefan Schneider
* @version 1.3
* @see Lager
*/
public class MusikCD extends Ware {
/*
* 6. Erzeugen er Klasse MusikCD durch Kopieren der Klasse Ware
* 6.1 Mehrwertsteuer auf vollen Mehrwertsteuer hart kodieren
* 7. Die Klasse Ware soll nicht mehr instanziiert werden. Sie soll
* abstrakt werden
*/
/**
* Konstruktor fuer die Klasse Ware
* @param n der Name der Ware
* @param np der Nettorpreis
* @param hmws halber Mehrwertsteuersatz für Ware gueltig
*/
public MusikCD(String n, double np) {
super(n,np,false);
}
/**
* tiefes Vergleichen. Berücksichtigt beim Namen nicht
* die Groß/kleinschreinung. Berücksichtigt rekursiv die
* Empfehlungen
* @param dieAndereWare
* @return wahr wenn beide Waren dem Vergleich entsprechen.
*/
public boolean equals(MusikCD dieAndereWare) {
return super.equals((Ware)dieAndereWare);
}
}

 Klasse Main

package block9.stufe3;
/**
* Eine Hilfsklasse zur Implementierung eines Hauptprogramms
* @author sschneid
* @version 1.3
*/
public class Main {
/**
* Die Methode wareTesten testet die Implementierung der
* von Waren mit tiefem Kopieren, tiefem Vergleichen.
* Sie nutzt nicht die Vererbung aus.
*/
public static void warenTesten() {
Ware ware1, ware2;
Lager dasLager;
// Testen der Klasse Ware
ware1 = new Ware("Zeitung", 12.12, true);
ware1.drucken();
double p = ware1.getNettoPreis();
// Generieren von Empfehlungen
ware2 = new Ware("Potter Band 1", 31.12, false);
ware2.generiereEmpfehlungen(7);
// Abfrage des Lagers
dasLager = Lager.getLager();
dasLager.einlagern(ware1, 0);
dasLager.einlagern(ware2, 1);
// Prüfen der Lagerbestände mit tiefem Vergleichen
Ware testWare = dasLager.holen(0);
if (testWare == ware2)
System.out.println("testware und ware2 sind identisch (gut)");
if (testWare.equals(ware2))
System.out.println("testware und ware2 sind inhaltsgleich (gut)");
// vollständiges Befüllen des Lager
for (int i = 0; i < 1000; i++) {
ware2 = new Ware("Band" + i + "B", 12.12, true);
ware2.generiereEmpfehlungen(7);
dasLager.einlagern(ware2);
}
System.out.println("Lager vollständig gefüllt mit "
+ dasLager.lagerGroesse() + " Waren.");
for (int i = 0; i < 1000; i++) {
ware2 = new Ware("Volume" + i + "B", 12.12, true);
ware2.generiereEmpfehlungen(7);
dasLager.einlagern(ware2,i);
}
System.out.println("Lager erneut vollständig gefüllt mit "
+ dasLager.lagerGroesse() + " Waren.");
}
/**
* Diese Methode dient zum Testen der Klasse Buch.
* Sie nutzt die Veererbung in Java aus.
*/
public static void buecherTesten() {
/*
* 2. Testen: Anlegen einer Testmethode für Bücher
* Erzeugen von Büchern
* Anlegen einer Referenz auf eine alte Auflage
* Drucken zweier Bücher
* 3. Frage: Wie kann man Bücher in das Lager einfügen?
* 5. Einfügen der Bücher in das Lager
* 8. Anpassen der Hauptroutine
* 8.1 Alle Instanzen vom Typ Ware sollen MusikCDs werden da die Klasse
* Ware jetzt abstrakt ist.
*/
Lager dasLager;
dasLager = Lager.getLager();
Buch buch1 = new Buch("Das Grauen",22.22,"9876543");
Buch buch2 = new Buch("Das Kapital",33.33,"9876543");
buch1.setAlteAuflage(buch2);
buch1.drucken();
dasLager.einlagern(buch1);
dasLager.einlagern(buch2);
dasLager.drucken();
}
/**
* Diese Methode dient zum Testen der Klasse Buch.
* Sie nutzt die Veererbung in Java aus.
*/
public static void CDsTesten() {
/*
* 2. Testen: Anlegen einer Testmethode für Bücher
* Erzeugen von Büchern
* Anlegen einer Referenz auf eine alte Auflage
* Drucken zweier Bücher
* 3. Frage: Wie kann man Bücher in das Lager einfügen?
* 5. Einfügen der Bücher in das Lager
* 8. Anpassen der Hauptroutine
* 8.1 Alle Instanzen vom Typ Ware sollen MusikCDs werden da die Klasse
* Ware jetzt abstrakt ist.
*/
Lager dasLager;
dasLager = Lager.getLager();
MusikCD cd1 = new MusikCD("Thriller",8.88);
MusikCD cd2 = new MusikCD("Peter und der Wolf",9.99);
cd1.setEmpfehlung(cd2);
cd1.drucken();
dasLager.einlagern(cd1);
dasLager.einlagern(cd2);
dasLager.drucken();
}
/**
* Das Hauptprogramm
*
* @param args
*/
public static void main(String[] args) {
//warenTesten();
buecherTesten();
CDsTesten();
}
}

 

8.8 Lernziele

Am Ende dieses Blocks können Sie:

  • ... die Konzepte der Spezialisierung und Verallgemeinerung im Kontext der Vererbung erklären und anwenden
  • ... die Oberklasse einer Klasse mit Hilfe des Schlüsselworts extends identifizieren
  • ... alle Methoden und Attribute einer Klasse nennen
  • ... vererbte Methoden und Atribute einer Klasse von lokalen Methoden und Atrributen unterscheiden
  • ... in UML die Vererbungsbeziehung zweier Klassen erkennen
  • ... das Schlüsselwort super nutzen um gezielt einen Konstruktor der Oberklasse zu nutzen.
  • ... im Quellcode bei der Benutzung eines new Operators die Aufrufabfolge der beteiligten Konstruktoren analysieren und beschreiben
  • ... das Konzept des "Überschreibens" von Methoden und Attributen erklären, erkennen und nutzen
  • ... abstrakte und finale Klassen nutzen und Beispiele für eine vorteilhafte Anwendung nennen
  • ... das Zusammenspiel von abstrakten Methoden und abstrakten Klassen erklären und anwenden

Lernzielkontrolle

Sie sind in der Lage die folgenden Fragen zu beantworten: