Part 9

Vererbung

Klassen werden in der objektorientierten Programmierung verwendet, um die Konzepte der Problemstellung zu verdeutlichen. Jede Klasse, die wir erstellen, fügt der Programmiersprache Funktionalität hinzu. Diese Funktionalität wird benötigt, um die Probleme zu lösen, denen wir begegnen. Ein wesentlicher Gedanke der objektorientierten Programmierung ist, dass Lösungen durch die Interaktion zwischen Objekten, die aus Klassen erstellt wurden, entstehen. Ein Objekt in der objektorientierten Programmierung ist eine eigenständige Einheit, die einen Zustand hat, der durch die Methoden des Objekts geändert werden kann. Objekte arbeiten zusammen; jedes hat seinen eigenen Verantwortungsbereich. Zum Beispiel haben unsere Benutzungsschnittstellenklassen bisher Scanner-Objekte verwendet.

Jede Java-Klasse erweitert die Klasse Object, was bedeutet, dass jede von uns erstellte Klasse alle in der Klasse Object definierten Methoden zur Verfügung hat. Wenn wir ändern möchten, wie diese Methoden in der Klasse Object definiert sind, müssen sie durch eine neue Implementierung in der neu erstellten Klasse überschrieben werden. Die von uns erstellten Objekte erhalten unter anderem die Methoden equals und hashCode von der Klasse Object.

Jede Klasse leitet sich von Object ab, aber es ist auch möglich, von anderen Klassen abzuleiten. Wenn wir die API (Application Programming Interface) von Java's ArrayList betrachten, stellen wir fest, dass ArrayList die Oberklasse AbstractList hat. AbstractList wiederum hat die Klasse Object als Oberklasse.

  java.lang.Object
  java.util.AbstractCollection<E>
    java.util.AbstractList<E>
       java.util.ArrayList<E>

Jede Klasse kann direkt nur eine Klasse erweitern. Eine Klasse erbt jedoch indirekt alle Eigenschaften der Klassen, von denen sie abgeleitet ist. Die Klasse ArrayList leitet sich von der Klasse AbstractList ab und erbt indirekt die Eigenschaften der Klassen AbstractCollection und Object. Somit stehen der Klasse ArrayList alle Variablen und Methoden der Klassen AbstractList, AbstractCollection und Object zur Verfügung.

Um die Eigenschaften einer Klasse zu erben, verwenden Sie das Schlüsselwort extends. Die Klasse, die die Eigenschaften erhält, wird als Unterklasse bezeichnet, und die Klasse, deren Eigenschaften vererbt werden, wird als Oberklasse bezeichnet.

Schauen wir uns ein Automobilproduktionssystem an, das Autoteile verwaltet. Eine grundlegende Komponente der Teileverwaltung ist die Klasse Part, die den Bezeichner, den Hersteller und die Beschreibung definiert.

public class Part {

    private String identifier;
    private String manufacturer;
    private String description;

    public Part(String identifier, String manufacturer, String description) {
        this.identifier = identifier;
        this.manufacturer = manufacturer;
        this.description = description;
    }

    public String getIdentifier() {
        return identifier;
    }

    public String getDescription() {
        return description;
    }

    public String getManufacturer() {
        return manufacturer;
    }
}

Ein Teil des Autos ist der Motor. Wie bei allen Teilen hat auch der Motor einen Hersteller, eine Kennung und eine Beschreibung. Darüber hinaus hat jeder Motor einen Typ: zum Beispiel einen Verbrennungsmotor, einen Elektromotor oder einen Hybridmotor.

Die traditionelle Methode zur Implementierung der Klasse Engine, ohne Vererbung zu verwenden, wäre diese:

public class Engine {

    private String engineType;
    private String identifier;
    private String manufacturer;
    private String description;

    public Engine(String engineType, String identifier, String manufacturer, String description) {
        this.engineType = engineType;
        this.identifier = identifier;
        this.manufacturer = manufacturer;
        this.description = description;
    }

    public String getEngineType() {
        return engineType;
    }

    public String getIdentifier() {
        return identifier;
    }

    public String getDescription() {
        return description;
    }

    public String getManufacturer() {
        return manufacturer;
    }
}

Wir bemerken eine erhebliche Überschneidung zwischen den Inhalten von Engine und Part. Es kann mit Sicherheit gesagt werden, dass Engine ein spezieller Fall von Part ist. Der Motor (Engine) ist ein Teil (Part), hat aber auch Eigenschaften, die ein Teil nicht hat, in diesem Fall den Motortyp.

Lassen Sie uns die Klasse Engine neu erstellen und diesmal Vererbung in unserer Implementierung verwenden. Wir erstellen die Klasse Engine, die die Klasse Part erbt: ein Motor ist ein spezieller Fall eines Teils.

public class Engine extends Part {

    private String engineType;

    public Engine(String engineType, String identifier, String manufacturer, String description) {
        super(identifier, manufacturer, description);
        this.engineType = engineType;
    }

    public String getEngineType() {
        return engineType;
    }
}

Die Klassendefinition public class Engine extends Part zeigt an, dass die Klasse Engine die Funktionalität der Klasse Part erbt. Wir definieren auch eine Objektvariable engineType in der Klasse Engine.

Der Konstruktor der Engine-Klasse ist erwähnenswert. In der ersten Zeile verwenden wir das Schlüsselwort super, um den Konstruktor der Oberklasse aufzurufen. Der Aufruf super(identifier, manufacturer, description) ruft den Konstruktor public Part(String identifier, String manufacturer, String description) auf, der in der Klasse Part definiert ist. Durch diesen Prozess werden die in der Oberklasse definierten Objektvariablen mit ihren Anfangswerten initialisiert. Nach dem Aufruf des Oberklassenkonstruktors setzen wir auch den richtigen Wert für die Objektvariable engineType.

Der super-Aufruf ähnelt dem this-Aufruf in einem Konstruktor; this wird verwendet, um einen Konstruktor dieser Klasse aufzurufen, während super verwendet wird, um einen Konstruktor der Oberklasse aufzurufen. Wenn ein Konstruktor den Konstruktor der Oberklasse durch den Aufruf von super verwendet, muss der super-Aufruf in der ersten Zeile des Konstruktors stehen. Dies ist ähnlich wie bei einem this-Aufruf (muss ebenfalls in der ersten Zeile des Konstruktors stehen).

Da die Klasse Engine die Klasse Part erweitert, hat sie alle Methoden zur Verfügung, die die Klasse Part bietet. Sie können Instanzen der Klasse Engine genauso erstellen wie jede andere Klasse.

Engine engine = new Engine("combustion", "hz", "volkswagen", "VW GOLF 1L 86-91");
System.out.println(engine.getEngineType());
System.out.println(engine.getManufacturer());
Beispielausgabe

combustion volkswagen

Wie Sie sehen, hat die Klasse Engine alle Methoden, die in der Klasse Part definiert sind.

Loading

Zugriffsmodifikatoren private, protected und public

Wenn eine Methode oder Variable den Zugriffsmodifikator private hat, ist sie nur für die internen Methoden dieser Klasse sichtbar. Unterklassen sehen sie nicht, und eine Unterklasse hat keinen direkten Zugang dazu. Aus der Klasse Engine gibt es also keine Möglichkeit, direkt auf die Variablen identifier, manufacturer und description zuzugreifen, die in der Oberklasse Part definiert sind. Der Programmierende kann nicht auf die Variablen der Oberklasse zugreifen, die mit dem Zugriffsmodifikator private definiert wurden.

Eine Unterklasse sieht alles, was in der Oberklasse mit dem Modifikator public definiert ist. Wenn wir Variablen oder Methoden definieren möchten, die für die Unterklassen sichtbar, aber für alles andere unsichtbar sind, können wir den Zugriffsmodifikator protected verwenden.

Aufruf des Konstruktors der Oberklasse

Sie verwenden das Schlüsselwort super, um den Konstruktor der Oberklasse aufzurufen. Der Aufruf erhält als Parameter die Typen von Werten, die der Konstruktor der Oberklasse erfordert. Wenn es mehrere Konstruktoren in der Oberklasse gibt, bestimmen die Parameter des super-Aufrufs, welcher davon verwendet wird.

Wenn der Konstruktor (der Unterklasse) aufgerufen wird, werden die in der Oberklasse definierten Variablen initialisiert. Die Ereignisse, die während des Konstruktaufrufs auftreten, sind praktisch identisch mit denen eines normalen Konstruktaufrufs. Wenn die Oberklasse keinen parameterlosen Konstruktor bereitstellt, muss in den Konstruktoren der Unterklasse immer ein expliziter Aufruf des Konstruktors der Oberklasse enthalten sein.

Wir demonstrieren im folgenden Beispiel, wie this und super aufgerufen werden. Die Klasse Superclass enthält eine Objektvariable und zwei Konstruktoren. Einer davon ruft den anderen Konstruktor mit dem Schlüsselwort this auf. Die Klasse Subclass enthält einen parameterisierten Konstruktor, hat jedoch keine Objektvariablen. Der Konstruktor der Subclass ruft den parameterisierten Konstruktor der Superclass auf.

public class Superclass {

    private String objectVariable;

    public Superclass() {
        this("Beispiel");
    }

    public Superclass(String value) {
        this.objectVariable = value;
    }

    public String toString() {
        return this.objectVariable;
    }
}
public class Subclass extends Superclass {

    public Subclass() {
        super("Subclass");
    }
}
Superclass sup = new Superclass();
Subclass sub = new Subclass();

System.out.println(sup);
System.out.println(sub);
Beispielausgabe

Beispiel Subclass

Aufruf einer Methode der Oberklasse

Sie können die in der Oberklasse definierten Methoden aufrufen, indem Sie den Aufruf mit super voranstellen, genauso wie Sie die in dieser Klasse definierten Methoden mit this voranstellen können. Wenn Sie beispielsweise die toString-Methode überschreiben, können Sie die Definition dieser Methode in der Oberklasse auf folgende Weise aufrufen:

@Override
public String toString() {
    return super.toString() + "\n  Und fügen wir noch meine eigene Nachricht hinzu!";
}
Loading

Der tatsächliche Typ eines Objekts bestimmt, welche Methode ausgeführt wird

Der Typ eines Objekts entscheidet darüber, welche Methoden das Objekt bereitstellt. Zum Beispiel haben wir früher die Klasse Student implementiert. Wenn ein Verweis auf ein Objekt des Typs Student in einer Variablen des Typs Person gespeichert wird, sind nur die in der Klasse Person (und deren Oberklasse und Schnittstellen) definierten Methoden verfügbar:

Person ollie = new Student("Ollie", "6381 Hollywood Blvd. Los Angeles 90028");
ollie.credits();        // FUNKTIONIERT NICHT!
ollie.study();          // FUNKTIONIERT NICHT!
System.out.println(ollie);   // ollie.toString() FUNKTIONIERT

Ein Objekt hat also die Methoden zur Verfügung, die sich auf seinen Typ und auch auf seine Oberklassen und Schnittstellen beziehen. Das oben genannte Student-Objekt bietet die in den Klassen Person und Object definierten Methoden an.

Im letzten Beispiel haben wir eine neue toString-Implementierung für Student geschrieben, um die Methode zu überschreiben, die von Person geerbt wird. Die Klasse Person hatte bereits die toString-Methode überschrieben, die sie von der Klasse Object geerbt hatte. Wenn wir ein Objekt anhand eines anderen Typs als seines tatsächlichen Typs behandeln, welche Version der Methode des Objekts wird dann aufgerufen?

Im folgenden Beispiel haben wir zwei Studenten, die durch Variablen unterschiedlicher Typen referenziert werden. Welche Version der toString-Methode wird ausgeführt: die in Object, Person oder Student definierte?

Student ollie = new Student("Ollie", "6381 Hollywood Blvd. Los Angeles 90028");
System.out.println(ollie);
Person olliePerson = new Student("Ollie", "6381 Hollywood Blvd. Los Angeles 90028");
System.out.println(olliePerson);
Object ollieObject = new Student("Ollie", "6381 Hollywood Blvd. Los Angeles 90028");
System.out.println(ollieObject);

Object alice = new Student("Alice", "177 Stewart Ave. Farmington, ME 04938");
System.out.println(alice);
Beispielausgabe
Ollie 6381 Hollywood Blvd. Los Angeles 90028 study credits 0 Ollie 6381 Hollywood Blvd. Los Angeles 90028 study credits 0 Ollie 6381 Hollywood Blvd. Los Angeles 90028 study credits 0 Alice 177 Stewart Ave. Farmington, ME 04938 study credits 0

Die auszuführende Methode wird basierend auf dem tatsächlichen Typ des Objekts ausgewählt, was bedeutet, dass die Klasse, deren Konstruktor beim Erstellen des Objekts aufgerufen wird, maßgeblich ist. Wenn die Methode in dieser Klasse keine Definition hat, wird die Version der Methode aus der Klasse gewählt, die der tatsächlichen Klasse im Vererbungshierarchie am nächsten steht.

Betrachten wir die Polymorphie mit einem weiteren Beispiel.

Sie könnten einen Punkt im zweidimensionalen Koordinatensystem mit der folgenden Klasse darstellen:

public class Point {

    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int manhattanDistanceFromOrigin() {
        return Math.abs(x) + Math.abs(y);
    }

    protected String location(){
        return x + ", " + y;
    }

    @Override
    public String toString() {
        return "(" + this.location() + ") distance " + this.manhattanDistanceFromOrigin();
    }
}
Die Methode location ist nicht für die externe Verwendung vorgesehen, weshalb sie als protected definiert ist. Unterklassen haben jedoch weiterhin Zugriff auf die Methode. Manhattan-Distanz bedeutet die Entfernung zwischen zwei Punkten, wenn man sich nur in Richtung der Koordinatenachsen bewegen kann. Sie wird in vielen Navigationsalgorithmen verwendet, zum Beispiel.

Ein farbiger Punkt ist ansonsten identisch mit einem Punkt, enthält jedoch auch eine Farbe, die als Zeichenkette ausgedrückt wird. Aufgrund der Ähnlichkeit können wir eine neue Klasse erstellen, indem wir die Klasse Point erweitern.

public class ColorPoint extends Point {

    private String color;

    public ColorPoint(int x, int y, String color) {
        super(x, y);
        this.color = color;
    }

    @Override
    public String toString() {
        return super.toString() + " color: " + color;
    }
}

Die Klasse definiert eine Objektvariable, in der wir die Farbe speichern. Die Koordinaten sind bereits in der Oberklasse definiert. Wir möchten, dass die Zeichenkettenrepräsentation der der Klasse Point entspricht, aber auch Informationen über die Farbe enthält. Die überschriebene toString-Methode ruft die toString-Methode der Oberklasse auf und fügt dieser die Farbe des Punktes hinzu.

Als Nächstes fügen wir der Liste einige Punkte hinzu. Einige davon sind "normale" Punkte, während andere Farbpunkte sind. Am Ende des Beispiels drucken wir die Punkte auf der Liste aus. Für jeden Punkt wird die toString-Methode basierend auf dem tatsächlichen Typ des Punktes ausgeführt, auch wenn die Liste alle Punkte als Point-Typen kennt.

public class Main {
    public static void main(String[] args) {
        ArrayList<Point> points = new ArrayList<>();
        points.add(new Point(4, 8));
        points.add(new ColorPoint(1, 1, "green"));
        points.add(new ColorPoint(2, 5, "blue"));
        points.add(new Point(0, 0));

        for (Point p: points) {
            System.out.println(p);
        }
    }
}
Beispielausgabe
(4, 8) distance 12 (1, 1) distance 2 color: green (2, 5) distance 7 color: blue (0, 0) distance 0

Wir möchten auch einen dreidimensionalen Punkt in unserem Programm einfügen. Da er keine Farbinformationen hat, leiten wir ihn von der Klasse Point ab.

public class Point3D extends Point {

    private int z;

    public Point3D(int x, int y, int z) {
        super(x, y);
        this.z = z;
    }

    @Override
    protected String location() {
        return super.location() + ", " + z;    // das Ergebnis ist eine Zeichenkette in der Form "x, y, z"
    }

    @Override
    public int manhattanDistanceFromOrigin() {
        // zuerst wird der Abstand basierend auf x und y berechnet
        // und dann der Einfluss der z-Koordinate hinzugefügt
        return super.manhattanDistanceFromOrigin() + Math.abs(z);
    }

    @Override
    public String toString() {
        return "(" + this.location() + ") distance " + this.manhattanDistanceFromOrigin();
    }
}

Ein dreidimensionaler Punkt definiert also eine Objektvariable, die die dritte Dimension darstellt, und überschreibt die Methoden location, manhattanDistanceFromOrigin und toString, sodass sie auch die dritte Dimension berücksichtigen. Lassen Sie uns nun das vorherige Beispiel erweitern und auch dreidimensionale Punkte in die Liste aufnehmen.

public class Main {

    public static void main(String[] args) {
        ArrayList<Point> points = new ArrayList<>();
        points.add(new Point(4, 8));
        points.add(new ColorPoint(1, 1, "green"));
        points.add(new ColorPoint(2, 5, "blue"));
        points.add(new Point3D(5, 2, 8));
        points.add(new Point(0, 0));

        for (Point p: points) {
            System.out.println(p);
        }
    }
}
Beispielausgabe

(4, 8) distance 12 (1, 1) distance 2 color: green (2, 5) distance 7 color: blue (5, 2, 8) distance 15 (0, 0) distance 0

Wir stellen fest, dass die toString-Methode in Point3D genau die gleiche ist wie die toString-Methode in Point. Könnten wir uns die Mühe sparen und toString nicht überschreiben? Die Antwort lautet ja! Die Klasse Point3D wird wie folgt vereinfacht:

public class Point3D extends Point {

    private int z;

    public Point3D(int x, int y, int z) {
        super(x, y);
        this.z = z;
    }

    @Override
    protected String location() {
        return super.location() + ", " + z;
    }

    @Override
    public int manhattanDistanceFromOrigin() {
        return super.manhattanDistanceFromOrigin() + Math.abs(z);
    }
}

Was passiert genau, wenn die toString-Methode eines dreidimensionalen Punktes aufgerufen wird?

Der Ablauf der Ausführung erfolgt in den folgenden Schritten:

  1. Zuerst wird in der Klasse Point3D nach einer Definition der Methode toString gesucht. Da sie dort nicht existiert, wird die Suche in der Superklasse fortgesetzt.

  2. In der Superklasse Point wird nach einer Definition von toString gesucht. Diese wird gefunden, sodass der Code innerhalb der Methode ausgeführt wird.

    • Der auszuführende Code lautet return "("+this.location()+") distance "+this.manhattanDistanceFromOrigin();.
    • Zuerst wird die Methode location ausgeführt.
    • Es wird in der Klasse Point3D nach einer Definition von location gesucht. Diese wird gefunden und der zugehörige Code wird ausgeführt.
    • Diese location-Methode ruft die location-Methode der Superklasse auf, um das Ergebnis zu berechnen.
    • Als nächstes wird in der Klasse Point3D nach der Methode manhattanDistanceFromOrigin gesucht. Diese wird gefunden und ihr Code wird ausgeführt.
    • Auch hier ruft die Methode während der Ausführung die gleichnamige Methode der Superklasse auf.

Wie wir sehen, besteht die durch den Methodenaufruf ausgelöste Ereigniskette aus mehreren Schritten. Das Prinzip ist jedoch klar: Die Definition der Methode wird zuerst in der Klassendefinition des tatsächlichen Typs des Objekts gesucht. Wenn sie dort nicht gefunden wird, wird in der Superklasse weitergesucht. Falls auch dort keine Definition gefunden wird, geht die Suche in der Superklasse der Superklasse weiter, und so weiter...

Wann lohnt es sich, Vererbung zu verwenden?

Vererbung ist ein Werkzeug zum Aufbau und zur Spezialisierung von Konzept-Hierarchien; eine Unterklasse ist immer ein Sonderfall der Superklasse. Wenn die zu erstellende Klasse ein Sonderfall einer bestehenden Klasse ist, könnte diese neue Klasse durch Erweiterung der bestehenden Klasse erstellt werden. Zum Beispiel ist in dem zuvor diskutierten Szenario eines Autoteils ein Motor ein Teil, aber ein Motor hat zusätzliche Funktionen, die nicht alle Teile haben.

Bei der Vererbung übernimmt die Unterklasse die Funktionalität der Superklasse. Wenn die Unterklasse einige der geerbten Funktionen nicht benötigt oder nutzt, ist die Vererbung nicht gerechtfertigt. Klassen, die erben, übernehmen alle Methoden und Schnittstellen der Superklasse, sodass die Unterklasse überall dort verwendet werden kann, wo die Superklasse verwendet wird. Es ist ratsam, die Vererbungshierarchie flach zu halten, da die Pflege und Weiterentwicklung der Hierarchie schwieriger wird, je größer sie wird. Allgemein gilt: Wenn Ihre Vererbungshierarchie mehr als 2 oder 3 Ebenen tief ist, könnte die Struktur des Programms wahrscheinlich verbessert werden.

Vererbung ist nicht in jedem Szenario nützlich. Beispielsweise wäre es falsch, die Klasse Car von der Klasse Part (oder Engine) abzuleiten. Ein Auto beinhaltet einen Motor und Teile, aber ein Motor oder ein Teil ist kein Auto. Allgemeiner gesagt, wenn ein Objekt andere Objekte besitzt oder aus ihnen besteht, sollte Vererbung nicht verwendet werden.

Bei der Verwendung von Vererbung sollten Sie darauf achten, dass das Single Responsibility Principle eingehalten wird. Jede Klasse sollte nur einen Grund zur Änderung haben. Wenn Sie feststellen, dass durch die Vererbung zusätzliche Verantwortlichkeiten zu einer Klasse hinzugefügt werden, sollten Sie die Klasse in mehrere Klassen aufteilen.

Beispiel für den Missbrauch von Vererbung

Betrachten wir ein Postdienstunternehmen und einige zugehörige Klassen. Customer enthält die Informationen eines Kunden, und die Klasse Order, die von der Klasse Customer erbt und die Informationen über das bestellte Produkt enthält. Die Klasse Order hat auch eine Methode namens postalAddress, die die Postadresse darstellt, an die die Bestellung versandt wird.

public class Customer {

    private String name;
    private String address;

    public Customer(String name, String address) {
        this.name = name;
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}
public class Order extends Customer {

    private String product;
    private String count;

    public Order(String product, String count, String name, String address) {
        super(name, address);
        this.product = product;
        this.count = count;
    }

    public String getProduct() {
        return product;
    }

    public String getCount() {
        return count;
    }

    public String postalAddress() {
        return this.getName() + "\n" + this.getAddress();
    }
}

Die oben gezeigte Vererbung ist nicht korrekt. Bei der Vererbung muss die Unterklasse ein spezieller Fall der Superklasse sein; eine Bestellung ist definitiv kein spezieller Fall eines Kunden. Der Missbrauch zeigt sich darin, wie der Code das Single Responsibility Principle verletzt: Die Klasse Order ist sowohl für die Pflege der Kundeninformationen als auch für die Bestellinformationen verantwortlich.

Das Problem wird sehr deutlich, wenn wir darüber nachdenken, was eine Adressänderung bei einem Kunden verursachen würde.

Falls sich die Adresse ändert, müssten alle Bestellobjekte, die sich auf diesen Kunden beziehen, geändert werden. Das ist kaum ideal. Eine bessere Lösung wäre, den Kunden als Objektvariable der Klasse Order zu kapseln. Wenn man genauer über die Semantik einer Bestellung nachdenkt, scheint dies intuitiv. Eine Bestellung hat einen Kunden.

Lassen Sie uns die Klasse Order so ändern, dass sie einen Verweis auf ein Customer-Objekt enthält.

public class Order {

    private Customer customer;
    private String product;
    private String count;

    public Order(Customer customer, String product, String count) {
        this.customer = customer;
        this.product = product;
        this.count = count;
    }

    public String getProduct() {
        return product;
    }

    public String getCount() {
        return count;
    }

    public String postalAddress() {
        return this.customer.getName() + "\n" + this.customer.getAddress();
    }
}

Diese Version der Klasse Order ist besser. Die Methode postalAddress verwendet den customer-Verweis, um die Postadresse zu erhalten, anstatt die Klasse Customer zu erben. Dies erleichtert sowohl die Wartung des Programms als auch dessen konkrete Funktionalität.

Jetzt müssen bei einer Änderung der Kundendaten nur noch die Kundeninformationen geändert werden; die Bestellungen müssen nicht mehr angepasst werden.

Loading

Abstrakte Klassen

Manchmal gibt es bei der Planung einer Vererbungshierarchie Fälle, in denen es ein klares Konzept gibt, dieses Konzept jedoch keine gute Wahl für ein eigenständiges Objekt ist. Das Konzept wäre aus Sicht der Vererbung von Vorteil, da es Variablen und Funktionalitäten enthält, die von allen Klassen geteilt werden, die es erben würden. Andererseits sollten keine Instanzen dieses Konzepts selbst erstellt werden können.

Eine abstrakte Klasse kombiniert Schnittstellen und Vererbung. Sie können keine Instanzen von ihnen erstellen – Sie können nur Instanzen von Unterklassen einer abstrakten Klasse erstellen. Sie können normale Methoden enthalten, die einen Methodenkörper haben, aber es ist auch möglich, abstrakte Methoden zu definieren, die nur die Methodendefinition enthalten. Die Implementierung der abstrakten Methoden ist Aufgabe der Unterklassen. Im Allgemeinen werden abstrakte Klassen in Situationen verwendet, in denen das Konzept, das die Klasse darstellt, kein klar unabhängiges Konzept ist. In einem solchen Fall sollten keine Instanzen davon erstellt werden können.

Um eine abstrakte Klasse oder eine abstrakte Methode zu definieren, wird das Schlüsselwort abstract verwendet. Eine abstrakte Klasse wird mit dem Ausdruck public abstract class *NameOfClass* definiert; eine abstrakte Methode wird durch public abstract returnType nameOfMethod definiert. Schauen wir uns die folgende abstrakte Klasse Operation an, die eine Struktur für Operationen und deren Ausführung bietet.

public abstract class Operation {

    private String name;

    public Operation(String name) {
            this.name = name;
     }

    public String getName() {
            return this.name;
    }

    public abstract void execute(Scanner scanner);
}

Die abstrakte Klasse Operation dient als Grundlage für die Implementierung verschiedener Aktionen. Beispielsweise können Sie die Plus-Operation implementieren, indem Sie die Klasse Operation auf folgende Weise erweitern.

public class PlusOperation extends Operation {

    public PlusOperation() {
    	   super("PlusOperation");
    }

    @Override
    public void execute(Scanner scanner) {
	   System.out.print("First number: ");
	   int first = Integer.valueOf(scanner.nextLine());
	   System.out.print("Second number: ");
	   int second = Integer.valueOf(scanner.nextLine());

	   System.out.println("The sum of the numbers is " + (first + second));
    }
}

Da alle Klassen, die von Operation erben, auch den Typ Operation haben, können wir eine Benutzungsschnittstelle erstellen, indem wir Variablen vom Typ Operation verwenden. Als nächstes zeigen wir die Klasse UserInterface, die eine Liste von Operationen und einen Scanner enthält. Es ist möglich, der Benutzungsschnittstelle dynamisch Operationen hinzuzufügen.

public class UserInterface {

    private Scanner scanner;
    private ArrayList<Operation> operations;

    public UserInterface(Scanner scanner) {
        this.scanner = scanner;
        this.operations = new ArrayList<>();
    }

    public void addOperation(Operation operation) {
        this.operations.add(operation);
    }

    public void start() {
        while (true) {
            printOperations();
            System.out.println("Choice: ");

            String choice = this.scanner.nextLine();
            if (choice.equals("0")) {
                break;
            }

            executeOperation(choice);
            System.out.println();
        }
    }

    private void printOperations() {
        System.out.println("\t0: Stop");
        int i = 0;
        while (i < this.operations.size()) {
            String operationName = this.operations.get(i).getName();
            System.out.println("\t" + (i + 1) + ": " + operationName);
            i = i + 1;
        }
    }

    private void executeOperation(String choice) {
        int operation = Integer.valueOf(choice);

        Operation chosen = this.operations.get(operation - 1);
        chosen.execute(scanner);
    }
}

Die Benutzungsschnittstelle funktioniert folgendermaßen:

UserInterface userInterface = new UserInterface(new Scanner(System.in));
userInterface.addOperation(new PlusOperation());

userInterface.start();
Beispielausgabe

Operations: 0: Stop 1: PlusOperation Choice: 1 First number: 8 Second number: 12 The sum of the numbers is 20

Operations: 0: Stop 1: PlusOperation Choice: 0

Der größte Unterschied zwischen Schnittstellen und abstrakten Klassen besteht darin, dass abstrakte Klassen Objektvariablen und Konstruktoren zusätzlich zu Methoden enthalten können. Da Sie auch in abstrakten Klassen Funktionalität definieren können, können Sie sie z. B. zur Definition von Standardverhalten verwenden. In der oben gezeigten Benutzungsschnittstelle wurde die Funktionalität zur Speicherung des Operationsnamens verwendet, die in der abstrakten Klasse Operation definiert wurde.

Loading
Sie haben das Ende dieses Abschnitts erreicht! Weiter zum nächsten Abschnitt: