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.
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.Objectjava.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());
combustion volkswagen
Wie Sie sehen, hat die Klasse Engine
alle Methoden, die in der Klasse Part
definiert sind.
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);
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!";
}
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);
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();
}
}
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);
}
}
}
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);
}
}
}
(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:
-
Zuerst wird in der Klasse
Point3D
nach einer Definition der MethodetoString
gesucht. Da sie dort nicht existiert, wird die Suche in der Superklasse fortgesetzt. -
In der Superklasse
Point
wird nach einer Definition vontoString
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 vonlocation
gesucht. Diese wird gefunden und der zugehörige Code wird ausgeführt. - Diese
location
-Methode ruft dielocation
-Methode der Superklasse auf, um das Ergebnis zu berechnen. - Als nächstes wird in der Klasse
Point3D
nach der MethodemanhattanDistanceFromOrigin
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.
- Der auszuführende Code lautet
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.
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();
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.