Kapitel 5 Objektorientierte Programmierung in Java

Werbung
Kapitel 5
Objektorientierte
Programmierung in Java
Grundzüge der objektorientierten Programmierung haben wir bereits
in Kapitel 2 kennengelernt, auch Teile der entsprechenden Java-Syntax.
Dieses Kapitel soll nun etwas systematischer und ausführlicher noch
einmal die entsprechenden Java-Konstrukte zur Umsetzung objektorientierter Programmierung sowie Ausnahmen und Spezialfälle behandeln. Beginnen wollen wir jedoch mit einem Blick in die Historie,
denn die Objektorientierung ist nur der (derzeitige) Schlusspunkt einer längeren Entwicklung.
5.1
Traditionelle Konzepte der Softwaretechnik
Folgende traditionelle Konzepte des Software-Engineering werden u.a. im objektorientierten Ansatz verwendet:
Datenabstraktion (bzw. Datenkapselung) und Information Hiding
Die zentrale Idee der Datenkapselung ist, dass auf eine Datenstruktur nicht
direkt zugegriffen wird, indem etwa einzelne Komponenten gelesen oder
geändert werden, sondern, dass dieser Zugriff ausschließlich über Zugriffsoperatoren erfolgt. Es werden also die Implementierungen der Operationen
und die Datenstrukturen selbst versteckt.
Vorteil: Implementierungdetails können beliebig geändert werden, ohne
Auswirkung auf den Rest des Programmes zu haben.
abstrakte Datentypen (ADT)
Realisiert wird die Datenabstraktion duch den Einsatz abstrakter Datentypen, die Liskov & Zilles (1974) folgendermaßen definierten:
61
“An abstract data type defines a class of abstract objects which
is completely characterized by the operations available on those
objects. This means that an abstract data type can be defined by
defining the characterizing operations for that type.”
Oder etwas prägnanter:
Datentyp = Menge(n) von Werten + Operationen darauf
abstrakter Datentyp = Operationen auf Werten, deren Repräsentation nicht
bekannt ist. Der Zugriff erfolgt ausschließlich über Operatoren.
Datenabstraktion fördert die Wiederverwendbarkeit von Programmteilen und die
Wartbarkeit großer Programme.
5.1.1
Beispiel: Der ADT Stack
Stack: Eine Datenstruktur über einem Datentyp T bezeichnet man als Stack1 ,
wenn die Einträge der Datenstruktur als Folge organisiert sind und es die
Operationen push, pop und peek gibt:
push fügt ein Element von T stets an das Ende der Folge.
pop entfernt stets das letzte Element der Folge.
peek liefert das letzte Element der Folge, ohne sie zu verändern.
Prinzip: last in first out (LIFO)
Typen der Operationen:
initStack:
push:
pop:
peek:
empty:
T × Stack
Stack
Stack
Stack
−→
−→
−→
−→
−→
Stack
Stack
Stack
T
boolean
Spezifikation der Operationen durch Gleichungen. Sei x eine Variable vom Typ
T, stack eine Variable vom Typ Stack:
empty (initStack)
empty (push (x, stack))
peek (push (x, stack))
pop (push (x, stack))
=
=
=
=
true
false
x
stack
initStack und push sind Konstruktoren (sie konstruieren Terme), daher gibt es
keine Gleichungen für sie.
1
bedeutet soviel wie Keller oder Stapel
62
5.2
Konzepte der objektorientierten Programmierung
Ziel jeglicher Programmierung ist:
• Modellierung von Ausschnitten der Realität
• sachgerechte Abstraktion
• realitätsnahes Verhalten
• Nachbildung von Ähnlichkeit im Verhalten
• Klassifikation von Problemen
Je nach Problem können verschiedene Klassifikationen sachgerecht sein, dies ist
anhand eines Beispiels aus der Biologie in der Abbildung 5.1 dargestellt.
Tiere
HH
?
HH
j
H
?
Insekten
HH
Säugetiere
Fische
@
@
R
@
?
@
@
R
@
?
@
@
R
@
Tiere
HH
HH
HH
j
H
?
Zuchttiere
Wild
?
@
@
R
@
?
@
Störtiere
@
R
@
?
@
@
R
@
Abbildung 5.1: Phylogenetische (oben) und ökonomische Klassifizierung (unten).
Es werden immer bestimmte Funktionen auf bestimmte Daten angewendet. Soll
nun die Architektur eines Systems (Modells) auf den Daten oder auf den Funktionen aufbauen?
Grundsätzlich gibt es drei Vorgehensweisen:
63
1. die funktionsorientierte
2. die datenorientierte
3. die objektorientierte
Der Kerngedanke des objektorientierten Ansatzes besteht darin, Daten und Funktionen zu verschmelzen. Im ersten Schritt werden die Daten abgeleitet, im zweiten
Schritt werden den Daten die Funktionen zugeordnet, die sie manipulieren. Die
entstehenden Einheiten aus Daten und Funktionen werden Objekte genannt. Wir
schränken den Begriff Objektorientierung gemäß folgender Gleichung von Coad
& Yourdon weiter ein:
Objektorientierung =
Klassen und Objekte
+ Kommunikation mit Nachrichten
+ Vererbung
Im folgenden erläutern wir diese Konzepte kurz.
5.3
Klassen und Objekte
Eine Klasse besteht konzeptionell aus einer Schnittstelle und einem Rumpf. In der
Schnittstelle sind die nach außen zur Verfügung gestellten Methoden (und manchmal auch öffentlich zugängliche Daten), sowie deren Semantik aufgelistet. Diese
Auflistung wird oft als Vertrag oder Nutzungsvorschrift zwischen dem Entwerfer
der Klasse und dem sie verwendenen Programmierer gedeutet. Der Klassenrumpf
enthält alle von außen unsichtbaren Implementierungdetails.
Historisch gesehen ist der Klassenbegriff älter als der Begriff des abstrakten Datentypen (ADT). In der Programmiersprache Simula 67 gab es bereits Klassen
als Mechanismus zur Datenkapselung (Abstakte Datentypen wurden erstmals
1974 von Liskov & Zilles definiert). Der Kerngedanke der Objektorientierung,
Daten und Funktionen konsequent als Objekte zusammenzufassen, wird jedoch
auf die Programmiersprache Smalltalk zurückgeführt (entwickelt seit Beginn der
70er Jahre).
5.4
Kommunikation mit Nachrichten
Objekte besitzen die Möglichkeit, mit Hilfe ihrer Methoden Aktionen auszuführen. Das Senden einer Nachricht stößt die Ausführung einer Methode an. Eine
Nachricht besteht aus einem Empfänger (das Objekt, das die Aktionen ausführen
soll), einem Selektor (die Methode, deren Aktionen auszuführen sind) und gegebenenfalls aus Argumenten (Werte, auf die während der Ausführung der Aktion
zugegriffen wird).
64
5.5
Vererbung
Gleichartige Objekte werden zu Klassen zusammengefasst. Häufig besitzen Objekte zwar bestimmte Gemeinsamkeiten, sind aber nicht völlig gleichartig. Um
solche Ähnlichkeiten auszudrücken, ist es möglich, zwischen Klassen Vererbungsbeziehungen festzulegen. Dazu wird das Verhalten einer existierenden Klasse erweitert. Die Erweiterung erzeugt eine von ihr alle Attribute und Methoden erbende neue Klasse, die um weitere Attribute und Methoden ergänzt wird. Die
neue Klasse wird Unterklasse, die ursprüngliche Klasse Oberklasse genannt.
Gemeinsamkeiten:
Unterschiede:
in der Oberklasse
in der Unterklasse
Eine Unterklasse kann auch von der Oberklasse ererbte Methoden redefinieren
(überschreiben). Wir sprechen von Einfachvererbung, wenn jede neue Klasse genau eine Oberklasse erweitert (Abbildung 5.2).
Object
@
@
R
@
?
System
Math
Point
@
@
R
@
...
Abbildung 5.2: Einfachvererbung (Java)
...
@
@
R
@
Tiere
@
Pflanzen
@
R
@
Fleischfresser
@
@
R
@
...
Abbildung 5.3: Mehrfachvererbung
65
Wenn eine Klasse mehrere Oberklassen besitzen kann, sprechen wir von Mehrfachvererbung (Abbildung 5.3). In Java gibt es nur Einfachvererbung (aus gutem
Grund). Die einzige Klasse, die keine Oberklasse erweitert, ist die vordefinierte
Klasse Object. Klassen, die nicht explizit andere Klassen erweitern, erweitern
implizit die Klasse Object. Alle Objektreferenzen sind in polymorpher Weise von
der Klasse Object, so dass Object die generische Klasse für Referenzen ist, die
sich auf Objekte jeder beliebigen Klasse beziehen können. Das nächste Beispiel
verdeutlicht dies.
Object oref = new Point();
oref = "eine Zeichenkette";
5.6
Konstruktoren und Initialisierungsblöcke
Einem neu erzeugten Objekt wird ein Anfangszustand zugewiesen. Datenfelder
können bei ihrer Deklaration mit einem Wert initialisiert werden, was manchmal
ausreicht, um einen sinnvollen Anfangszustand sicherzustellen. Oft ist aber mehr
als nur einfache Dateninitialisierung zur Erzeugung eines Anfangszustands nötig; der erzeugende Code muss vielleicht Anfangsdaten liefern oder Operationen
ausführen, die nicht als einfache Zuweisungen ausgedrückt werden können. Um
mehr als einfache Initialisierungen bewerkstelligen zu können, können Klassen
Konstruktoren enthalten. Konstruktoren sind keine Methoden, aber methodenähnlich: Sie haben denselben Namen wie die von ihnen initialisierte Klasse, haben keine oder mehrere Parameter und keinen Rückgabetyp. Bei der Erzeugung
eines Objekts mit new werden eventuelle Parameterwerte nach dem Klassennamen in einem Klammernpaar angegeben. Bei der Objekterzeugung werden zuerst
den Instanzvariablen ihre voreingestellten Anfangswerte zugewiesen, dann ihre
Initialisierungsausdrücke berechnet und zugewiesen und dann der Konstruktor
aufgerufen.
Im folgenden benutzen wir die Klasse Circle als Standardbeispiel. Ein Kreis
besteht aus einer x-Koordinate, einer y-Koordinate sowie dem Radius r. Desweiteren wird die Anzahl der erzeugten Kreise gezählt durch die Anweisung
numCircles++;, die bei jedem Aufruf des parameterlosen Konstruktors ausgeführt wird.
public class Circle {
int x=0, y=0, r=1;
static int numCircles=0;
public Circle() {
numCircles++;
}
66
public double circumference() {
return 2*Math.PI*r;
}
public double area() {
return Math.PI*r*r;
}
public static void main(String[] args) {
Circle c = new Circle();
System.out.println(c.r);
System.out.println(c.circumference());
System.out.println(c.area());
System.out.println(numCircles);
}
}
Statt des parameterlosen Konstruktors hätten wir in der Klasse auch einen Konstruktor mit drei Parametern definieren können, der nicht nur Einheitskreise erzeugen kann:
public Circle(int xCoord, int yCoord, int radius) {
numCircles++;
x = xCoord;
y = yCoord;
r = radius;
}
Standardmäßig benennt man die Parametervariablen im Konstruktor genauso wie
die Variablen in der Klasse. Da aber hierbei Namenskonflikte entstehen, muss man
die Variable des Objektes mit this.Variable referenzieren.
public Circle(int x, int y, int r) {
numCircles++;
this.x = x;
this.y = y;
this.r = r;
}
Für eine Klasse kann es in Java auch mehrere Konstruktoren geben. Diese müssen sich allerdings in der Anzahl der Attribute bzw. deren Typen unterscheiden.
Dies nennt man Überladen von Konstruktoren. In der folgenden Klasse gibt es
drei Konstruktoren namens Circle. Die Konstruktoren mit Parametern rufen den
67
parameterlosen Konstruktor mittels this() auf. Dies hat den Vorteil, dass Änderungen an den Konstruktoren nicht an drei Stellen gemacht werden müssen (was
fehleranfällig ist), sondern nur im parameterlosen Konstruktor. Um die Anzahl
der erzeugten Kreise zu zählen, muss man die Programmzeile numCircles++; nur
dem parameterlosen Konstruktor hinzufügen.
public class Circle {
int x = 0, y = 0, r = 1;
static int numCircles;
public Circle() {
numCircles++;
}
public Circle(int x, int y, int r) {
this();
this.x = x;
this.y = y;
this.r = r;
}
public Circle(int r) {
this(0,0,r);
}
public static void main(String[] args) {
Circle c1 = new Circle();
Circle c2 = new Circle(1,1,2);
Circle c3 = new Circle(3);
System.out.println(numCircles);
}
}
Klassenvariablen werden initialisiert, wenn die Klasse das erste Mal geladen wird.
Das Analogon zu Konstruktoren, um komplexe Initialisierungen von Klassenvariablen durchzuführen, sind die sogenannten Initialisierungsblöcke. Diese Blöcke
werden durch static {. . . } umschlossen, wie folgendes Beispiel demonstriert.
Beispiel 5.6.1 (Flanagan [3], S. 59)
public class Circle {
public static double[] sines = new double[1000];
public static double[] cosines = new double[1000];
68
static {
double x, delta_x;
int i;
delta_x = (Math.PI/2)/(1000-1);
for(i=0,x=0; i<1000; i++,x+=delta_x) {
sines[i] = Math.sin(x);
cosines[i] = Math.cos(x);
}
}
}
Es können mehrere klassenbezogene Initialisierungsblöcke in einer Klasse enthalten sein. Die Klasseninitialisierung erfolgt von links nach rechts und von oben
nach unten.
5.7
Java-Klassen als Realisierung und Implementierung von abstrakten Datentypen
Durch den Modifizierer private können wir Implementierungsdetails verstecken,
denn als private deklarierte Attribute und Methoden sind nur in der Klasse
selbst zugreifbar2 . Folgende Klasse implementiert einen ADT Stack mittels eines
Feldes:
public class Stack {
private Object[] stack;
private int top = -1;
private static final int CAPACITY = 10000;
/** liefert einen leeren Keller. */
public Stack() {
stack = new Object[CAPACITY];
}
/** legt ein Objekt im Keller ab und liefert dieses Objekt
zusaetzlich zurueck. */
public Object push(Object item) {
stack[++top] = item;
return item;
2
Synonyme für Zugreifbarkeit sind: Gültigkeit bzw. Sichtbarkeit.
69
}
/** entfernt das oberste Objekt vom Keller und liefert es zurueck.
Bei leerem Keller wird eine Fehlermeldung ausgegeben und
null zurueckgeliefert. */
public Object pop() {
if (empty()) {
System.out.println("Method pop: empty stack");
return null;
}
else
return stack[top--];
}
/** liefert das oberste Objekt des Kellers, ohne ihn zu veraendern.
Bei leerem Keller wird eine Fehlermeldung ausgegeben und
null zurueckgeliefert. */
public Object peek() {
if (empty()) {
System.out.println("Method peek: empty stack");
return null;
}
else
return stack[top];
}
/** liefert true genau dann, wenn der Keller leer ist. */
public boolean empty() {
return (top == -1);
}
/** liefert die Anzahl der Elemente des Kellers. */
public int size() {
return top+1;
}
}
Der Dokumentationskommentar /** ... */ wird zur automatischen Dokumentierung der Attribute und Methoden einer Klasse benutzt. Das Programm javadoc
70
generiert ein HTML-File, in dem alle sichtbaren Attribute und Methoden mit deren Parameterlisten aufgezeigt und dokumentiert sind.
> javadoc Stack.java
Dieses HTML-File ist der Vertrag (die Schnittstelle) der Klasse und entspricht
dem ADT Stack, wobei die Operationen bzw. Methoden allerdings nur natürlichsprachlich spezifiziert wurden. Die obige verbale Spezifikation entspricht weitgehend der der vordefinierten Java-Klasse Stack (genauer java.util.Stack). Man
beachte, dass (aus diesem Grund) die obige Spezifikation von der Gleichungsspezifikation aus dem Unterabschnitt 5.1.1 abweicht.
5.8
Methoden in Java
Methoden können wie Konstruktoren überladen werden. In Java besitzt jede Methode eine Signatur, die ihren Namen sowie die Anzahl und Typen der Parameter
definiert. Zwei Methoden können denselben Namen haben, wenn ihre Signaturen
unterschiedliche Anzahlen oder Typen von Parametern aufweisen; dies wird als
Überladen von Methoden bezeichnet. Wird eine Methode aufgerufen, vergleicht
der Übersetzer die Anzahl und die Typen der Parameter mit den verfügbaren
Signaturen, um die passende Methode zu finden.
Die Parameterübergabe zu Methoden erfolgt in Java durch Wertübergabe (call
by value). D.h., dass Werte von Parametervariablen in einer Methode Kopien der
vom Aufrufer angegebenen Werte sind. Das nächste Beispiel verdeutlicht dies.
public class CallByValue {
public static int sqr(int i) {
i = i*i;
return(i);
}
public static void main(String[] args) {
int i = 3;
System.out.println(sqr(i));
System.out.println(i);
}
}
> java CallByValue
9
3
71
Allerdings ist zu beachten, dass nicht Objekte, sondern Objektreferenzen übergeben werden. Wir betrachten unser Standardbeispiel Circle in folgender abgespeckter Form (gemäß der Devise, Implementierungsdetails zu verbergen, werden
die Datenfelder als private deklariert).
public class Circle {
private int x,y,r;
public Circle(int x, int y, int r) {
this.x = x;
this.y = y;
this.r = r;
}
public double circumference() {
return 2 * Math.PI * r;
}
public double area() {
return Math.PI * r * r;
}
public static void setToZero (Circle arg) {
arg.r = 0;
arg = null;
}
public static void main(String[] args) {
Circle kreis = new Circle(10,10,1);
System.out.println("vorher : r = "+kreis.r);
setToZero(kreis);
System.out.println("nachher: r = "+kreis.r);
}
}
> java Circle
vorher : r = 1
nachher: r = 0
Dieses Verhalten entspricht jedoch nicht der Parameterübergabe call by reference,
denn bei der Wertübergabe wird eine Kopie der Referenz erzeugt und die ursprüngliche Referenz bleibt erhalten. Bei call by reference würde die übergebene
72
Referenz eben nicht kopiert und daher in der Methode setToZero auf null gesetzt.
5.9
Unterklassen und Vererbung in Java
Wir wollen die Klasse Circle so erweitern, dass wir deren Instanzen auch graphisch darstellen können. Da ein solcher “graphischer Kreis” ein Kreis ist (es
herrscht eine “ist-ein” Beziehung), erweitern wir die Klasse Circle zu der neuen
Klasse GraphicCircle3 . Durch das Schlüsselwort extends wird GraphicCircle
eine Unterklasse von Circle. Wir sagen auch GraphicCircle erweitert die (Ober)Klasse Circle. Damit erbt die Klasse GraphicCircle alle Attribute und Methoden von Circle, nur die als private deklarierten sind nicht über ihren Namen
zugreifbar. Damit ist unsere Entscheidung, die Attribute x, y und r privat zu
halten, nicht mehr sinnvoll. Um diese Attribute dennoch vor unerwünschten Zugriffen zu schützen, werden sie als protected deklariert. Damit sind sie zugreifbar
für Unterklassen und werden an diese vererbt, in anderen Klassen sind sie nicht
zugreifbar4 .
import java.awt.Color;
import java.awt.Graphics;
public class GraphicCircle extends Circle {
protected Color outline;
// Farbe der Umrandung
protected Color fill;
// Farbe des Inneren
public GraphicCircle(int x,int y,int r,Color outline) {
super(x,y,r);
this.outline = outline;
this.fill = Color.lightGray;
}
public GraphicCircle(int x,int y,int r,Color outline,Color fill) {
this(x,y,r,outline);
this.fill = fill;
}
public void draw(Graphics g) {
g.setColor(outline);
3
Nur wenn eine solche “ist-ein” Beziehung herrscht, ist eine Erweiterung sinnvoll. Beispielsweise wäre eine Erweiterung der Klasse Circle zu einer Klasse Ellipse ein Design-Fehler, da
eine Ellipse kein Kreis ist. Umgekehrt wäre dieses sinniger, da ein Kreis eine Ellipse ist.
4
Es sei denn, die Klasse befindet sich im selben Paket (siehe Abschnitt 2.4.2)!
73
g.drawOval(x-r, y-r, 2*r, 2*r);
g.setColor(fill);
g.fillOval(x-r, y-r, 2*r, 2*r);
}
public static void main(String[] args) {
GraphicCircle gc = new GraphicCircle(0,0,100,Color.red,Color.blue);
double area = gc.area();
System.out.println(area);
Circle c = gc;
double circumference = c.circumference();
System.out.println(circumference);
GraphicCircle gc1 = (GraphicCircle) c;
Color color = gc1.fill;
System.out.println(color);
}
}
Color und Graphics sind vordefinierte Klassen, die durch import zugreifbar gemacht werden (vgl. Abschnitt 2.4.2). Diese Klassen werden z.B. in [3] beschrieben.
Zum Verständnis reicht es hier zu wissen, dass der erste Konstruktor den Konstruktor seiner Oberklasse aufruft (vgl. Abschnitt 5.11) und das Kreisinnere die
Farbe hellgrau erhält, sowie, dass die Methode draw einen farbigen Kreis zeichnet.
Da GraphicCircle alle Methoden von Circle erbt, können wir z.B. den Flächeninhalt eines Objektes gc vom Typ GraphicCircle berechen durch:
double area = gc.area();
Jedes Objekt gc vom Typ GraphicCircle ist ebenfalls ein Objekt vom Typ
Circle bzw. vom Typ Object. Deshalb sind folgende Zuweisungen korrekt.
Circle c = gc;
double area = c.area();
Man kann c durch casting 5 in ein Objekt vom Typ GraphicCircle zurückverwandeln.
GraphicCircle gc1 = (GraphicCircle)c;
Color color = gc1.fill;
Die oben gezeigte Typumwandlung funktioniert nur, weil c tatsächlich ein Objekt
vom Typ GraphicCircle ist.
5
explizite Typumwandlung
74
5.10
Überschreiben von Methoden und Verdecken
von Datenfeldern
Wir betrachten folgendes Java-Programm (Arnold & Gosling [1], S. 66):
public class SuperShow {
public String str = "SuperStr";
public void show() {
System.out.println("Super.show: "+str);
}
}
public class ExtendShow extends SuperShow {
public String str = "ExtendStr";
public void show() {
System.out.println("Extend.show: "+str);
}
public static void main(String[] args) {
ExtendShow ext = new ExtendShow();
SuperShow sup = ext;
sup.show();
ext.show();
System.out.println("sup.str = "+sup.str);
System.out.println("ext.str = "+ext.str);
}
}
Verdecken von Datenfeldern
Jedes ExtendShow-Objekt hat zwei String-Variablen, die beide str heißen und
von denen eine ererbt wurde. Die neue Variable str verdeckt die ererbte; wir
sagen auch die ererbte ist verborgen. Sie existiert zwar, man kann aber nicht
mehr durch Angabe ihres Namens auf sie zugreifen.
Überschreiben von Methoden
Die Methode show() der Klasse ExtendShow überschreibt die gleichnamige Methode der Oberklasse. Dies bedeutet, dass die Implementierung der Methode der
Oberklasse durch eine neue Implementierung der Unterklasse ersetzt wird. Dabei
müssen Signatur und Rückgabetyp dieselben sein. Überschreibende Methoden
75
besitzen ihre eigenen Zugriffsangaben. Eine in der Oberklasse als protected deklarierte Methode kann wieder als protected redeklariert werden6 , oder sie wird
mit dem Modifizierer public erweitert. Der Gültigkeitsbereich kann aber nicht
z.B durch private eingeschränkt werden. (Eine Begründung dafür findet man in
Arnold & Gosling [1], S. 66.)
Wenn eine Methode von einem Objekt aufgerufen wird, dann bestimmt immer
der tatsächliche Typ des Objektes, welche Implementierung benutzt wird. Bei
einem Zugriff auf ein Datenfeld wird jedoch der deklarierte Typ der Referenz
verwendet. Daher erhalten wir folgende Ausgabe beim Aufruf der main-Methode:
> java ExtendShow
Extend.show: ExtendStr
Extend.show: ExtendStr
sup.str = SuperStr
ext.str = ExtendStr
Die Objektreferenz super
Das Schlüsselwort super kann in allen objektbezogenen Methoden und Konstruktoren verwendet werden. In Datenfeldzugriffen und Methodenaufrufen stellt es
eine Referenz zum aktuellen Objekt als eine Instanz seiner Oberklasse dar. Wenn
super verwendet wird, so bestimmt der Typ der Referenz über die Auswahl
der zu verwendenden Methodenimplementierung. Wir illustrieren dies wieder an
einem Beispielprogramm.
public class T1 {
protected int x = 1;
protected String s() {
return "T1";
}
}
public class T2 extends T1 {
protected int x = 2;
protected String s() {
return "T2";
}
protected void test() {
System.out.println("x= "+x);
System.out.println("super.x= "+super.x);
System.out.println("((T1)this).x= "+((T1)this).x);
6
Dies ist die übliche Vorgehensweise.
76
System.out.println("s(): "+s());
System.out.println("super.s(): "+super.s());
System.out.println("((T1)this).s(): "+((T1)this).s());
}
public static void main(String[] args) {
new T2().test();
}
}
> java T2
x= 2
super.x= 1
((T1)this).x= 1
s(): T2
super.s(): T1
((T1)this).s(): T2
5.11
Konstruktoren in Unterklassen
In Konstruktoren der Unterklasse kann direkt einer der Oberklassenkonstruktoren
mittels des super() Konstruktes aufgerufen werden.
Achtung: Der super-Aufruf muss die erste Anweisung des Konstruktors sein!
Wird kein Oberklassenkonstruktor explizit aufgerufen, so wird der parameterlose
Konstruktor der Oberklasse automatisch aufgerufen, bevor die Anweisungen des
neuen Konstruktors ausgeführt werden. Verfügt die Oberklasse nicht über einen
parameterlosen Konstruktor, so muss ein Konstruktor der Oberklasse explizit
mit Parametern aufgerufen werden, da es sonst einen Fehler bei der Übersetzung
gibt.
Ausnahme: Wird in der ersten Anweisung eines Konstruktors ein anderer Konstruktor derselben Klasse mittels this aufgerufen, so wird nicht automatisch der parameterlose Oberklassenkonstruktor aufgerufen.
Java liefert einen voreingestellten parameterlosen Konstruktor für eine erweiternde Klasse, die keinen Konstruktor enthält. Dieser ist äquivalent zu:
public class ExtendedClass extends SimpleClass {
public ExtendedClass () {
super();
}
}
77
Der voreingestellte Konstruktor hat dieselbe Sichtbarkeit wie seine Klasse.
Ausnahme: Enthält die Oberklasse keinen parameterlosen Konstruktor, so muss
die Unterklasse mindestens einen Konstruktor bereitstellen.
5.12
Reihenfolgeabhängigkeit von Konstruktoren
Wird ein Objekt erzeugt, so werden zuerst alle seine Datenfelder auf voreingestellte Werte initialisiert. Jeder Konstruktor durchläuft dann drei Phasen:
• Aufruf des Konstruktors der Oberklasse.
• Initialisierung der Datenfelder mittels der Initialisierungsausdrücke.
• Ausführung des Rumpfes des Konstruktors.
Beispiel 5.12.1
public class X {
protected String infix = "fel";
protected String suffix;
protected String alles;
public X() {
suffix = infix;
alles = verbinde("Ap");
}
public String verbinde(String original) {
return (original+suffix);
}
}
public class Y extends X {
protected String extra = "d";
public Y() {
suffix = suffix+extra;
alles = verbinde("Biele");
}
public static void main(String[] args) {
78
new Y();
}
}
Die Reihenfolge der Phasen ist ein wichtiger Punkt, wenn während des Aufbaus
Methoden aufgerufen werden (wie im obigen Beispiel). Wenn man eine Methode
aufruft, erhält man immer die Implementierung dieser Methode für den derzeitigen Objekttyp. Verwendet die Methode Datenfelder des derzeitigen Typs, dann
sind diese vielleicht noch nicht initialisiert worden. Die folgende Tabelle zeigt die
Inhalte der Datenfelder beim Aufruf der main-Methode (d.h. des Y-Konstruktors).
Schritt
0
1
2
3
4
5
6
Aktion
infix extra suffix alles
Datenfelder auf Voreinstellungen
Y-Konstruktor aufgerufen
X-Konstruktor aufgerufen
X-Datenfeld initialisiert
fel
X-Konstruktor ausgeführt
fel
fel
Apfel
Y-Datenfeld initialisiert
fel
d
fel
Apfel
Y-Konstruktor ausgeführt
fel
d
feld
Bielefeld
Die während des Objektaufbaus aufgerufenen Methoden sollten unter Beachtung
dieser Faktoren entworfen werden. Auch sollte man alle vom Konstruktor aufgerufenen Methoden sorgfältig dokumentieren, um diejenigen, die den Konstruktor
überschreiben möchten, von den potentiellen Einschränkungen in Kenntnis zu
setzen.
5.13
Abstrakte Klassen und Methoden
Ein sehr nützliches Merkmal der objektorientierten Programmierung ist das der
abstrakten Klasse. Mittels abstrakter Klassen können Klassen deklariert werden,
die nur einen Teil der Implementierung definieren und erweiternden Klassen die
spezifische Implementierung einiger oder aller Methoden überlassen. Abstraktion
ist hilfreich, wenn Teile des Verhaltens für alle oder die meisten Objekte eines
gegebenen Typs richtig sind, es aber auch Verhalten gibt, das nur für bestimmte
Objekte sinnvoll ist und nicht für alle. Es gilt:
• eine abstrakte Methode hat keinen Rumpf;
• jede Klasse, die eine abstrakte Methode enthält, ist selbst abstrakt und
muss als solche gekennzeichnet werden;
• jede abstrakte Klasse muss mindestens eine abstrakte Methode besitzen;
79
• man kann von einer abstrakten Klasse keine Objekte erzeugen;
• von einer Unterklasse einer abstrakten Klasse kann man Objekte erzeugen
– vorausgesetzt sie überschreibt alle abstrakten Methoden der Oberklasse
und implementiert diese;
• eine Unterklasse, die nicht alle abstrakten Methoden der Oberklasse implementiert ist selbst wieder abstrakt.
Beispiel 5.13.1 (vgl. Arnold & Gosling [1], S. 72 ff.)
Wir wollen ein Programm zur Bewertung von Programm(teilen) schreiben. Unsere
Implementierung weiß, wie eine Bewertung gefahren und gemessen wird, aber sie
kann nicht im voraus wissen, welches andere Programm bewertet werden soll.
Die meisten abstrakten Klassen entsprechen diesem Muster: eine Klasse ist zwar
Experte in einem Bereich, doch ein fehlendes Stück kommt aus einer anderen
Klasse. In unserem Beispiel ist das fehlende Stück ein Code, der bewertet werden
muss. Eine solche Klasse könnte wie folgt aussehen:
public abstract class Benchmark {
public abstract void benchmark();
public long repeat(int count) {
long start = System.currentTimeMillis();
for(int i=0; i<count; i++)
benchmark();
return (System.currentTimeMillis()-start);
}
}
Die Klasse ist als abstract deklariert, weil eine Klasse mit abstrakten Methoden
selbst als abstract deklariert werden muss. Diese Redundanz hilft dem Leser,
schnell zu erfassen, dass die Klasse abstrakt ist, ohne alle Methoden der Klasse
durchzusehen, ob zumindest eine von ihnen abstrakt ist.
Die Methode repeat stellt das Sachwissen zur Bewertung bereit. Sie weiß, wie der
Zeitbedarf für die Ausführung von count Aufrufen des zu bewertenden Codes zu
messen ist. Wird die Messung komplizierter (vielleicht durch Messung der Zeiten
jeder Ausführung und Berechnung der Varianz als statistisches Maß darüber), so
kann diese Methode verbessert werden, ohne die Implementierung des speziellen
zu bewertenden Codes in einer erweiternden Klasse zu beeinflussen.
Die abstrakte Methode benchmark muss von jeder selbst nicht wieder abstrakten
Unterklasse implementiert werden. Deshalb gibt es in dieser Klasse keine Implementierung, sondern nur eine Deklaration. Hier nun ein Beispiel einer einfachen
Erweiterung von Benchmark:
80
public class MethodBenchmark extends Benchmark {
public void benchmark() { }
public static void main(String[] args) {
int count = Integer.parseInt(args[0]);
long time = new MethodBenchmark().repeat(count);
System.out.println(count+" Methodenaufrufe in "+time+
" Millisekunden");
}
}
Die Implementierung von benchmark ist denkbar einfach: die Methode hat einen
leeren Rumpf. Man kann daher den Zeitbedarf von n Methodenaufrufen feststellen, indem man die main-Methode der Klasse MethodBenchmark mit der Angabe
n der gewünschten Testwiederholungen laufen lässt.
5.14
Aufgaben
Aufgabe 5.14.1 Eine Folge heißt Schlange (engl. queue), wenn Elemente eines
gegebenen Datentyps T nur am Ende eingefügt und am Anfang entfernt werden
dürfen (FIFO-Prinzip: first in first out).
In Analogie zum abstrakten Datentypen Stack sollen Sie hier einen abstrakten
Datentypen Queue spezifizieren, der folgende Operationen enthält:
initQueue: Erzeugen einer leeren Schlange.
enqueue: Einfügeoperation.
dequeue: Entfernt das vorderste Element.
peek: Liefert das vorderste Element der Schlange, ohne die Schlange zu verändern.
empty: Liefert true gdw. die Schlange leer ist.
Die Operationen sind durch Gleichungen zu spezifizieren.
Hinweis: Fallunterscheidungen über Schlangen mit nur einem Element und Schlangen mit mindestens zwei Elementen sind hilfreich.
Aufgabe 5.14.2 Implementieren Sie eine Klasse Rent in Java, die die Klasse
Stack benutzt. Nehmen Sie an, Herr Meier ist Besitzer eines Buches. Herr Meier,
der Eigentümer, wird im ersten Eintrag des Stapels beschrieben. Leiht jemand
das Buch aus, vielleicht Herr Schmidt, wird dessen Name auf dem Stapel abgelegt. Verleiht Herr Schmidt es wiederum weiter, z.B. an Herrn Müller, erscheint
dessen Name an der Spitze des Stapels, usw. Wird das Buch an seinen Vorgänger
zurückgegeben, wird der Name des Entleihers vom Stapel entfernt. Z.B. wird der
Name Müller vom Stapel entfernt, wenn er das Buch Herrn Schmidt zurückgibt.
Der letzte Name wird nie aus dem Stapel entfernt, denn sonst ginge die Informa81
tion über den Bucheigentümer verloren.
Hinweis: Die Klassen Stack und Rent müssen sich im selben Verzeichnis befinden.
Aufgabe 5.14.3 (a) Harry Hacker hat wieder einmal programmiert, ohne genau
nachzudenken. Er wollte mit der folgenden Methode einen Stack kopieren (d.h.
einen neuen Stack mit den gleichen Werten kreieren):
public static Stack copy(Stack stack) {
Stack cpStack = stack;
return cpStack;
}
Was hat Harry nicht bedacht? Und wie kann man Harry helfen? Schreiben Sie in
Java eine Methode betterCopy, die den ursprünglichen Gedanken von Harry erfüllt. Ergänzen Sie ebenfalls eine main-Methode, in der die beiden copy-Methoden
aufgerufen werden, so dass der Unterschied deutlich wird.
(b) Implementieren Sie statt der klassenbezogenen Methode betterCopy eine objektbezogene Methode gleichen Namens, die dasselbe leistet.
Aufgabe 5.14.4 Objektorientierte Programmierung ermöglicht eine relativ einfache Modellierung von Ausschnitten der realen Welt. In dieser Aufgabe sollen Sie
eine Klasse Vehicle implementieren, die zwei Unterklassen enthält: (i) motorgetriebene Fahrzeuge (Motorrad, Auto, Bus, LKW, ...) und (ii) personengetriebene
Fahrzeuge (Fahrrad, Tretroller, Inliner, ...). Diese Klassen sollen wiederum Unterklassen besitzen. Z.B. kann man die motorgetriebenen Fahrzeuge in zweirädrige,
vierrädrige und mehr-als-vierrädrige Fahrzeuge unterteilen. Modellieren Sie Fahrzeuge in sinnvoller Klassenhierarchie. Obligatorisch sind folgende Attribute und
Methoden:
(a) Die Klasse Vehicle sollte mindestens Datenfelder für die aktuelle Geschwindigkeit, die aktuelle Richtung in Grad, den Preis und den Besitzernamen enthalten.
(b) Eine Klasse EnginePoweredVehicle soll mindestens Datenfelder über die Leistung in kW, Front- oder Heckantrieb und Höchstgeschwindigkeit besitzen.
(c) Die Klasse PersonPoweredVehicle soll mindestens ein Datenfeld besitzen,
das Auskunft über die Anzahl der Personen gibt, die das Fahrzeug antreiben.
(d) Es sollen Klassen Car, Bus, Truck, Bike, Motorbike, Inliner und Scooter
geben. Alle besitzen ein Datenfeld für eine eindeutige Identifikationsnummer.
(e) Schreiben Sie Methoden, die die einzelnen Eigenschaften verändern könnnen. Z.B. sollte die Klasse EnginePoweredVehicle eine Methode besitzen, die
es ermöglicht, die Höchstgeschwindigkeit zu setzen (verändern). Jede Klasse soll
mindestens zwei Methoden enthalten!
(f) Schreiben Sie schließlich eine Klasse SomeVehicles mit einer main-Methode,
die sechs Fahrzeuge konstruiert. Darauf sollen jeweils mindestens zwei Methoden
angewendet werden.
82
(g) Wenn diese Aufgabe Sie unterfordert, brechen Sie die Übung ab. Hauptsache,
Sie haben das Prinzip verstanden.
Aufgabe 5.14.5 Schreiben Sie eine Klasse LinkedList, die ein Datenfeld vom
Typ Object und eine Referenz zum nächsten LinkedList-Element in der Liste
enthält. Schreiben Sie zusätzlich für Ihre Klasse LinkedList eine main-Methode,
die einige Objekte vom Typ Vehicle erzeugt und sie aufeinanderfolgend in die
Liste einfügt. Können Sie mit Ihrer Implementierung eine leere Liste erzeugen?
Aufgabe 5.14.6 Sie haben schon die Klasse Circle kennengelernt, die drei Datenfelder besaß: die Koordinaten x und y, die den Mittelpunkt eines Kreises
angeben, und eine Variable r, die den Radius enthält.
(a) Schreiben Sie analog dazu ein Klasse Rectangle, die vier Datenfelder besitzt.
Je zwei Koordinaten x1 und y1, sowie x2 und y2, beschreiben die Endpunkte der
Diagonalen eines Rechtecks, das damit vollständig beschrieben ist.
(b) Schreiben Sie eine abstrakte Klasse Shape, die die beiden abstrakten Methoden area (berechnet den Inhalt eines geometrischen Objekts) und circumference
(berechnet den Umfang eines geometrischen Objekts) beinhaltet.
(c) Die Klassen Circle und Rectangle sollen als erweiternde Klassen von Shape
implementiert werden.
(d) Schreiben Sie dann noch eine main Methode, in der ein Array von ShapeObjekten der Länge 5 konstruiert wird, das Circle- und/oder Rectangle-Objekte
enthalten kann. Dann soll das Array mit 5 entsprechenden Objekten gefüllt werden und der Gesamtumfang bzw. der Gesamtflächeninhalt aller Objekte ausgegeben werden.
83
84
Herunterladen