Computerorientierte Mathematik I - homepages.math.tu

Werbung
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].
Herunterladen