180 Kapitel 7 Abstraktion von Methoden und Daten 7.1 Funktionale (Prozedurale) Abstraktion Funktionale Abstraktion erlaubt die “Auslagerung” häufig auftretender ähnlicher oder gleicher Programmteile auf eigene “Untereinheiten” des Hauptprogramms. Diese Untereinheiten oder Unterprogramme existieren in allen Programmiersprachen unter verschiedenen Namen: procedures, functions, subroutines. In Java sind alle Unterprogramme Funktionen, egal ob sie Werte zurückgeben oder nicht. Die Java-interne Bezeichnung für Funktion ist Methode. Funktionen sind ein Werkzeug zur Abstraktion, da man ihr Input-Output Verhalten (was tut die Funktion) von ihrer Implementation (wie tut sie es) trennen kann. Sie bilden daher ein Werkzeug sowohl für den Algorithmenentwurf (Aufteilung des Algorithmus in “kleine” Einheiten die alle Funktionen sind) als auch für die Schaffung wiederverwendbarer Software (gut implementierte Funktionen können in unterschiedlichen Aufgabenbereichen eingesetzt werden). In beiden Fällen ist alles, was der Programmierer braucht, die Spezifikation der Funktion (d. h. eine Beschreibung dessen, was die Funktion tut). Die Implementation selbst ist für ihn irrelevant, sofern sie die Spezifikation erfüllt. √ Beispiele sind mathematische Funktionen wie sqrt(x) (berechnet x) oder pow(x,n) (berechnet xn ), Funktionen zur Handhabung von Strings und viele andere mehr. In allen Fällen interessiert nur das Verhalten, aber nicht die Implementation. 7.1.1 Funktionen und Prozeduren Funktionen fallen in zwei Kategorien, solche die einen einzelnen Funktionswert zurückgeben (in Pascal “functions”) und solche, die keinen Wert zurückgeben (in Pascal “procedures”). In Java hat jede Funktion einen Rückgabetyp, der in der Definition der Funktion angegeben werden Version vom 6. Januar 2005 181 182 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN muss. Die allgemeine Definition hat die Form1 Rückgabetyp FunktionsName(formale Parameterliste ) { Funktionsrumpf } Dabei gelten folgende semantische Regeln: • Ist der Rückgabetyp void, so wird kein Wert zurückgegeben (wie bei einer Pascal Prozedur). • Ist der Rückgabetyp verschieden von void, so wird pro Aufruf genau ein Wert vom Rückgabetyp mit einer return Anweisung im Rumpf zurückgegeben. 2 • Als Rückgabetyp sind alle Typen erlaubt. • Die formale Parameterliste ist optional. Falls vorhanden, so besteht sie aus einer durch Kommas getrennten Folge De f1 , De f2 , . . . , De fk von Variablendeklarationen ohne Initialisierungen. Jede Definition De fi definiert genau eine Variable. Der Aufruf einer Methode erfolgt mit Werten“ für die formalen Parameter. Diese Werte werden aktu” elle Parameter oder Aufrufparameter genannt. Sie müssen natürlich typverträglich mit den formalen Parametern sein und auch in derselben Anzahl und Reihenfolge auftreten. 7.1.2 Parameter und Datenfluss Der Datenfluss bezeichnet die Art des Datenaustausches zwischen der Funktion und dem sie aufrufenden Programm. Für jeden Parameter in der formalen Parameterliste gibt es dabei drei Möglichkeiten • Fluss nur in die Funktion, • Fluss nur aus der Funktion heraus, • Fluss sowohl in die Funktion, als auch aus der Funktion heraus. Die Arten des Datenflusses sind geeignet zu kommentieren (zum Beispiel mit den @param und @return Verweisen in den javadoc Kommentaren). Zur Realisierung dieses Datenflusses stehen in Programmiersprachen verschiedene Methoden zur Parameterübergabe zur Verfügung: • Call by value (Wertparameter). 1 Modifizierer wie public, private usw. werden in Abschnitt 7.3.5 behandelt. Ausnahme: Eine throw Anweisung zur Erzeugung einer Exception beendet die Abarbeitung einer Funktion und gibt eine Exception zurück. 2 Einzige 183 7.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION • Call by reference (Variabler Parameter). Call by value (Wertparameter): Eine Kopie des aktuellen Parameters wird beim Aufruf an die Funktion übergeben. Die Funktion arbeitet mit der Kopie und ändert den aktuellen Parameter des aufrufenden Programms nicht. Beispiel 7.1 (Berechnung der Fakultät) Für eine natürliche Zahl n > 0 ist n! := 1 · 2 · 3 · . . . · n zu berechnen. Dies leistet die folgende Java Funktion. int factorial (int n) { int product = 1; for (int i = 2; i <= n; i++) product *= i; return product; } factorial ist also eine Funktion mit einem Wertparameter n, die einen Wert zurück gibt. Schematisch ist dies in Abbildung 7.1 dargestellt. Der Datenfluss erfolgt nur in die Funktion über den Wertparameter n. Eine solche Funktion entspricht am ehesten der in der Mathematik üblichen Vorstellung einer Funktion. ( n by value ) Funktionswert ? Abbildung 7.1: Datenfluss bei der Funktion factorial. Das aufrufende Programm kann diese Funktion in beliebigen Ausdrücken verwenden, z. B. in x = factorial(5*a) + b; Beim Aufruf wird 5*a berechnet und dem Parameter n zugewiesen, der im Rumpf von factorial wie eine Variable verwendet wird. Die return Anweisung gibt den Funktionswert zurück, und dieser wird der Variablen x zugewiesen. Call by reference (Variabler Parameter): Die Adresse des aktuellen Parameters wird beim Aufruf an die Funktion übergeben. Die Funktion arbeitet im Rumpf auf dem Speicherplatz des aktuellen Parameters und kann (aber muss nicht) diesen dadurch modifizieren. 184 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN Diese Art der Parameterübergabe ist in Java nicht möglich (aber z. B. in Pascal und C++). In Java werden grundsätzlich alle Parameter durch call by value übergeben. Da jedoch alle Datentypen außer den elementaren Referenztypen sind, lässt sich der call by reference durch einen call by value mit einem Referenztyp weitgehend simulieren.3 Für den Datenfluss nur aus der Funktion heraus betrachten wir folgendes Beispiel. Beispiel 7.2 (Initialisierung eines Arrays) Ein Array ist mit den ersten Quadratzahlen zu initialisieren. Dies leistet folgende Funktion void initializeWithSquares(int[] vec) { for (int i = 0; i < vec.length; i++) { vec[i] = i*i; } } Die Anweisungen int[] myArray = new int[7]; initializeWithSquares(myArray); System.out.println(myArray[3]); bewirken die Initialisierung des Arrays myArray mit 0, 1, 4, 9, 16, 25, 36, 49. Die Zahl 9 wird auf die Konsole geschrieben. Die Funktion gibt keinen Wert zurück. Schematisch ist dies in Abbildung 7.2 dargestellt. Der Datenfluss erfolgt nur aus der Funktion. 4 Als Variante hiervon betrachten wir eine Funktion, die zu gegebener Zahl n ein Array der ersten n Quadratzahlen erzeugt und (die Referenz auf) das erzeugte Array als Funktionswert zurück gibt. int[] squareNumbers(int n) { int[] vec = new int[n]; for (int i = 0; i < vec.length; i++) { vec[i] = i*i; } return vec; } 3 Gelegentlich wird dies fälschlicherweise als call by reference bezeichnet. Ein call by reference beinhaltet jedoch eine automatische Dereferenzierung, daher sind als Parameter nur lvalues (z. B. Variablennamen) erlaubt. Beim call by value können jedoch rvalues als aktuelle Parameter (z. B. Ausdrücke) übergeben werden, und genau dies geschieht in der Anweisung initializeWithSquares(squareNumbers(n)); mit der Funktion squareNumbers() von Seite 184. 4 Zumindest im Wesentlichen. Natürlich fließt die Referenz auf das Array myArray und über myArray.length auch die Zahl der Komponenten als Information in die Funktion. Aber die Werte der Komponenten von myArray sind unerheblich. 185 7.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION 6 ( vec ) Abbildung 7.2: Datenfluss bei der Funktion initializeWithSquares(). Der Funktionswert kann dann in Ausdrücken der Form int[] myArray = squareNumbers(8); verwendet werden, wodurch der Arrayvariablen myArray das Array der ersten 8 Quadratzahlen (genauer: die Referenz des in der Funktion squareNumbers() erzeugten Arrays der ersten 8 Quadratzahlen) zugewiesen wird. Der zugehörige Datenfluss ist in Abbildung 7.3 dargestellt. ( n ) Array Referenz ? Abbildung 7.3: Datenfluss bei der Funktion squareNumbers(). Als Beispiel für den Datenfluss in eine und aus einer Funktion betrachten wir die Addition zweier Vektoren. Beispiel 7.3 (Addition von Vektoren) Zwei Arrays der Länge n sollen komponentenweise addiert werden und in einem Ergebnisarray zurückgegeben werden. Dies leistet folgende Funktion /** * Adds array a to array b and stores the result in c * PRE: All arrays have the same length */ void arrayAdd(int[] a, int[] b, int[] c) { for (int i = 0; i < a.length; i++) { c[i] = a[i] + b[i]; 186 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN } } Die Anweisungen int[] vec1 = {1, 2, 3, 4}, vec2 = {4, 3, 2, 1}, sum = new int[4]; arrayAdd(vec1, vec2, sum); bewirken dann, dass sum == {5, 5, 5, 5} gilt. Der Datenfluss der Funktion ist in Abbildung 7.4 dargestellt. 6 ( a b ? ? c ) Abbildung 7.4: Datenfluss bei der Funktion arrayAdd(). Natürlich wäre es analog zur Funktion squareNumbers auch möglich gewesen, das Ergebnis als Funktionswert zurückzugeben. Die Rückgabe interessierender Größen als Funktionswert ist prinzipiell immer möglich, da man eigene Klassen für die interessierenden Größen definieren kann und eine Referenz auf ein Objekt dieser Klasse zurückgeben kann. Dies illustriert das folgende Beispiel. Beispiel 7.4 (Minimale und maximale Komponente eines Arrays) In einem Array von ganzen Zahlen sollen der minimale und der maximale Wert ermittelt und zurückgegeben werden. Um 2 Werte zurückgeben zu können, definieren wir eine entsprechende Klasse IntPair. public class IntPair{ public int first; public int second; public IntPair(int x, int y) { // constructor first = x; second = y; } } Der zu dieser Klasse gehörige Datentyp wird in der Funktion arrayMinMax als Rückgabetyp benutzt. 7.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION 187 IntPair arrayMinMax(int[] vec){ int min, max; min = max = vec[0]; for (int i = 0; i < vec.length; i++) { if (vec[i] < min) { min = vec[i]; } if (vec[i] > max) { max = vec[i]; } } IntPair pair = new IntPair(min, max); return pair; } Die Anweisungen int[] vector = { 10, 20, 3, 17, 9 }; IntPair xy = arrayMinMax(vector); im aufrufenden Programmteil bewirken dann, dass xy.first == 3 und xy.second == 20 gilt. 7.1.3 Gültigkeitsbereiche von Identifiern (Scope) In Unterprogrammen können Identifier verwendet werden, die auch in anderen Programmteilen oder Unterprogrammen auftreten. Dies geschieht zwangsläufig, wenn Programme von mehreren Personen entwickelt werden oder Fremdsoftware benutzt wird. Jede Programmiersprache braucht daher Regeln, die festlegen, welcher Identifier wann gemeint ist, und wie lange ein ihm eventuell zugeordneter Speicherplatz mit dem Identifier angesprochen wird. In Java werden solche Gültigkeitsbereiche oder Scopes (wie auch in Pascal) durch den Programmtext festgelegt. Man spricht daher auch von statischen Scoperegeln.5 Man unterscheidet in Java zwischen Klassenscope (class scope) und Blockscope (block scope). Der Klassenscope ist der Bereich zwischen den Klammern {...}, die den Programmtext der Klasse begrenzen. In ihm sind alle Identifier von class members, also Variablen (Feldern) und Funktionen (Methoden) der Klasse bekannt, und zwar unabhängig davon, wo sie in der Klasse definiert werden.6 Identifier mit Klassenscope sind außerdem in allen Unterklassen der Klasse, in der sie deklariert werden, bekannt. Der Blockscope wird durch die Blöcke definiert. Dies sind strukturierte Anweisungen einschließlich durch {...} geklammerter Programmteile als compound statement. Stellt man sich alle Blöcke als mit 5 Andere Programmiersprachen wie LISP oder APL verwenden dynamische Scoperegeln, bei der die Hierarchie der Aufrufe zur Laufzeit festlegt, welcher Identifier gemeint ist. 6 Man benötigt also keine forward-Deklaration wie in Pascal oder Funktionsprototypen wie in C++. 188 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN {...} geklammert vor, so bilden die Blöcke in einem korrekten Programm einen korrekten Klammerausdruck. Also liegen wegen Satz 4.1 je 2 Blöcke entweder disjunkt hintereinander im Programmtext, oder einer ist vollständig im anderen enthalten. {...} ... {...} | {z } | {z } Block 1 Block 2 bzw. {. . . { . . . } . . . } | {z } Block 1 {z } | Block 2 Der Scope eines Identifiers, der innerhalb eines Blocks definiert wurde (man nennt das lokal definiert), ist der gesamte Block ab der Definition. { . . . { . . . De f . . . { . . . { . . . } . . . } . . . { . . . } . . . } . . . } | {z } Scope von Def Identifier (einer Klasse oder eines Blockes) bleiben in allen in der Klasse oder dem Block direkt oder indirekt enthaltenen Blöcke gültig, sofern keine Überdeckung durch Neudefinition in einem “tieferen” Block auftritt. Neudefinition eines Identifiers (mit völlig anderer Bedeutung!) in anderen Blöcken ist beschränkt möglich. Erfolgt die Neudefinition in einem Block B1 , der innerhalb eines Blocks B2 liegt, in dem der Identifier bereits definiert war, so tritt Überdeckung auf. Der Scope der ersten, “äußeren” Definition wird vom Scope der zweiten, “inneren” Definition überdeckt. { . . . { . . . De f . . . { . . . Neude f . . . { . . . } . . . } . . . { . . . } . . . } . . . } | {z }| {z } | {z } Def Neudef Def Eine solche Überdeckung durch Neudefinition ist in Java nur für Identifier mit Klassenscope möglich (also Klassenvariable oder Funktionen), nicht jedoch für Identifier mit Blockscope7 . Man benötigt die Neudefinition bei der Vererbung, muss sie also bei Identifiern mit Klassenscope erlauben. Ansonsten wird Überdeckung durch Neudefinition in Java jedoch verboten und führt zu einem Compiler-Fehler. Formale Parameter in Funktionsdefinitionen haben als Scope den gesamten äußersten Block der Funktion. Sie unterliegen den Blockscope Regeln. Funktionen können (im Gegensatz zu Pascal) nicht geschachtelt werden. Allerdings ist es möglich, Klassen zu schachteln (vgl. Abschnitt 7.3.3). Das Programmfragment for (int i = 0; i < n; i++) { ... } while (i < 10) { i++; ... } 7 im Gegensatz zu vielen anderen Programmiersprachen wie C++ oder Pascal. 7.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION 189 ist also nur korrekt, wenn i eine Klassenvariable ist, während das Fragment for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) { ... } } falsch ist, da i in der ersten for-Schleife durch i in der zweiten for-Schleife unzulässig überdeckt wird. Dagegen ist { ... { ... int a; ... } ... double a; ... } erlaubt, da die int-Variable a am Ende ihres Blockes ihre Gültigkeit verliert, also nicht durch die double-Variable a überdeckt wird. Das folgende Beispiel demonstriert den Unterschied zwischen statischen und dynamischen Scoperegeln. Beispiel 7.5 (Statische versus dynamische Scoperegeln) Im Programmfragment public class Test extends Applet { // other variables int a; void P() { System.out.println(a); } void Q() { double a; a = 3.14; P(); } public void init() { a = 1; Q(); } } wird der Identifier a zweimal definiert, als Klassenvariable vom Typ int, und als lokale double Variable in Q(). Die lokale Neudefinitionen überdeckt die Klassenvariable a innerhalb des Blockes von Q(). 190 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN In init() wird der Klassenvariablen a der Wert 1 zugewiesen. Der Aufruf von P() innerhalb von Q() bezieht sich ebenfalls auf die Klassenvariable a, da sie im Block von P() Gültigkeit hat. Das Programm gibt also 1 aus. Bei Verwendung der dynamischen Scoperegeln (LISP, APL) würde der Scope aus der Aufrufhierarchie ermittelt. init() ruft Q() auf, und Q() ruft P() auf. Daher würde P() auf die in Q() definierte double-Variable a zugreifen und 3.14 ausgeben. Aus den Scope Regeln ergibt sich, dass jede Funktion auf Klassenvariable (auch globale Variable genannt) zugreifen kann und ihre Werte verwenden bzw. ändern kann. Dies stellt eine zusätzliche Form des Datenflusses dar (neben Parametern und Funktionswert). Da diese Art des Datenflusses nicht aus der Parameterliste ersichtlich ist, sollte die Verwendung globaler Variablen stets gut dokumentiert werden, sofern sie nach außen public sind. Funktionen werden außer durch Namen auch durch ihre Parameterlisten unterschieden (aber nicht durch den Rückgabetyp!). Es können also in einem Scopebereich mehrere Funktionen den gleichen Namen haben, sofern sie sich in ihren Parameterlisten (Anzahl, Typ, Reihenfolge der Typen) unterscheiden. 7.1.4 Abarbeitung von Funktionsaufrufen Der Aufruf einer Funktion erfolgt mit den sogenannten aktuellen Parametern, die mit den in der Definition der Funktion aufgeführten formalen Parametern typkompatibel sein müssen. Bei Wertparametern darf der aktuelle Parameter ein Ausdruck sein, bei variablen Parametern muss es eine Variable sein. Beim Aufruf der Funktion werden Speicherplätze für die formalen Parameter angelegt, die unter den Namen dieser Parameter im Rumpf der Funktion angesprochen werden. Wertparameter werden ausgewertet und ihr Wert in den Speicherplatz des zugehörigen formalen Parameters kopiert. Bei variablen Parametern wird die Referenz (Adresse) der übergebenen Variablen ermittelt und im Speicherplatz des zugehörigen formalen Parameters abgelegt. Im Rumpf arbeitet man dann bei Nennung dieses Parameters stets auf dem Speicherplatz der übergebenen Variablen. Da es in Java nur Wertparameter gibt, kann zwar eine Referenz auf ein Objekt als Wert übergeben werden, aber man arbeitet nicht automatisch auf dem Speicherplatz des übergebenen Objektes. Das Objekt kann natürlich verändert werden, aber dazu benötigt man die für das Objekt verfügbaren Methoden. Durch ein return-Statement wird ein Wert zurückgegeben und die Abarbeitung der Funktion beendet. Ansonsten (bei void-Funktionen) endet die Abarbeitung der Funktion mit der Ausführung des Rumpfes oder einer return Anweisung ohne Rückgabe eines Wertes. Nach Abarbeitung der Funktion werden alle Speicherplätze für formale Parameter, lokale Variablen usw. gelöscht, und im aufrufenden Programm wird an der Stelle nach dem Aufruf der Funktion weitergemacht. Da bei variablen Parametern auf dem Speicherplatz des aktuellen Parameters gearbeitet wurde, bleiben im Funktionsrumpf vorgenommene Änderungen erhalten. 191 7.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION Beispiel 7.6 (Fortsetzung von Beispiel 7.3) Betrachte den Aufruf arrayAdd(vec1, vec2, sum) auf Seite 185. Vor dem Aufruf ist die Situation im Speicher wie folgt (wobei Referenzen als Pfeile dargestellt werden). - 1 vec1 2 3 r 4 - 4 vec2 3 2 r - 0 1 sum 0 0 0 - 0 0 6 0 0 r Unmittelbar nach der Parameterübergabe ergibt sich folgendes Bild im Speicher. vec1 r a r - 1 2 6 3 4 vec2 r b r - 4 3 6 2 1 sum r c r Bei den Zuweisungen c[i] = a[i] + b [i] wird also das Array sum verändert. Nach Abarbeitung der Funktion werden die für die formalen Parameter angelegten Speicherplätze wieder freigegeben und man erhält folgende Situation im Speicher. - 1 vec1 r 2 3 4 vec2 - 4 r 3 2 - 5 1 sum 5 5 5 r Wird bei der Abarbeitung einer Funktion ein neues Objekt mit new erzeugt, so steht dieses auch nach Abarbeitung der Funktion zur Verfügung (sofern die Referenz auf diese Objekte im aufrufenden Programmsegment noch bekannt ist). Dies geschieht z. B. beim Aufruf der Funktion squareNumbers() in int[] myArray = squareNumbers(8); wobei das im Funktionsrumpf erzeugte Array über die Zuweisung der zurückgegebenen Referenz jetzt über die Arrayreferenzvariable myArray ansprechbar ist. Dies liegt daran, dass durch new erzeugter Speicherplatz in einem gesonderten Bereich (dem sogenannten Heap) angelegt wird, der getrennt von dem Bereich ist, in dem lokale Variable von Funktionen angelegt werden (dem sogenannten Stack). 192 7.1.5 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN Der Run-Time-Stack Wir sehen uns jetzt die Organisation der Speicherplatzverwaltung beim Ein- und Austritt in Scopeblöcke etwas genauer an. Jeder Scopeblock hat zur Laufzeit ein sogenanntes Environment in Form eines Activation Record mit 1. Einträgen für lokale Identifier (inklusive formale Parameter bei Funktionen), 2. Pointern auf class-Identifier bzw. Identifier aus übergeordneten Blöcken, die nicht im momentanen Block neu definiert werden, 3. der Adresse der Anweisung im übergeordneten Block, mit der nach Verlassen des Blocks weitergemacht wird (Rücksprungadresse). Beim Eintritt in den Scopeblock werden diese Records mit den entsprechenden Einträgen auf einem Stack, dem sogenannten Run-Time-Stack, abgelegt. Die Adressen innerhalb eines Activation Records ergeben sich dann durch die Anfangsadresse des Records plus dem jeweiligen Offset innerhalb des Records, der zur Compilierzeit bekannt ist. Zur Einrichtung der Pointer auf Identifier aus übergeordneten Blocks gibt es mehrere Möglichkeiten. Eine gängige besteht in der Einrichtung eines Zeigers (static pointer), der auf den Record des nächsten Scopeblocks in der statischen Hierarchie zeigt. Dadurch kann der definierende Scopeblock eines Identifiers über eine Kette von Pointern erreicht werden. Neben diesem Run-Time-Stack ist zur Analyse der Aufrufe von Funktionen bzw. des Ein- und Austritts in Scopeblöcke der sogenannte Aufrufbaum von Bedeutung, der die Aufrufhierarchie zur Laufzeit darstellt. In Zusammenhang mit der Rekursion (Kapitel 8) wird er auch Rekursionsbaum genannt. Beide Begriffe sollen nun an folgendem Beispiel erläutert werden. Beispiel 7.7 (Aufrufbaum und Run-Time-Stack) Betrachte die Applet Klasse in Abbildung 7.5. Die Scopeblöcke sind mit A (Klassenscope) und B–D (Blöcke aufgrund von Methoden bzw. compound statements) gekennzeichnet. Dabei wird die Methode init() zuerst aufgerufen. Der Aufrufbaum hat die in Abbildung 7.6 angegebene Gestalt.8 (Durch rekursive Aufrufe kann der Aufrufbaum im Prinzip unendlich groß werden.) Wir sehen uns jetzt den Run-Time-Stack an. In Block A werden alle Klassenvariablen und Funktionen definiert, also double x,y, int z; und die Funktionen f(), g(), und init(). Diese werden nicht auf dem Run-Time-Stack, sondern als globale Größen in einen anderen Bereich des Speichers, dem sogenannten Heap abgelegt, vgl. Abbildung 7.7. Beim Aufruf von init() werden keine lokalen Größen definiert. Im Activation Record wird nur die Rücksprungadresse der static Pointer abgelegt. Da noch kein übergeordneter Block existiert, fin8 Die Wurzel ist dabei der oberste Knoten, und alle gerichteten Kanten verlaufen von “höheren” zu “tieferen” Knoten. Daher verzichtet man bei derart dargestellten Bäumen auf die Angabe der Kantenrichtung durch Pfeile wie sonst bei gerichteten Graphen üblich. 193 7.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION class RunTimeStack extends Applet { double x = 1, y = 2; int z = 3; void f(double a) { int i = 4; ... x = x + i*a; System.out.println("B: a = " + a + ", x = " + x + ", y = " + y); ... } void g(int x) { int y = 5; ... { double i = 6; ... f(x); System.out.println("D: i = " + i + ", y = " + y + ", x = " + x); ... } ... int i = 7; f(y); System.out.println("C: i = " + i + ", y = " + y + ", x = " + x); ... } void init() { ... g(z) System.out.println("E: x = " + x + ", y = " + y + ", z = " + z); ... } } Abbildung 7.5: Ein Programm mit seinen Scopes. B D C E A 194 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN ← Aufruf von init() E ← Aufruf von g() in init() C @ @ D @ B ← Eintritt in Block D und Aufruf von f() in C ← Aufruf von f() in D B Abbildung 7.6: Der Aufrufbaum zum Programm aus Abbildung 7.5. H EAP x y z f g init double double int function function function Wert 1 Wert 2 Wert 3 Abbildung 7.7: Der Heap zum Programm aus Abbildung 7.5. 195 7.1. FUNKTIONALE (PROZEDURALE) ABSTRAKTION det man alle übergeordneten Identifier im Heap (gekennzeichnet durch H EAP im Stack), vgl. Abbildung 7.8. Der Aufruf g(z) in init() bewirkt den Eintritt in den Block C und die Parameteridentifikation von x (definiert in C) mit z (global definiert). Da noch kein übergeordneter Block existiert, erübrigt sich die Einrichtung eines static Pointers. Alle übergeordneten Identifier findet man im Heap (gekennzeichnet durch H EAP im Stack), vgl. Abbildung 7.8. E Rücksprungadresse 1 static pointer: H EAP C E x int (Wert 3 von z) y int (Wert 5) Rücksprungadresse 2 static pointer: H EAP Rücksprungadresse 1 static pointer: H EAP Abbildung 7.8: Der Stack nach dem Aufruf von init() (links) und g(z) in init() (rechts). Abbildung 7.9 beschreibt den Run-Time-Stack beim Eintritt in den Scopeblock D aus Block C (links) und beim Eintritt in den Scopeblock B aus Block D (rechts). D C E i double (Wert 6) Rücksprungadresse 3 r static pointer: x int (Wert 3 von z) y int (Wert 5) Rücksprungadresse 2 static pointer: H EAP Rücksprungadresse 1 static pointer: H EAP B D C E a double (Wert 3 von x) i int (Wert 4) Rücksprungadresse 4 static pointer: H EAP i double (Wert 6) Rücksprungadresse 3 r static pointer: x int (Wert 3 von z) y int (Wert 5) Rücksprungadresse 2 static pointer: H EAP Rücksprungadresse 1 static pointer: H EAP Abbildung 7.9: Der Stack beim Eintritt in den Scopeblock D aus C (links) und in B aus D (rechts). Im Statement x = x + i*a in Block B ist also mit x die Klassenvariable x und nicht die an f übergebene Variable x gemeint, da der static pointer auf den Heap zeigt. Also wird x + i*a zu 1 + 4 ∗ 3 = 13 ausgewertet. Nach Abarbeitung des Blocks B wird der entsprechende Activation Record auf dem Stack gelöscht. Die Rücksprungadresse 3 gibt an, wo im Programm weitergemacht wird. Die dann entstehende Situa- 196 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN tion ist in Abbildung 7.10 links angegeben, die nach Abarbeitung von Block D rechts. D C E i double (Wert 6) Rücksprungadresse 3 r static pointer: x int (Wert 3 von z) y int (Wert 5) Rücksprungadresse 2 static pointer: H EAP Rücksprungadresse 1 static pointer: H EAP C E x int (Wert 3 von z) y int (Wert 5) Rücksprungadresse 2 static pointer: H EAP Rücksprungadresse 1 static pointer: H EAP Abbildung 7.10: Der Stack nach dem Austritt aus Scopeblock B (links) und aus D (rechts). Die Situation beim Eintritt in Block B aus Block C ist in Abbildung 7.11 angegeben. Jetzt wird x + i*a zu 13 + 4 ∗ 5 = 33 ausgewertet. Danach werden B, C und E abgearbeitet und die zugehörigen Activation Records gelöscht. B C E a double (Wert 5 von y) i int (Wert 4) Rücksprungadresse 5 static pointer: H EAP i int (neu im Scope, Wert 7) x int (Wert 3 von y) y int (Wert 5) Rücksprungadresse 2 static pointer: H EAP Rücksprungadresse 1 static pointer: H EAP Abbildung 7.11: Der Stack nach dem Eintritt in Scopeblock B aus C. Das Programm schreibt gemäß der Abarbeitung der Scopeblöcke in der Reihenfolge B – D – B – C – E mit den println Anweisungen die Zeilen B: D: B: C: E: a i a i x = = = = = 3.0, x = 13.0, y = 2.0 6.0, y = 5, x = 3 5.0, x = 33.0, y = 2.0 7, y = 5, x = 3 33.0, y = 2.0, z = 3 197 7.2. MODULARE ABSTRAKTION auf den Bildschirm. Sie geben die Werte der gerade sichtbaren Variablen im jeweiligen Block an dieser Stelle an. Aus den Regeln der Abarbeitung von Scopeblöcken ergibt sich bezüglich des Aufrufbaumes und des Stacks folgender Satz. Satz 7.1 (Eigenschaften des Run-Time-Stack und des Aufrufbaumes) 1. Der Aufrufbaum wird in der Reihenfolge LRW (linker Teilbaum vor rechter Teilbaum vor Wurzel) abgearbeitet. 2. Die maximale Anzahl von Activation Records auf dem Run-Time-Stack, die ein Maß für die Größe des zur Laufzeit beanspruchten Speicherplatzes darstellt, ist gleich der Höhe des Aufrufbaumes + 1.9 Im Zusammenhang mit der Rekursion (Kapitel 8) nennt man die Zahl Höhe(Aufrufbaum) + 1 auch die Rekursionstiefe. 7.2 Modulare Abstraktion Die durch Funktionen gewonnene Abstraktion beschränkt sich weitgehend auf das Input-Output Verhalten von Programmteilen, also auf den Datenfluss. Oft möchte man jedoch weiter gehen und auch ganze Datenstrukturen mit mehreren zugehörigen Variablen, Funktionen und Typen abstrahieren, so wie bei den in Kapitel 5 besprochenen Datenstrukturen. Dies geschieht in vielen Programmiersprachen in sogenannten Modulen. Sie stellen Verallgemeinerungen von Funktionen dar, indem sie eine Kollektion miteinander zusammenhängender Objekte (z. B. mehrere Funktionen, Typen, Konstanten, Variable) zu einer separat compilierten Einheit zusammenfasst. Dies ist schematisch in Abbildung 7.12 dargestellt. Modul Variable Anweisungen Modul Variable lokale Variable Funktion Funktion f Modul M lokale Variable Funktion ... Funktion ... Abbildung 7.12: Funktion versus Modul. Module sind so konzipiert, dass andere Module oder Programme Teile oder das Modul als Ganzes nutzen können. Man nennt die Nutzer Klienten des genutzten Moduls. 9 Die Höhe eines Baumes ist die maximale Kantenzahl auf einem Weg von der Wurzel bis zu einem Blatt. 198 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN Beispiele für Module sind Bibliotheken in C oder Implementationen von abstrakten Datentypen. Module werden meist beschrieben durch eine Spezifikation, die das Verhalten und die Eigenschaften des Moduls festlegt. Diese wird getrennt von der Implementation, die die zugehörigen Programme enthält. Die Spezifikation ist der öffentliche (public) Teil des Moduls. Klienten können (oder sollten) nur die dort deklarierten Begriffe nutzen.10 Im privaten (private) Teil sind die Rümpfe der Funktionen und zusätzliche private Variable enthalten, die nach außen verborgen bleiben (sollten). Man spricht daher auch von Einkapselung (encapsulation). Wirksame encapsulation setzt voraus, dass die Programmiersprache getrennte Kompilation von Programmteilen erlaubt oder Konstrukte ermöglicht, die Daten als privat erklären. Getrennte Compilierung hat viele Vorteile: 1. Module sind wiederverwendbar (reusable, off-the-shelf components). 2. Module können bei Änderung rekompiliert werden, ohne die Klienten rekompilieren zu müssen. 3. Klienten können geändert werden, ohne das Modul ändern zu müssen. 4. Module können nur als Objektcode zur Verfügung gestellt werden, so dass Details der Implementation verborgen bleiben (encapsulation). 7.3 Abstraktion durch Klassen Java bietet ein eigenes Konstrukt zur Abstraktion von Datentypen mit den zugehörigen Operationen: Klassen. Eine Klasse (class) ist ein durch den Programmierer definierter strukturierter Typ. Seine Komponenten heißen class members. Dies können Variablen, Funktionen und auch wieder Klassen sein. In der Java Terminologie werden sie Klassenvariablen oder Felder, Methoden bzw. innere Klassen genannt. Da Klassen Typen sind, kann man Variable dieses Typs definieren. Jedes Objekt hat dann (im Prinzip11 ) als Komponenten alle Felder und Methoden der Klasse (und aller Oberklassen, von denen sie abgeleitet wird, vgl. Abschnitt 7.3.3. 7.3.1 Definition von Klassen Wir betrachten zunächst die eingeschränkte Definition class KlassenName { Def 1 ; Def 2 ; .. . Def r ; } 10 Sprachabhängig 11 Ausnahmen kann auch der Zugriff auf private Daten möglich sein. Er sollte jedoch unterbleiben. sind als static deklarierte Felder und Methoden, vgl. dazu Abschnitt 7.3.2. 7.3. ABSTRAKTION DURCH KLASSEN 199 Dabei ist class das Schlüsselwort, das die Klassendefinition einleitet, und Def 1 , . . . ,Def r sind Definitionen von Variablen (Feldern der Klasse), Funktionen (Methoden der Klasse) oder inneren Klassen, vgl. Abschnitt 7.3.8. Unter den Methoden sind Konstruktoren von besonderer Bedeutung. Sie haben immer denselben Namen wie die Klasse und folgende Form KlassenName(formale Parameterliste ){. . .} Es gibt weder einen expliziten Rückgabetyp, noch das Schlüsselwort void. Im gewissen Sinne ist der Name des Konstruktors der Rückgabetyp. In der Regel haben Klassen mehrere Konstruktoren mit verschiedenen Parameterlisten. Der Aufruf eines Konstruktors erfolgt mit new. Er erzeugt ein neues Objekt der Klasse und gibt eine Referenz auf dieses Objekt als Wert zurück. Wir betrachten diese Begriffe an einer Variation der auf Seite 186 definierten Klasse IntPair. Programm 7.1 IntPair class IntPair { int first; int second; IntPair(int x, int y) { first = x; second = y; } int sum() { return first + second; } } Diese Klasse hat 2 Felder first und second, eine Methode sum und einen Konstruktor IntPair. Durch IntPair xy = new IntPair(3, 7); wird ein neues Objekt dieser Klasse erzeugt, dessen Felder first und second die Werte 3 und 7 haben. Der Zugriff eines Objektes auf seine Felder und Methoden erfolgt durch den . gemäß object.feld bzw. object.methode(). xy.first = -4; ändert also den Wert von first zu -4, 200 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN xy.second = xy.sum(); ändert den Wert von second zum Ergebnis des Aufrufes von sum() für das Objekt xy, also zu −4 + 7 = 3. Der Zugriff eines Objektes auf eine Methode übergibt der Methode implizit eine Referenz auf das Objekt als ersten Parameter. Diese implizite Referenz kann innerhalb der Klasse durch die Variable mit dem reservierten Namen this angesprochen werden. Die Definition von sum in der Klasse IntPair hätte also auch als int sum() { return this.first + this.second; } geschrieben werden können. Außerhalb der Klasse des Objektes ist dies jedoch nicht möglich. Die Verwendung von this gehört zur Namenskonvention von Java, insbesondere in Konstruktoren. Dort sollen nach Konvention die Parameter, mit denen Felder gesetzt werden, dieselben Bezeichner wie die Feldnamen bekommen. Die Unterscheidung zwischen Parametern und Feldern ist dann nur mit this möglich. Bei Befolgung der Namenskonvention wird der Konstruktor der Klasse IntPair zu IntPair(int first, int second) { this.first = first; this.second = second; } Klassen können auch ohne die Definition von Konstruktoren geschrieben werden. Dann verfügen sie automatisch über den sogenannten leeren Konstruktor oder Default Konstruktor, der keine Argumente hat. Dies gilt jedoch nicht mehr, sobald ein Konstruktor definiert wird. Da Default Konstruktoren für die Vererbung (vgl. Abschnitt ??) immens wichtig sind, sollte man Klassen, die Konstruktoren haben, immer zusätzlich mit einem Default Konstruktor ausstatten. In der Klasse IntPair wäre folgender Default Konstruktor sinnvoll, der das Paar (0,0) erzeugt. public IntPair() { first = 0; second = 0; } Oft ist auch ein Copy Konstruktor sinnvoll. Ein solcher Konstruktor erstellt eine identische Kopie des ihm übergebenen Objektes derselben Klasse. Wir erläutern es an der Klasse IntPair: public IntPair(IntPair xy) { first = xy.first; second = xy.second; } 7.3. ABSTRAKTION DURCH KLASSEN 201 Statt eines Copy Konstruktors kann man auch das Interface Cloneable implementieren, siehe Abschnitt 7.3.7. 7.3.2 Static-Felder und Methoden In manchen Situationen möchte man Felder oder Methoden für die Klasse als Ganzes anlegen. Dies ist z. B. sinnvoll bei der Definition von Konstanten, die für alle Objekte der Klasse gleich sind, oder Methoden, die unabhängig von den Objekten der Klasse sind. Dies kann erreicht werden durch Verwendung des Modifizierers static vor der Definition. In der Klasse Integer wird etwa der maximale Wert MAX VALUE definiert als12 public static final int MAX_VALUE = Ox7fffffff; Ebenso sind in der Klasse Math von mathematischen Funktionen alle Methoden als static definiert, etwa public static native double sin(double a); für die Sinus-Funktion. static Methoden und Felder können nicht auf Instanzen der Klasse zugreifen. Die Benutzung von static Methoden und Feldern innerhalb der Klasse geschieht über ihre Namen, außerhalb können sie (sofern sichtbar) durch Nennung des Klassennamens und den . angesprochen werden, also etwa myColor = Color.orange; für die static Konstante orange der Klasse Color, und x = Math.max(y, z); für die static Funktion max der Klasse Math. Der etwas seltsame Name static ist historisch bedingt und meint den Gegensatz zu dynamic. Identifier ohne den Zusatz static sind automatisch dynamic. Für sie wird Speicherplatz bei Betreten des Scopeblocks eingerichtet (vgl. Abschnitt 7.1.5) und nach Verlassen des Scopeblocks wieder vernichtet. Bei als static deklarierten Identifiern bleibt dieser Speicherplatz auch zwischen Verlassen und erneutem Wiedereintritt in den Scopeblock erhalten. Sie sind in diesem Sinne nicht dynamic“. ” 7.3.3 Unterklassen und Vererbung Klassen können andere Klassen erweitern. Die neue, erweiterte Klasse heißt Unterklasse oder Subklasse, die vorgegebene Klasse heißt Oberklasse. Man sagt auch, dass die Unterklasse von der Oberklasse abgeleitet wird. Die Syntax hierfür ist 12 Ox7fffffff ist Hexadezimalnotation, vgl. Beispiel 4.3. Die entsprechende Dezimalzahl ist 2.147.483.647 = 231 − 1. 202 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN class NameUnterklasse extends NameOberklasse {...} Die Unterklasse hat dabei Zugriff auf alle Felder und Methoden der Oberklasse, diese werden ge” erbt“. Die Unterklasse darf Methoden und Felder der Oberklasse neu definieren und zusätzliche Methoden und Felder einführen. Hierdurch entstehen ganze Hierarchien von Klassen13 . Die Java Bibliothek liefert viele Beispiele hierfür. Wir haben dieses Prinzip schon in der Applet-Programmierung verwendet, alle Klassen waren Erweiterungen der Oberklasse Applet. Alle Klassen von Objekten sind in Java Unterklassen der Klasse Object. Dadurch lassen sich Datenstrukturen für Objekte sehr allgemein definieren, vgl. die Beispiele Stack und Liste in Abschnitt 5.7 und Abschnitt 5.7. Für den Zugriff auf Felder und Methoden der Oberklasse dient die Referenz super. Entsprechend können Konstruktoren der Superklasse mit super(...) und den entsprechenden aktuellen Parametern angesprochen werden. Hierzu ein Beispiel14 : Die Klasse class Square { double width; Square(double width) { this.width = width; } double area() { return width * width; } } wird erweitert durch die Klasse class Rectangle extends Square { double height; Rectangle(double width, double height) { super(width); this.height = height; } 13 In Java 1.1.4 gab es 21 Pakete mit 503 vordefinierten Klassen und ca. 5000 Methoden, in Java 1.3 gab es bereits 76 Pakete mit 1841 Klassen und ca. 20000 Methoden. Wieviele gibt es in Java 1.4? 14 das auch zeigt, dass die logische Hierarchie (Quadrat ist “Unterklasse” von Rechteck) nicht mit der Vererbungshierarchie übereinstimmen muss. Bei der Vererbung bedeutet Spezialisierung immer Hinzufügen bzw. Überschreiben von Feldern und Methoden. 7.3. ABSTRAKTION DURCH KLASSEN 203 double area() { return width * height; } } Im Konstruktor Rectangle wird mit super(width) der Konstruktor von Square aufgerufen, eine Zuweisung super.width = width statt dieses Aufrufes resultiert (da kein expliziter Konstruktoraufruf der Klasse Square erfolgt) in einen impliziten Aufruf von super(), also Square(). Ein solcher Konstruktor existiert aber nicht in der Klasse Square, so dass der Compiler einen Fehler meldet. 15 Die Methode area() wird in der Klasse Rectangle überschrieben. Für beide Klassen steht daher derselbe Name für die (unterschiedliche!) Berechnung der Fläche zur Verfügung. Um jetzt (z. B. in einer umfangreichen Graphik) verschiedene Quadrate und Rechtecke abzuspeichern, kann man ein Array Square[] vec = new Square[n]; definieren. Da jedes rectangle Objekt durch die Vererbung auch vom Typ Square ist, können gleichzeitig Rechtecke und Quadrate im Array verwaltet werden, also etwa vec[0] = new Square(1); vec[1] = new Rectangle(2, 3); Der Durchlauf for (int i = 0; i < vec.length; i++) { System.out.println(vec[i].area()); } schreibt dann nacheinander die Fläche der Rechtecke und Quadrate auf den Bildschirm. Dabei wird automatisch die richtige area() Methode gewählt! Will man als Programmierer die Klasse eines Objektes ermitteln, so geht dies mit der Methode getClass() der Klasse Object. Diese Methode liefert eine Referenz auf ein Objekt der Klasse Class zurück, das u. a. den Namen der Klasse des betrachteten Objektes enthält, und zwar mit dem package Präfix der Klasse (vgl. Abschnitt 7.3.4). Object o = new Object(); String str = o.getClass().getName(); weist also der Variablen str den Wert "java.lang.Object" zu. Im obigen Beispiel schreibt die Anweisung 15 Es ist daher guter Programmierstil, jede Klasse mit einem parameterlosen Defaultkonstruktor zu versehen. 204 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN for (int i = 0; i < 2; i++) { System.out.println(vec[i].getClass().getName()); } nacheinander Square und Rectangle auf den Bildschirm. Der package Präfix entfällt hier, da die Klassen Square und Rectangle keinem Package angehören. 7.3.4 Packages Ein Programm in Java ist eine Menge von Dateien der Form Klassenname.java, wobei Klassenname.java genau den Programmtext der Klasse Klassenname enthält16 . Die Funktionsweise des Programms wird durch die Interaktion der Klassen (Verwendung der Methoden aus einer anderen Klasse, Vererbung, usw.) festgelegt. Um Klassen zu ähnlich gelagerten Aufgaben zusammenfassen zu können, gibt es die Möglichkeit zur Definition von Paketen (packages). Ein package ist eine Menge von Klassen in einem gemeinsamen Verzeichnis. Der Pfad zu diesem Directory stimmt mit der Bezeichnung des packages überein. Das package java.lang liegt (in einem UNIX System) in einem Verzeichnis .../java/lang relativ zu den durch die Environment Variable CLASSPATH festgelegten Verzeichnis-Pfaden. Das Herstellen eigener Packages geschieht mit der Anweisung package PackageName; vor jeder Klassendefinition des Packages, also z. B. mit package java.awt; public class TextField extends TextComponent { .. . } bei der Definition der Klasse TextField des Paketes java.awt. 7.3.5 Sichtbarkeit von Klassen, Methoden und Feldern Damit Klassen in einem Java Programm interagieren können, muss die Sichtbarkeit der Namen nach außen festgelegt werden. Gutes Softwaredesign macht nur ausgewählte, wohl überlegte Methoden nach außen sichtbar, verbirgt aber alle Methoden und Felder, die nur als interne Hilfsmittel dienen. Zur Regelung der Sichtbarkeit zwischen Klassen dienen die Modifizierer public, protected und private, die der Definition vorangestellt werden. Dabei darf nur einer dieser Modifizierer auftreten. Sie haben folgende Bedeutung. Sichtbarkeitsmodifizierer für Felder und Methoden: 16 bis auf zusätzliche Klassen, die nach außen nicht sichtbar sind, vgl. Abschnitt 7.3.8. 7.3. ABSTRAKTION DURCH KLASSEN Feld ist überall sichtbar (Klasse muss ebenfalls public sein). Default, Feld ist in dem Package sichtbar. Wie Default, und das Feld ist auch in den Subklassen anderer Packages sichtbar, die aus dieser Klasse abgeleitet wurden (protected ist also de facto weniger geschützt als der Default!). Feld ist nur in dieser Klasse sichtbar. public leer protected private Klassen haben als Sichtbarkeitsmodifizierer nur Klasse ist in anderen Packages sichtbar. Default, Klasse ist in dem Package sichtbar. public leer 7.3.6 Weitere Modifizierer Für Felder einer Klasse wird die Art der Verwendung durch folgende Modifizierer festgelegt: Eins pro Klasse, nicht eins pro Objekt (vgl. Abschnitt 7.3.2). Wert kann nicht verändert werden. Solche Felder werden nicht mit dem Objekt abgespeichert. Reserviert für zukünftige Verwendung. Diese Daten können an verschiedene Steuerthreads übergeben werden, so dass das Laufzeitsystem Lesen und Beschreiben solcher Felder synchronisieren muss. static final transient volatile Bei Methoden unterscheidet man final static abstract native synchronized Kann nicht überschrieben werden. Eine pro Klasse, nicht eine für jedes Objekt. Muss überschrieben werden (um einen Nutzen zu haben). Nicht in Java geschrieben (kein Rumpf, sonst aber normal und vererbbar, statisch usw.). Der Rumpf wird in einer anderen Sprache geschrieben. Hierzu dient das JNI (Java Native Interface), das in einer eigenen JNI-Specification festgelegt ist. Es kann in dieser Methode jeweils nur ein Thread ausgeführt werden. Der Zugriff auf diese Methode wird überwacht (vgl. Threads in der Übung). Bei Klassen gibt es schließlich final abstract Klasse kann nicht erweitert werden. Klasse muss erweitert werden, wobei alle abstrakten Methoden überschrieben werden müssen. Sinnvolle Kombinationen dieser Modifizierer wie 205 206 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN native private static ... sind möglich. 7.3.7 Interfaces Java sieht keine Mehrfachvererbung vor, so dass eine Klasse nur eine andere erweitern kann. Um dennoch Methoden aus weiteren Klassen benutzen zu können, stellt Java Interfaces bereit. Ein Interface wird wie eine Klasse definiert Modifizierer interface InterfaceName {...} und kann mehrere andere Interfaces erweitern. Im Gegensatz zu Klassen haben Interfaces keine Konstruktoren, sondern nur statische Konstanten und abstrakte Methoden. Bei den Methoden wird also lediglich der Methodenkopf angegeben und mit einem Semikolon beendet. Klassen können Interfaces über das Schlüsselwort implements nutzen, und zwar mehrere Interfaces gleichzeitig. class myClass extends Applet implements ActionListener { ... } Dies bedeutet, dass die Methodennamen aus dem Interface zur Verfügung stehen, die Methoden aber alle noch in der Klasse implementiert werden müssen, d. h. die Methodenköpfe werden mit einem Rumpf versehen. Interfaces schreiben also Namen und Parameterlisten für Methoden vor, die Implementation muss jedoch in der Klasse erfolgen. Wir erläutern dies am Beispiel der Klasse Stack aus Abschnitt 5.7. /** * The <code>StackInterface</code> defines an interface * for a stack of objects. * * @see ListNode */ public interface StackInterface { /** * Tests if this stack has no entries. * * @return <code>true</code> if the stack is empty; * <code>false</code> otherwise */ boolean isEmpty(); 7.3. ABSTRAKTION DURCH KLASSEN 207 /** * Return the value of the current node. * @throws NoSuchElementException */ Object top() throws NoSuchElementException; /** * Inserts a new stack node at the top. * * @param <code>someData</code> the object to be added. */ void push(Object someData); /** * Delete the current node from the list. * @throws NoSuchElementException */ void pop() throws NoSuchElementException; } Man beachte, dass die Methoden eines Interfaces implizit public und abstract (sofern nicht final) sind. Diese Modifizierer brauchen also nicht hinzugefügt werden. Die Implementation des Stacks auf Seite 101) mit diesem Interface geschieht dann wie folgt: public class Stack implements StackInterface { // Hinzufügen von Feldern für die Implementation // Implementation der Methoden // Hinzufügen von Konstruktoren } Die Klassenbibliothek von Java macht ausführlich von Interfaces Gebrauch. Ein bereits genanntes Beispiel ist das Interface ActionListener (vgl. Abschnitt 7.3.8, andere sind Cloneable, Comparable und Runnable. Diese definieren die Methoden clone() (vgl. Abschnitt 6), compareTo() (für Vergleiche von Objekten) und run() (für nebenläufige Prozesse in Form von Threads). Wir geben ein Beispiel für Cloneable mit der Klasse IntPair aus Abschnitt 7.3.1: import java.lang.Cloneable; class IntPair implements Cloneable { ... public Object clone() { 208 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN return new IntPair(this.first, this.second); } } Die überschriebene Methode clone() gibt ein allgemeines Objekt zurück und muss daher mit casting verwendet werden: IntPair p = new IntPair(1,2); IntPair q = (IntPair) p.clone(); 7.3.8 Klassen in Klassen Klassen können als Komponenten außer Datenfeldern und Methoden auch Klassen haben. Sie werden wie Datenfelder oder Methoden über den . angesprochen und können Modifizierer haben. Sind sie nicht static, so werden sie innere Klassen genannt. Klassen können ferner (wie lokale Variable) lokal in Methoden verwendet werden. Sie heißen dann lokale Klassen. Sie werden wie gewöhnliche Klassen deklariert. Bezüglich der Sichtbarkeit von Klassen in Klassen gelten die gleichen Scope-Regeln wie beim Klassenscope bzw. Blockscope. Zusätzlich können Komponentenklassen, die public sind, von anderen Klassen importiert und genutzt werden. Müssen lokale Klassen nicht über einen Klassennamen angesprochen werden, so kann man sie als anonyme Klassen direkt nach der Angabe einer Oberklasse oder eines Interfaces definieren, ohne sie zu benennen. Wir haben hiervon bereits regen Gebrauch bei den ActionListenern gemacht, siehe Abschnitt 7.3.9. 7.3.9 Implementationen des Interface ActionListener Wir erläutern jetzt Varianten der Implementation des Interface ActionListener. Zur Illustration benutzen wir das Applet Temperatur (Abschnitt 2.1). Die dort benutzten Anweisungen TextField input = new TextField(10); input.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { // perform the action calculateTemperature(); } }); zur Anbindung eines ActionListeners lassen sich jetzt wie folgt erklären. ActionListener ist ein Interface, das als einzige Methode die abstrakte Methode 7.3. ABSTRAKTION DURCH KLASSEN 209 public void actionPerformed(ActionEvent e); enthält. Die Methode public void addActionListener(ActionListener l) {...} der Klasse TextField verlangt die Angabe eines ActionListener-Objektes l. Dieses geschieht mit new ActionListener(). Da ActionListener ein Interface ist, braucht man eine Klasse, die “implements ActionListener” durchführt, d. h. die Methode actionPerformed(ActionEvent) definiert. Genau dies leistet die anonyme Klasse { public void actionPerformed(ActionEvent e){ // perform the action calculateTemperature(); } } Natürlich könnte man den ActionListener auch in einer eigenen Klasse implementieren, oder durch das Applet implementieren lassen. Wir betrachten zunächst eine Implementation als innere Klasse: public class Temperatur extends Applet { ... public void init() { ... TextField input = new TetField(10); input.addActionListener(new MyActionListener()); ... } class MyActionListener extends ActionListener { public void actionPerformed(ActionEvent e) { // perform the action calculateTemperature(); } } public void calculateTemperature() { ... } } 210 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN Hier ist MyActionListener als innere Klasse auf Klassenniveau definiert, unterliegt also dem Klassenscope und kann daher an beliebiger Stelle definiert werden. Es wäre auch möglich, sie als lokale Klasse direkt innerhalb der Methode init() zu definieren; dann würde sie jedoch dem Blockscope unterliegen und muss vor der Anweisung input.addActionListener(...) erfolgen, da sonst MyActionListener() nicht bekannt ist. Die Anweisung new MyActionListener() ruft den Default Konstruktor der inneren Klasse MyActionListener auf. Dieser musste nicht definiert werden, da Klassen ohne Definition von Konstruktoren automatisch über den Default Konstruktor verfügen. Natürlich ist auch die Implementation des Interface ActionListener als eigene (nicht innere) Klasse MyActionListener möglich. Dann ist jedoch der Informationstransfer schwieriger zu gestalten. Da die Methode actionPerformed() in der Klasse MyActionListener angesiedelt ist, muss man dem einem Konstruktor der Klasse MyActionListener zumindest die TextFields input und output des Applets Temperatur übergeben, damit die Methode actionPerformed() auf sie zugreifen kann. Wir lösen dies, indem wir das gesamte Applet Temperatur übergeben. Innerhalb der Klasse Temperatur erfolgt die Anbindung des ActionListener an das TextField input mit der Anweisung input.addActionListener(new MyActionListener(this)); die einen Konstruktor der Klasse MyActionListener aufruft, dem ein Objekt der Klasse Temperatur übergeben werden kann. Die Klasse MyActionListener sieht dann folgendermaßen aus. import import import import java.awt.*; java.applet.Applet; java.awt.event.ActionListener; java.awt.event.ActionEvent; class MyActionListener implements ActionListener { private Temperatur tempApplet; public MyActionListener(Temperatur tempApplet) { this.tempApplet = tempApplet; } public void actionPerformed(ActionEvent e) { // perform the action calculateTemperature(); } 7.3. ABSTRAKTION DURCH KLASSEN 211 // process user’s action on the input text field public void calculateTemperature() { // get input number double fahrenheit = Double.parseDouble(tempApplet.input.getText()); // calculate celsius and round it to 1/100 degrees double celsius = 5.0 / 9 * (fahrenheit - 32); // use Math class for round celsius = Math.round(celsius * 100); celsius = celsius / 100.0; // show result in textfield output tempApplet.output.setText(Double.toString(celsius)); } } Die Methode calculateTemperature() ist jetzt in dieser Klasse, die Klasse Temperatur enthält nur die Methode init() und auch nicht mehr die Variablen double und fahrenheit. Zum Abschluss betrachten wir die Implementation des ActionListener durch das Applet Temperatur selbst. Dann muss die Klasse Temperatur das Interface ActionListener implementieren und daher die Methode actionPerformed() definieren. public class Temperatur extends Applet implements ActionListener { ... public void init() { ... input = new TextField(10); // register this applet as ActionListener for // TextField input input.addActionListener(this); ... } // duties of this Applet as ActionListener public void actionPerformed(ActionEvent e) { // body of method calculateTemperature() } } 212 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN 7.3.10 Der Lebenszyklus von Objekten Objekte werden durch Aufruf von Konstruktoren von Klassen mit new erzeugt und auf dem Heap angelegt, unterliegen also nicht der Speicherverwaltung auf dem Run-Time-Stack. Hierdurch ist es möglich, auch innerhalb von Methoden Objekte zu erzeugen und sie z. B. durch Rückgabe einer Referenz (wie in squareNumbers auf Seite 184) an den aufrufenden Programmteil zu übergeben. Objekte bleiben so lange erhalten, wie es eine Referenz auf sie gibt. Das Java Run-Time-Environment überwacht dies und stellt den Speicherplatz, der durch ein nicht mehr referenziertes Objekt belegt wird, wieder zur Verfügung. Dieser Automatismus erlaubt jedoch keinen Einfluss auf den Zeitpunkt der Rückgabe. Die Überwachung und Rückgabe nicht mehr referenzierter Objekte bezeichnet man in allen Programmiersprachen als Garbage Collection. Java hat also eine eingebaute Garbage Collection, um die sich der Programmierer nicht kümmern muss. Der Nachteil dieses Automatismus besteht in gewissen Einbußen an Laufzeit, die zudem zeitlich unkontrollierbar auftreten können. Hat man jedoch gerade mal Zeit für die Garbage Collection, so kann man sie mit der Methode System.gc(); starten. 7.4 7.4.1 Beispiele von Klassen Bruchrechnung Die folgende Klasse Fraction (vgl. auch Abschnitt 5.2) stellt Datenstrukturen und Methoden zum Rechnen mit Brüchen zur Verfügung. import java.lang.Cloneable; /** * The <code>Fraction</code> class implements fractions. * Each fraction is a pair numerator/denominator of longs * in simplified form, i.e. gcd(numerator,denominator) = 1 */ public class Fraction implements Cloneable { /** * the numerator */ private long num; 213 7.4. BEISPIELE VON KLASSEN /** * the denominator. */ private long denom; It is always > 0 /** * Default constructor, constructs 0 as fraction 0/1 */ public Fraction() { num = 0; denom = 1; } /** * Constructs Fraction object from a long * @param a yields the fraction a/1 */ public Fraction(long a) { num = a; denom = 1; } /** * Constructor with two long argument num and denom, * constructs the fraction num/denom and simplifies it. * @param num the numerator * @param denom the denominator * Throws <code>ArithmeticException</code> if * <code>denom == 0</code> */ public Fraction(long num, long denom) throws ArithmeticException{ if (denom == 0) throw new ArithmeticException( "Division by zero in constructor" ); else { this.num = num; this.denom = denom; this.simplify(); } } /** * simplifies this fraction */ private void simplify() { 214 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN if (num == 0) { this.num = 0; this.denom = 1; } else { long gcd = gcd(this.num, this.denom); this.num = this.num/gcd; this.denom = this.denom/gcd; if ( this.denom < 0 ) { this.num = -this.num; this.denom = -this.denom; } } } /** * Calculates the greatest common divisor of |a| and |b| * @param a * @param b * @return the greatest common divisor of * |<code>a</code>| and |<code>b</code>| */ private static long gcd(long a, long b) throws ArithmeticException { if ( a == 0 || b == 0 ) throw new ArithmeticException( "Zero argument in gcd calculation"); a = Math.abs(a); b = Math.abs(b); while (a != b) { if (a > b) a = a - b; else b = b - a; } return a; } /** * Returns a string representation of this fraction. * @return fraction in the form "num/denum" */ public String toString() { return Long.toString(this.num) + "/" + Long.toString(this.denom); } /** * Returns the double value of this fraction. 7.4. BEISPIELE VON KLASSEN 215 * @return num/denum */ public double doubleValue() { return (double) this.num / (double) this.denom; } /** * Checks equality with other fraction r. * @param r the fraction to be compared with * @return <code>true</code> if this fraction equals <code>r</code>. */ public boolean equals(Fraction r) { return (this.num == r.num) && (this.denom == r.denom); } /** * Get the nominator of this fraction * @return the numerator of this fraction */ public long getNumerator() { return num; } /** * Get the denominator of this fraction * @return the denominator of this fraction */ public long getDenominator() { return denom; } /** * Multiplies this fraction with other fraction r and * simplifies the result. * @param r the fraction to be multiplied with. */ public void multiply(Fraction r) { this.num = this.num * r.num; this.denom = this.denom * r.denom; this.simplify(); } /** * Adds fraction r to this fraction and 216 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN * simplifies result. * @param r the fraction to be added. */ public void add(Fraction r) { // use a/b + c/d = (ad + cb)/bd and simplify() this.num = this.num * r.denom + r.num * this.denom; this.denom = this.denom * r.denom; this.simplify(); } /** * clones this fraction by implementing Cloneable */ public Object clone() { return new Fraction(this.num, this.denom); } } Die Folge System.out.println( "Demo der Klasse Fraction:" ); Fraction r, s, t; r = new Fraction(3, 8); s = new Fraction(1, 6); double x = r.doubleValue(); System.out.println( "Wert von " + r.toString() + " ist " + x ); t = (Fraction) r.clone(); t.add(s); System.out.println(r.toString() + " + " + s.toString() + " = " + t.toString()); r = new Fraction(3, 4); t = (Fraction) r.clone(); t.multiply(s); System.out.println(r.toString() + " * " + s.toString() + " = " + t.toString()); System.out.println(r.toString() + " == " + s.toString() + " ergibt " + r.equals(s)); von Anweisungen schreibt dann Demo der Klasse Fraction: Wert von 3/8 ist 0.375 3/8 + 1/6 = 13/24 217 7.4. BEISPIELE VON KLASSEN 3/4 * 1/6 = 1/8 3/4 == 1/6 ergibt false auf den Bildschirm. 7.4.2 Erzeugung von Zufallszahlen Zufallszahlen sind ein Standardwerkzeug für die Simulation vieler technischer Abläufe. Die Aufgabe eines Zufallszahlengenerators ist es, wiederholt (d. h. in der Regel sehr lange Folgen von) Zahlen im Interval [0, 1] zu generieren, die den Charakter zufälliger Ziehungen haben.17 Erfahrungen (und Überlegungen der Wahrscheinlichkeitstheorie) zeigen, dass sich mit der Funktion f (x) = (a · x) mod m mit a = 16807 und m = 231 − 1 “gute” Zufallszahlen generieren lassen. Man startet mit beliebigem x0 ∈ {0, 1, . . . , m − 1} (der sogenannten seed) und erzeugt gemäß xn+1 = f (xn ) eine Folge x0 , x1 , x2 , . . . , xn , xn+1 , . . . von Zahlen aus {0, 1, . . . , m − 1}. Die zugehörige Folge xn0 := xn , n = 0, 1, 2, . . . m liefert dann “Zufallszahlen” im Interval [0, 1]. Diese Folge ist natürlich bei festem Startwert x0 alles andere als zufällig, da man alle Werte berechnen kann. Außerdem wird irgendwann ein Wert xr zum zweiten mal auftreten und die Folge wird sich von da ab wiederholen. Man spricht daher auch von Pseudozufallszahlen. Dennoch verhalten sich lange Anfangsstücke dieser Folge angenähert zufällig, so dass man sie in Simulationen gut nutzen kann. Die unten stehende Klasse implementiert Generatoren für Zufallszahlen als Objekte einer Klasse RandomNumber. Die Konstruktoren dieser Klasse erlauben entweder das Setzen der Startzahl x0 , oder einen “zufälligen” Start, indem x0 als Systemzeit genommen wird. Als Methoden haben die Objekte das Erzeugen der nächsten Zufallszahl aus dem Intervall [0, 1] mit nextDoubleRand(), bzw. mit nextIntRand(int,int) das Erzeugen einer zufälligen gleichverteilten ganzen Zahl aus dem Bereich {lo, lo+1 . . . , hi}. /** * * * * * The <code>RandomNumber</code> class offers facilities for pseudorandom number generation. <p> An instance of this class is used to generate a stream of pseudorandom numbers. The class uses a long seed, which is 17 Genauer, im Intervall [0, 1] gleichverteilt sind. Teilt man also [0, 1] in n gleichlange Teilintervalle und erzeugt man N n2 Zufallszahlen, so sollten in jedes Teilintervall ungefähr gleich viele (also ∼ N/n) Zufallszahlen fallen. 218 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN * modified using a linear congruential formula. See <ul> * <li>Donald Knuth, <i>The Art of Computer Programming, * Volume 2</i>, Section 3.2.1. for general information about * random number gerneration and * <li>S. Park and K. Miller, Random number generators: Good * ones are hard to find, <i>Comm. ACM</i> 31 (1988) 1192-1201 * for the specific one implemented here. * </ul> * @see java.util.Random * @see java.lang.Math#random() */ public class RandomNumber { private static final long MULTIPLIER = 16807; private static final long MODULUS = 2147483647; // Quotient of MODULUS / MULTIPLIER private static final long QUOT = 127773; // Remainder of MODULUS / MULTIPLIER private static final long REM = 2836; /** * The current seed of the generator. */ private long currentSeed; /** * Constructs a RandomNumber object and initializes it * with <code>System.currentTimeMillis()</code> */ public RandomNumber() { currentSeed = System.currentTimeMillis() % MODULUS; } /** * Constructs a RandomNumber object and initializes it * with the value <code>seed</code> * @param seed A value that permits a controlled * setting of the start seed. */ public RandomNumber(long seed) { currentSeed = Math.abs(seed) % MODULUS; } /** 7.4. BEISPIELE VON KLASSEN 219 * Generates the next random number in the interval [0,1] * @return The next random number in [0,1]. */ public double nextDoubleRand() { long temp = MULTIPLIER*(currentSeed%QUOT) REM*(currentSeed/QUOT); currentSeed = (temp > 0) ? temp : temp + MODULUS; return (double) currentSeed / (double) MODULUS; } /** * Generates a random int value between the given limits. * @param lo The lower bound. * @param hi The upper bound. * @return An integer value in {lo,...,hi} * @throws InvalidOperationException if lo > hi */ public int nextIntRand(int lo, int hi) throws InvalidOperationException { if (lo > hi) throw new InvalidOperationException( "invalid range: " + lo + " > " + hi); return (int) (nextDoubleRand() * (hi - lo + 1) + lo); } } Die Implementation nutzt currentSeed als Variable, die die momentane Zufallszahl enthält. Die Definition dieser Variablen als private sorgt dafür, dass diese Variable nur innerhalb der Klasse benutzt werden kann. Die Formel xk+1 = xk · a mod n wird hier zu currentSeed = (currentSeed * MULTIPLIER) % MODULUS Die modulo Berechnung wird, um einen Überlauf bei currentSeed * MULTIPLIER zu verhindern, zerlegt in MULTIPLIER * (currentSeed % QUOT) - REM * (currentSeed/QUOT); wobei QUOT = MODULUS / MULTIPLIER und REM = MODULUS % MULTIPLIER ist. Zum Resultat muss, falls es nicht positiv ist, noch MODULUS hinzu addiert werden, um es in den gewünschten Bereich 0 ≤ currentSeed ≤ MODULUS − 1 zu bringen18 (dies geschieht als bedingte Anweisung). 18 Beweis als Übung. 220 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN Die Methode nextIntRand() nutzt die Tatsache, dass bei der Konvertierung von double zu int nach unten gerundet wird. Es gilt also 0 ≤ nextDoubleRand() < 1 ⇒ 0 ≤ nextDoubleRand() * (hi - lo + 1 ) < hi − lo + 1 ⇒ lo ≤ nextDoubleRand() * (hi - lo + 1 ) < hi + 1 ⇒ lo ≤ (int)(nextDoubleRand() * (hi - lo + 1) + lo) < hi + 1 ⇒ lo ≤ (int)(nextDoubleRand() * (hi - lo + 1) + lo) ≤ hi Die Gleichverteilung der Zufallszahlen auf [0, 1] übersetzt sich daher auf die Gleichverteilung auf {lo, lo + 1, . . . , hi}. Eine mögliche Verwendung der Klasse RandomNumber zeigt das folgende Applet, das die Güte der Zufallszahlen für die Simulation eines Würfelspiels testet. Dabei wird 360000 mal mit zwei Würfeln gewürfelt. Für jeden Wurf wird die Summe der Augenzahlen ermittelt. Über diese Summe wird eine Statistik geführt und ausgegeben. Programm 7.2 RollDice.Java /** * This class investigates the odds for rolling * two dice by randomly generating such rolls * and calculating the sum of their values */ import java.awt.*; import java.applet.Applet; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; public final class RollDice extends Applet { Label seedPrompt, no_rollsPrompt, progressMsg; TextField seedFld, no_rollsFld; TextArea output; final static int MAX_VALUE = 6; // number of sides of the dice static int rolls; // number of rolls static long[] rollCount = new long[2 * MAX_VALUE + 1]; // rollCount[i] == number of times // that i was obtained as sum of the two dice static long seed; // seed for random number generator public void init() { setLayout(new FlowLayout(FlowLayout.LEFT)); 7.4. BEISPIELE VON KLASSEN 221 setFont(new Font("Times", Font.PLAIN, 24)); Font courier = new Font("Courier", Font.PLAIN, 24); no_rollsPrompt = new Label( "Bitte Anzahl der Würfe eingeben: "); add(no_rollsPrompt); no_rollsFld = new TextField("360000", 10); add(no_rollsFld); no_rollsFld.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ rollDice(); } }); seedPrompt = new Label( "Bitte Startzahl für die Zufallszahlen eingeben: "); add(seedPrompt); seedFld = new TextField("0", 10); add(seedFld); seedFld.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e){ rollDice(); } }); output = new TextArea(12, 30); output.setFont(courier); add(output); } public void rollDice() { try { output.setText(""); // get seed seed = Long.parseLong(seedFld.getText()); // may throw NumberFormatException rolls = Integer.parseInt(no_rollsFld.getText()); // get no of rolls // may throw NumberFormatException if (rolls < 0){ output.setText("Anzahl der Würfe " 222 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN + "muss positiv sein"); return; } for (int i = 0; i <= 2 * MAX_VALUE; i++) { rollCount[i] = 0; } RandomNumber dice1, dice2 ; // the two dice // make them different by different seeds if (seed != 0) { dice1 = new RandomNumber(seed); dice2 = new RandomNumber( seed*seed + 77*seed + 113); } else { dice1 = new RandomNumber(); dice2 = new RandomNumber( System.currentTimeMillis() + 10000); } // throw the dice rolls many times for (int i = 0; i < rolls; i++) { rollCount[dice1.nextIntRand(1, MAX_VALUE) + dice2.nextIntRand(1, MAX_VALUE)]++; } // generate the output nicely formatted String outputStr = format(rollCount); output.setText(outputStr); } catch (NumberFormatException e) { output.setText("Bitte nur ganze Zahlen eingeben."); } } private static String format(long[] rollCount) { // generate the 3 colums of out put as 3 arrays of Strings // first column are the indices of rollCount String[] index = new String[rollCount.length]; for (int i = 0; i < rollCount.length; i++) { index[i] = Integer.toString(i); } // second column are the counts, ie. rollCount itself String[] count = new String[rollCount.length]; for (int i = 0; i < rollCount.length; i++) { 7.4. BEISPIELE VON KLASSEN 223 count[i] = Long.toString(rollCount[i]); } // third column are the normalized frequencies // with decimal point String[] frequency = new String[rollCount.length]; for (int i = 0; i < rollCount.length; i++) { long freq = (long) ((rollCount[i] / (double) rolls) * 10000); StringBuffer strBuf = new StringBuffer(); strBuf.append(freq / 100 + "."); // decimal point if (freq % 100 == 0) strBuf.append("00"); else if (freq % 100 < 10) strBuf.append("0" + freq % 100); else strBuf.append(freq % 100); frequency[i] = strBuf.toString(); } // determine maximum Stringlength of every // column for indentation int maxIndex = maxLength(index); int maxCount = maxLength(count); int maxFreq = maxLength(frequency); // generate the rows of the output StringBuffer outputBuf = new StringBuffer(); outputBuf.append("sum count frequency\n"); for (int i = 2; i < rollCount.length; i++) { // first column outputBuf.append(indent(maxIndex, index[i]) + ": "); // now the number of times that i is rolled outputBuf.append(indent(maxCount, count[i]) + " "); // now the frequencies outputBuf.append(indent(maxFreq, frequency[i]) + " %"); if (i < rollCount.length - 1) outputBuf.append("\n"); } return outputBuf.toString(); } private static int maxLength(String[] arr) { int max = arr[0].length(); for (int i = 1; i < arr.length; i++) if (max < arr[i].length()) max = arr[i].length(); 224 KAPITEL 7. ABSTRAKTION VON METHODEN UND DATEN return max; } private static String indent(int max, String str) { StringBuffer strBuf = new StringBuffer(); for (int i = 0; i < max - str.length(); i++) strBuf.append(" "); strBuf.append(str); return strBuf.toString(); } } Der folgende Output für rolls = 360.000 zeigt, dass der Zufallszahlengenerator eine sehr gleichmäßige Verteilung liefert. Jede Augenzahl i = 2, 3, . . . , 12 erscheint mit der zu erwartenden Häufigkeit von (i − 1) · 10000 für i = 2, . . . , 7 bzw. (12 − i + 1) · 10000 für i = 8, 9, . . . , 12. sum 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 7.5 count 9942 19899 29768 39745 50104 60243 50225 40202 29888 19920 10064 frequency 2.76 % 5.52 % 8.26 % 11.04 % 13.91 % 16.73 % 13.95 % 11.16 % 8.30 % 5.53 % 2.79 % Literaturhinweise Die hier gegebene Darstellung lehnt sich stark an [HR94] an. Dies gilt insbesondere für den Zufallszahlengenerator und die Klasse Fraction. Zufallsgeneratoren und Methoden zum Testen des Zufallsverhaltens werden ausführlich in [Knu98a] behandelt. Der hier implementierte Generator geht auf [PM88] zurück. Weitere Beispiele für Klassen und eine ausführliche Beschreibung aller (hier nicht aufgeführten) Feinheiten und Variationen von Klassen in Java finden sich in [Küh99].