In der Folge 6.1 haben wir uns angesehen, welche Probleme entstehen können, wenn man eine Klasse wie Circle einfach kopiert, um daraus eine neue Klasse wie Rectangle zu erstellen. Die beiden Klassen haben einige gleiche Methoden, und die Methoden, die sich unterscheiden, ähneln sich auch noch ziemlich stark.
Ein wichtiges Prinzip der OOP heißt:
Vermeide Redundanzen
Das heißt, man sollte doppelten oder sogar mehrfachen Quelltext in einem Projekt nach Möglichkeit vermeiden. Bei jeder Änderung des Codes muss man dann die selben Änderungen an zwei, drei oder mehr Stellen durchführen, wobei leicht Fehler entstehen können.
Generalisierung
Bei der Entwicklung größerer Programme stellt man häufig fest, dass mehrere Klassen sehr ähnlichen oder sogar identischen Quelltext enthalten. Genau das haben wir bei den Klassen Circle und Rectangle beobachtet.
In der objektorientierten Programmierung geht man in einem solchen Fall folgendermaßen vor:
- Man sucht den Quelltext, der in mehreren Klassen gemeinsam vorkommt.
- Dieser gemeinsame Quelltext wird in eine neue, allgemeinere Klasse ausgelagert.
- Die bisherigen Klassen werden anschließend zu Unterklassen dieser neuen Oberklasse. Mithilfe der Vererbung übernehmen sie automatisch die gemeinsamen Instanzvariablen und Methoden.
Dieses Vorgehen bezeichnet man als Generalisierung, weil mehrere spezielle Klassen zu einer allgemeineren Oberklasse zusammengefasst werden.
Das klingt zunächst noch etwas theoretisch. Deshalb werden wir dieses Vorgehen jetzt Schritt für Schritt auf unsere Klassen Circle und Rectangle anwenden.
Die Oberklasse Figure
Bei der Implementierung der Oberklasse Figure gehen wir genauso vor wie bei der Erstellung der Klasse Rectangle: Wir kopieren den Quelltext von Circle in die Zwischenablage, erstellen dann eine neue leere Klasse Figure und kopieren den Inhalt der Zwischenablage in diese neue Klasse.
Dann gehen wir die Instanzvariablen, den Konstruktor und die Methoden systematisch durch und übernehmen die Gemeinsamkeiten von Circle und Rectangle. Das Ergebnis sieht so aus:
import java.awt.*;
public class Figure
{
protected int xPos;
protected int yPos;
protected Color fillColor;
protected Color borderColor;
protected int borderWidth;
public Figure(int xPos, int yPos)
{
this.xPos = xPos;
this.yPos = yPos;
fillColor = Color.RED;
borderColor = Color.BLACK;
borderWidth = 1;
}
public void setFillColor(Color color)
{
fillColor = color;
}
public void setBorderColor(Color color)
{
borderColor = color;
}
public void setBorderWidth(int width)
{
if (width < 1)
width = 1;
borderWidth = width;
}
public void paint(Graphics g)
{
// leere Methode
}
}
Ausführliche Quelltextbesprechung
Die Instanzvariablen
protected int xPos;
protected int yPos;
protected Color fillColor;
protected Color borderColor;
protected int borderWidth;
Hier fallen zwei Dinge auf.
Erstens wurde der Zugriffsmodifizierer private durch protected ersetzt. Instanzvariablen, die als private deklariert werden, sind nur innerhalb der eigenen Klasse direkt lesbar und veränderbar. Auch Unterklassen können nicht direkt auf die privaten Instanzvariablen oder Methoden einer Oberklasse zugreifen.
Mit dem Zugriffsmodifizierer protected wird ein solcher direkter Zugriff durch Unterklassen ermöglicht. Das heißt, alle Unterklassen von Figure haben Zugriff auf die fünf Instanzvariablen der Oberklasse. Sie können diese nicht nur lesen, sondern ihnen auch neue Werte zuweisen.
Zweitens wurde das Deutsch-Englisch-Mischmasch bereinigt; alle Bezeichner liegen nun auf Englisch vor. Aus randfarbe wurde borderColor, aus color wurde fillColor und aus randstaerke wurde borderWidth.
Persönliche Anmerkung / Tipp
Ich arbeite beim Programmieren zunächst mit deutschen Begriffen, damit ich nicht dauernd überlegen muss, wie die passenden englischen Begriffe lauten. Wenn das Programm vollständig fertiggestellt ist, lasse ich den gesamten Quelltext einschließlich der Kommentare von einer KI ins Englische übertragen.
Der Konstruktor
public Figure(int xPos, int yPos)
{
this.xPos = xPos;
this.yPos = yPos;
fillColor = Color.RED;
borderColor = Color.BLACK;
borderWidth = 1;
}
Der Konstruktor hat nur zwei Parameter, nämlich die Position des Grafikobjekts. Bei einem Kreis ist damit der Mittelpunkt gemeint, bei einem Rechteck die linke obere Ecke. Radius, Breite, Höhe oder andere Attribute werden hier noch nicht initialisiert; das ist die Aufgabe der jeweiligen Unterklassen.
Die beiden Farben sowie die Randstärke werden im Konstruktor mit Standardwerten belegt. Mit den entsprechenden Setter-Methoden können diese Werte jederzeit angepasst werden.
Die drei Methoden
Die drei Methoden setFillColor(), setBorderColor() und setBorderWidth() wurden unverändert aus der Klasse Circle übernommen; lediglich die Bezeichner wurden ins Englische übertragen.
Die Unterklassen von Figure können nun auf diese drei Methoden zugreifen und sie verwenden, genauso als wären es ihre eigenen Methoden.
Die leere Methode paint()
Die Methode paint() wird bereits angelegt, bleibt in der Oberklasse aber zunächst leer. Die Oberklasse kann nicht festlegen, wie die verschiedenen Unterklassen ihre geometrischen Figuren darstellen sollen. Ein Kreis wird anders gezeichnet als ein Rechteck; die dafür erforderlichen Anweisungen müssen daher in den jeweiligen Unterklassen stehen.
Die Unterklassen Circle und Rectangle
Wir können nun die Klassen Circle und Rectangle stark vereinfachen. Die Instanzvariablen xPos und yPos sowie fillColor, borderColor und borderWidth werden bereits von der Oberklasse zur Verfügung gestellt und müssen daher in den Unterklassen nicht mehr deklariert werden.
Auch die drei Methoden setFillColor(), setBorderColor() und setBorderWidth() werden jetzt von der Oberklasse Figure zur Verfügung gestellt. Die beiden Unterklassen können diese Methoden so benutzen, als wären es ihre eigenen Methoden.
Auch die Konstruktoren von Circle und Rectangle können wir stark vereinfachen. Am besten schauen wir uns jetzt den Quelltext der Unterklasse Circle an.
import java.awt.*;
public class Circle extends Figure
{
private int radius;
public Circle(int xPos, int yPos, int radius)
{
super(xPos, yPos);
this.radius = radius;
}
@Override
public void paint(Graphics g)
{
g.setColor(borderColor);
g.fillOval(xPos-radius, yPos-radius,
radius*2, radius*2);
g.setColor(fillColor);
g.fillOval(xPos-radius+borderWidth,
yPos-radius+borderWidth,
radius*2-2*borderWidth,
radius*2-2*borderWidth);
}
}
Quelltextbesprechung
Die erste auffällige Veränderung gegenüber dem alten Quelltext der Klasse Circle besteht darin, dass der Quelltext jetzt auf ungefähr die Hälfte geschrumpft ist. Fünf Instanzvariablen und drei Methoden wurden in die Oberklasse ausgelagert.
Das Schlüsselwort extends
Die zweite Veränderung ist das neue Schlüsselwort extends. Auf Deutsch übersetzt heißt "extends" so viel wie "erweitert". Die Klasse Circle erweitert die Klasse Figure um eine Instanzvariable und eine spezielle paint()-Methode. Dieses Vorgehen wird auch als Spezialisierung bezeichnet: Die Klasse Circle ist eine spezialisierte Version der Klasse Figure.
Eine zusätzliche Instanzvariable
Die Klasse Circle benötigt eine zusätzliche Instanzvariable, welche den Radius des Kreises speichert.
Falls wir bereits wissen, dass später von der Klasse Circle weitere Unterklassen abgeleitet werden sollen, könnten wir den Zugriffsmodifizierer private durch protected ersetzen. Das ist hier jedoch nicht erforderlich.
Anpassung des Konstruktors
Wenn wir ein neues Circle-Objekt erzeugen, ist es sinnvoll, die Position und den Radius gleich mit anzugeben. Der Konstruktor der Klasse Circle besitzt daher drei Parameter und nicht nur zwei wie der Konstruktor der Oberklasse.
Die Initialisierung von xPos und yPos übernimmt der Konstruktor der Oberklasse. Dieser wird vom Konstruktor der Klasse Circle mit dem Schlüsselwort super aufgerufen:
super(xPos, yPos);
Diese Anweisung ruft den Konstruktor der Oberklasse auf und übergibt ihm die beiden Parameter xPos und yPos. Danach wird die zusätzliche Instanzvariable radius initialisiert.
Die paint()-Methode
Die Oberklasse stellt zwar eine paint()-Methode zur Verfügung, mit dieser leeren Methode können die Unterklassen jedoch nichts anfangen. Daher sollte jede Unterklasse von Figure eine eigene paint()-Methode implementieren.
Im nächsten Abschnitt werden wir abstrakte Methoden kennenlernen. Wenn eine Oberklasse eine abstrakte Methode deklariert, sind die Unterklassen gezwungen, diese Methode zu implementieren. Die paint()-Methode der Oberklasse Figure ist aber noch nicht abstrakt. Daher könnte die Klasse Circle theoretisch auf eine eigene paint()-Methode verzichten. In diesem Fall würde sie jedoch nur die leere Methode der Oberklasse erben, und ihre Objekte könnten nicht gezeichnet werden.
@Override
public void paint(Graphics g)
{
g.setColor(borderColor);
g.fillOval(xPos-radius, yPos-radius,
radius*2, radius*2);
g.setColor(fillColor);
g.fillOval(xPos-radius+borderWidth,
yPos-radius+borderWidth,
radius*2-2*borderWidth,
radius*2-2*borderWidth);
}
Diese Methode wurde – abgesehen von den englischen Bezeichnern – unverändert aus der alten Klasse Circle übernommen. Hier sieht man gut, wie direkt auf die mit protected deklarierten Instanzvariablen der Oberklasse zugegriffen werden kann, also ohne den Umweg über Getter-Methoden.
Hätten wir die Instanzvariablen der Oberklasse als private deklariert, könnte Circle nicht direkt auf diese Variablen zugreifen. In diesem Fall müssten entsprechende Getter-Methoden in der Oberklasse bereitgestellt und von der Unterklasse aufgerufen werden.
Übung 6.1-1
Führen Sie die hier beschriebenen Veränderungen nun in Ihrem Grafikprojekt durch. Die Klasse Zeichenfenster müssen Sie im Prinzip nicht verändern; lediglich die Methodenaufrufe müssen jetzt die neuen englischen Bezeichner verwenden.
Übung 6.1-2
Implementieren Sie die Klasse Rectangle als Unterklasse von Figure.
Sie können dazu gern den Quelltext von Circle über die Zwischenablage kopieren und anschließend entsprechend verändern – die meisten Programmierer würden genauso vorgehen.
Übung 6.1-3
Auch Unterklassen können ihrerseits Unterklassen besitzen.
Erstellen Sie die Klasse Square (Quadrat) als Unterklasse von Rectangle und testen Sie die neue Unterklasse mithilfe der Klasse Zeichenfenster.
Abstrakte Klassen und Methoden
Die paint()-Methoden
Wir betrachten noch einmal die paint()-Methode der Klasse Figure:
public void paint(Graphics g)
{
// leere Methode
}
Die Unterklassen Circle und Rectangle haben jeweils eine eigene, spezielle paint()-Methode implementiert. Die eine Version zeichnet einen Kreis, die andere ein Rechteck.
Es fällt auf, dass alle drei paint()-Methoden den gleichen Methodenkopf besitzen:
- Klasse Figure: public void paint(Graphics g)
- Klasse Circle: public void paint(Graphics g)
- Klasse Rectangle: public void paint(Graphics g)
Dadurch ist es möglich, dass die Unterklassen die von der Oberklasse geerbte Methode durch eine eigene Version ersetzen. Dieses Vorgehen bezeichnet man als Überschreiben einer Methode.
Die Methode der Unterklasse muss dabei den gleichen Methodenkopf besitzen wie die Methode der Oberklasse. Sie trägt also denselben Namen, besitzt dieselbe Parameterliste und liefert denselben Datentyp zurück. Nur der Schleifenkörper beziehungsweise Methodenrumpf wird neu festgelegt.
Bei einem Objekt der Klasse Circle wird daher die paint()-Methode aus Circle ausgeführt. Bei einem Objekt der Klasse Rectangle wird dagegen die entsprechende Methode aus Rectangle verwendet. Die leere paint()-Methode der Oberklasse kommt bei diesen Objekten nicht zum Einsatz.
Kein Zwang zum Überschreiben
Die Unterklassen müssen die leere paint()-Methode der Oberklasse aber nicht zwingend überschreiben. Sie sollten es, damit die Objekte graphisch dargestellt werden, aber ein Überschreiben ist nicht zwingend notwendig.
Abstrakte Methoden
Will man nun die Unterklassen dazu zwingen, eine Methode der Oberklasse zu überschreiben - und in diesem Beispiel wäre das tatsächlich sinnvoll - muss man die zu überschreibende Methode als abstrakte Methode deklarieren:
public abstract void paint(Graphics g);
Man schreibt in die Oberklasse also nur den Methodenkopf hin, abgeschlossen von einem Semikolon. Jede einzelne Unterklasse ist nun verpflichtet, diese Methode mit einer eigenen Implementierung zu überschreiben.
Abstrakte Klassen
Wenn eine Klasse eine abstrakte Methode (oder mehrere) enthält, muss die gesamte Klasse als abstract gekennzeichnet werden:
abstract class Figure
{
// Instanzvariablen und Methoden ...
public abstract void paint(Graphics g);
}
Keine Objekte von abstrakten Klassen
Eine abstrakte Klasse dient nur als gemeinsame Oberklasse für andere Klassen. Von ihr können keine Objekte erzeugt werden.
Folgender Quelltext ist daher nicht zulässig:
Figure figur = new Figure(100,100);
Der Java-Compiler würde hier einen Fehler melden, da Figure eine abstrakte Klasse ist.
Objekte können nur von den nicht-abstrakten Unterklassen erzeugt werden:
Circle kreis = new Circle(100,100,50); Rectangle rechteck = new Rectangle(50,50,120,80);
Beide Klassen erben die gemeinsamen Eigenschaften der Oberklasse und stellen gleichzeitig ihre eigene Implementierung der Methode paint() bereit.