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