3.2 Klassen benutzen
 
Klassen sind das wichtigste Merkmal objektorientierter Pronramme. Eine Klasse beschreibt die Eigenschaften der Objekte und gibt somit den Bauplan an. Jedes Objekt ist ein Exemplar (auch »Instanz«1
oder »Ausprägung« genannt) einer Klasse.
Eine Klasse definiert
|
Attribute |
|
Operationen |
|
weitere Klassen (innere Klassen) |
Attribute und Operationen heißen auch Eigenschaften4 eines Objekts. Welche Eigenschaften eine Klasse tatsächlich besitzen soll, wird in der Analyse- und Designphase festgesetzt. Dies wird in diesem Buch kein Thema sein; für uns liegen die Klassenbeschreibungen schon vor.
Die Operationen einer Klassendefinition werden in einer Programmiersprache durch Methoden (auch Funktionen genannt) umgesetzt. Die Attribute eines Objekts definieren die Zustände, und sie werden durch Variablen (auch »Felder«2
genannt) implementiert.
Um sich einer Klasse zu nähern, können wir einen lustigen Ich-Ansatz (Objektansatz) verwenden, der auch in der Analyse- und Designphase eingesetzt wird. Bei diesem Ich-Ansatz versetzen wir uns in das Objekt und sagen »Ich bin ...« für die Klasse, »Ich habe ...« für die Attribute und »Ich kann ...« für die Operationen. Meine Leser sollten dies einmal an den Klassen Mensch, Auto, Wurm und Kuchen testen.
3.2.1 Die Klasse Point
 
Bevor wir uns mit eigenen Klassen beschäftigen, wollen wir zunächst einige Klassen aus der Standardbibliothek kennen lernen. Eine dieser Klassen ist Point. Sie beschreibt einen Punkt in einer zweidimensionalen Ebene durch die Koordinaten x und y und bietet einige Operationen an, mit denen sich Punkt-Objekte verändern lassen. Testen wir einen Punkt wieder mit dem Objektansatz:
Klassenname
|
Ich bin ein Punkt.
|
Attribute
|
Ich habe eine x- und y-Koordinate.
|
Operationen
|
Ich kann mich verschieben und meine Position festlegen.
|
Für die Darstellung einer Klasse lässt sich Programmcode verwenden, also eine Textform, oder aber eine grafische Notation. Eine dieser grafischen Beschreibungsformen ist die UML. Grafische Abbildungen sind für Menschen deutlich besser zu verstehen und erhöhen die Übersicht.
Im ersten Abschnitt des UML-Diagramms lassen sich die Attribute ablesen, im zweiten die Methoden. Das + vor den Eigenschaften zeigt an, dass sie öffentlich sind und jeder sie nutzen kann. Die Typenangabe ist gegenüber Java umgekehrt: Zuerst kommt der Name der Variable, dann der Typ beziehungsweise bei Methoden der Typ des Rückgabewerts.
 Hier klicken, um das Bild zu Vergrößern
Abbildung 3.1
Die Klasse java.awt.Point in der üblichen UML-Darstellung und in der ‚Eclipse-Darstellung’
3.2.2 Etwas über die UML
 
Die UML (Unified Modeling Language) ist mehr als nur eine Notation zur Darstellung von Klassen. Mit ihrer Hilfe lassen sich die Analyse und das Design im Softwareentwicklungsprozess beschreiben. Mittlerweile hat sich die UML jedoch zu einer allgemeinen Notation für andere Beschreibungen entwickelt, zum Beispiel für Datenbanken oder Workflow-Anwendungen.
Vor der UML waren andere Darstellungsvarianten wie OMT oder Booch verbreitet. Diese waren eng mit einer Methode verbunden, die einen Entwicklungsprozess und ein Vorgehensmodell beschrieb. Methoden versuchen eine Vorgehensweise beim Entwurf von Systemen zu beschreiben, etwa »erst Vererbung einsetzen und dann die Attribute finden« oder »erst die Attribute finden und dann mit Vererbung verfeinern«. Bekannte OO-Methoden sind etwa Shlaer/Mellor, Coad/Yourdon, Booch, OMT und OOSE/Objectory. Aus dem Wunsch heraus, OO-Methoden zusammenzufassen, entstand die UML. Anfangs stand die Abkürzung noch für Unified Method. Die Urversion 0.8 war die erste Veröffentlichung im Jahre 1995. Die Initiatoren waren Jim Rumbaugh und Grady Booch. Später trat Ivar Jacobson dazu, und die drei »Amigos« erweiterten die UML, die in der Version 1.0 bei der Object Management Group (OMG) als Standardisierungsvorschlag eingereicht wurde. Die Amigos nannten die UML nun »Unified Modeling Language«, was deutlich macht, dass die UML keine Methode ist, sondern lediglich eine Modellierungssprache. Folgende Tabelle gibt eine Kurzübersicht über die Veränderungen der UML:
1994
|
Booch und Rumbaugh vereinigen ihre Ansätze OOAD und OMT.
|
1995
|
Unified Method in der Version 0.8. Jacobson bringt OOSE mit ein, die UML wird vereinfacht, von einer einheitlichen Methode wird abgesehen.
|
1996
|
Die UML in der Version 0.9. Notation wird verfeinert, unter anderem von vielen Unternehmen wie Oracle, Microsoft, Digital und HP.
|
1997
|
Versionen 1.0 und 1.1 erscheinen. Die UML wird bei der OMG eingereicht. Im September wird die UML 1.1 zum Standard.
|
1998
|
Version 1.2 mit Detailverbesserungen und einigen Korrekturen
|
1999
|
Die UML in der Version 1.3
|
2000
|
UML 1.4
|
2004
|
UML 2.0. Augenmerk auf XML Metadata Interchange (XMI), Model Driven Architecture (MDA), Geschäftsprozessmodellierung (BPM) und Unterstützung von Echtzeitmodellierung (RT) durch neue Diagrammtypen
|
Eine aktuelle Version des Standards lässt sich unter http://www.rational.com/uml/ beziehen.
Diagramme in der UML
In der UML werden unterschiedliche Diagramme definiert, die die unterschiedlichen Sichten auf die Software beschreiben. Für die einzelnen Phasen im Softwareentwurf sind unterschiedliche Diagrammtypen wichtig. Wir wollen kurz drei Diagramme und ihr Einsatzgebiet besprechen.
Use-Cases
Die Use-Cases-Diagramme entstehen meist während der Anforderungsphase und beschreiben die Geschäftsprozesse, indem die Interaktion von Personen oder von bereits existierenden Programmen mit dem System dargestellt werden. Die handelnden Personen oder aktiven Systeme werden Aktoren genannt. Ein Use-Case beschreibt dann eine Interaktion mit dem System. Dazu werden die Aktoren als Männchen gemalt (wobei die Geschlechter nicht zu erkennen sind) und die einzelnen Anwendungsfälle (Use-Cases) als Ellipsen.
Klassendiagramme
Für die statische Sicht auf einen Programmentwurf sind Klassendiagramme einer der wichtigsten Diagrammtypen. Sie sind besonders interessant, da Hilfsprogramme aus diesen Diagrammen automatisch Teile des Quellcodes erzeugen können. Die Diagramme stellen zum einen die Elemente der Klasse dar, zum anderen die Beziehungen der Klassen untereinander. Die Diagramme werden in diesem Buch häufiger eingesetzt. Klassen werden als Rechteck dargestellt und die Beziehungen zwischen den Klassen durch Linien angedeutet.
Interaktionsdiagramme
Der Begriff umfasst zwei Unterdiagramme zur Darstellung der zeitlichen Abläufe eines Systems, die Sequenzdiagramme und die Kollaborationsdiagramme. Damit wird im Gegensatz zum Klassendiagramm das dynamische Verhalten von Objekten dargestellt.
3.2.3 Anlegen eines Exemplars einer Klasse mit new
 
Von der Klasse Point werden zur Laufzeit Exemplare erzeugt, die Point-Objekte. Eine Klasse beschreibt also, wie ein Objekt aussehen soll. In einer Mengen- beziehungsweise Element-Beziehung ausgedrückt entsprechen Objekte den Elementen und Klassen den Mengen, in denen die Objekte als Elemente enthalten sind.
Wir verbinden nun einen Variablennamen mit der Klasse und definieren beziehungsweise deklarieren somit eine Variable, die eine Referenz auf ein Point-Objekt (ein Element der Klasse Point) erzeugt.
Beispiel Definiere die Variable p vom Typ Point
Point p;
|
Vergleichen wir dies mit der bereits bekannten Deklaration einer Variablen für einen ganzzahligen Wert.
int i;
Links steht in beiden Fällen der Typ und rechts der Name der Variable.
Im oberen Beispiel deklarieren wir eine Variable p und teilen dem Compiler mit, dass diese Variable Referenzen auf Objekte vom Typ Point speichern soll. Falls es sich bei p um eine Objekt- oder Klassenvariable handelt, wird p anfangs mit der Null-Referenz (null) initialisiert, die auf kein Objekt verweist; als lokale Variable hätte p keinen vordefinierten Wert, sie ist undefiniert. Referenztypen können nicht in primitive Typen konvertiert werden und umgekehrt.
Der new-Operator
Durch die Deklaration einer Variablen mit dem Namen einer Klasse als Typ wird noch kein Exemplar erzeugt. Dazu müssen wir mit dem new-Operator explizit ein Objekt erzeugen. Hinter dem new-Operator folgt immer der Name der Klasse, von der ein Exemplar erzeugt werden soll, und ein Paar Klammern. Wir werden später sehen, dass hier ein spezieller Methodenaufruf (Konstruktoraufruf) stattfindet, bei dem wir auch Werte übergeben können.
Beispiel Anlegen eines Objekts und Speichern der Referenz in der Variablen p
p = new Point();
|
Das tatsächliche Punkt-Objekt wird erst dynamisch, also zur Laufzeit, mit new erzeugt. Damit stellt das System Speicher für ein Point-Objekt bereit und speichert eine Referenz auf diesen reservierten Speicherblock in der Variablen p ab.
Die Deklaration der Variablen p und die separate Erzeugung eines Exemplars der Klasse Point lassen sich, wie bei der Deklaration primitiver Datentypen, auch kombinieren.
Beispiel Deklaration mit Initialisierung
double pi = 3.1415926535;
Point p = new Point();
|
 Hier klicken, um das Bild zu Vergrößern
(Strg)+(1) ermöglicht, entweder eine neue lokale Variable oder Objektvariable für den Ausdruck anzulegen.
 Hier klicken, um das Bild zu Vergrößern
3.2.4 Zugriff auf Variablen und Methoden mit dem ».«
 
Die in einer Klasse definierten Variablen werden »Objektvariablen« (auch »Exemplar-, Instanz- oder Ausprägungsvariablen«) genannt. Wird ein Objekt geschaffen, dann erhält es seinen eigenen Satz von Objektvariablen3
. Sie bilden einen Zustand.
Ist das Objekt angelegt, wird auf die Methoden oder Variablen mit einem ».« zugegriffen. Der Punkt (auch »Selektor« genannt) ist ein Operator und steht zwischen einem Ausdruck, der eine Referenz zurückgibt4
, und der Objekteigenschaft. (Der Punkt als Operator hat natürlich nichts mit der gleichnamigen Klasse zu tun.)
Beispiel Folgende Zeilen erzeugen ein Point-Objekt, speichern eine Referenz auf dieses Objekt in der Variablen p und weisen den Objektvariablen x und y Werte zu.
Point p = new Point();
p.x = 12;
p.y = 45;
|
 Hier klicken, um das Bild zu Vergrößern
(Strg)+(_____) zeigt an, welche Eigenschaften eine Referenz definiert. Eine Auswahl mit Return wählt die Eigenschaft aus und setzt insbesondere bei Funktionen den Cursor zwischen das Klammerpaar.
 Hier klicken, um das Bild zu Vergrößern
Der Typ links vom Punkt muss immer eine Referenz sein. Im Prinzip funktioniert auch Folgendes:
new Point().x = 1;
Dies ist allerdings unsinnig, da zwar das Objekt erzeugt und ein Attribut gesetzt wird, anschließend aber der Garbage-Collector das Objekt wieder wegräumt. Für einen Methodenaufruf kann dies schon sinnvoller sein.
Ein Methodenaufruf gestaltet sich genau so einfach wie eine Objekterzeugung. Hinter dem Ausdruck mit der Referenz und dem Punkt folgt der Methodenname. Das nachfolgende Beispiel ist lauffähig und bindet zugleich noch die Point-Klasse aus dem Paket java.awt ein. Ein Paket ist eine Gruppe zusammengehöriger Klassen.
Listing 3.1
MyPoint.java
import java.awt.Point;
class MyPoint
{
public static void main( String args[] )
{
Point p = new Point();
p.x = p.y = 12;
p.setLocation( –3, 2 );
System.out.println( p.toString() ); // java.awt.point[x=-3,y=2]
// alternativ
// System.out.println( p );
}
}
Die letzte Anweisung ist gültig, da println() bei einem Objekt automatisch die toString()-Methode aufruft.
MyPointUsesPoint
Abbildung 3.2
Die eigene Klasse MyPoint nutzt java.a.wt.Point, was als Abhängigkeit im UML-Diagramm angezeigt werden kann. Die Parameter und Rückgabetypen sind in UML optional und hier nicht dargestellt.
 Hier klicken, um das Bild zu Vergrößern
(Strg)+(_____) auf einem Eigenschaftennamen (oder bei einer Funktion im Klammerpaar) bringt die API-Dokumentation hervor. Dazu muss allerdings das Java SDK eingebunden sein – das JRE reicht nicht, da bei ihm keine Dokumentation zu finden ist.
 Hier klicken, um das Bild zu Vergrößern
Nach dem Punkt geht’s weiter
Die Funktion toString() liefert als Ergebnis ein String-Objekt, das den Zustand des Punkts preisgibt.
Point p = new Point();
String s = p.toString();
System.out.println( s ); // java.awt.Point[x=0,y=0]
System.out.println( s.length() ); // 23
Das String-Objekt besitzt selbst wieder Methoden, eine davon ist length(), die die Länge der Zeichenkette liefert. Das Erfragen des String-Objekts und dessen Länge können wir verbinden zu einer Anweisung – p sei wieder unser Point-Objekt.
System.out.println( p.toString().length() );
Der Punkt-Operator für den Zugriff auf Objekt- oder Klassenvariablen beziehungsweise -methoden sollte nicht durch Freiraum von dem Klassennamen oder dem Ausdruck für die Objektreferenz oder dem Methoden- beziehungsweise Variablennamen abgetrennt werden. So ist es vernünftiger, anstatt
p. toString() . length()
die Anweisung mit p.toString().length() zu programmieren. Obwohl der Java-Compiler auch Programmcode mit eingebetteten Kommentaren und Zeilenvorschüben akzeptiert, ist von Konstruktionen wie
p./* um wen geht es */toString()/* ich bin’s */.
/*meine Länge*/length()
dringend abzuraten.
3.2.5 Konstruktoren
 
Werden Objekte mit dem new-Operator angelegt, so wird ein Konstruktor aufgerufen, eine Art Methode mit besonderer Signatur5
. Bei der Schaffung eines Objekts sollen in der Regel die Objektvariablen initialisiert werden. Diese Initialisierung wird dazu in den Konstruktor gesetzt, um sicherzustellen, dass das neue Objekt einen sinnvollen Anfangszustand aufweist.
Ein Konstruktor ohne Argumente ist der Standard-Konstruktor (auch »Default-Konstruktor«, selten »No-Arg-Konstruktor« genannt).
Beispiel Folgende Zeilen erzeugen schlussendlich zwei Point-Objekte mit denselben Koordinaten. Die Variablen p und q referenzieren jedoch zwei völlig getrennte Objekte; lediglich die Belegung der x- und y-Koordinaten ist bei den beiden Objekten »zufällig« gleich.
Point p = new Point();
p.setLocation( 10, 10 );
Point q = new Point( 10, 10 );
Der erste Konstruktor ist der Standard-Konstruktor, der Zweite ein parametrisierter Konstruktor.
|
Was bei new passiert
Ein Konstruktoraufruf wird bei der Erschaffung eines Objekts durch den new-Operator ausgelöst. So erzeugt
Point p = new Point();
ein Exemplar der Klasse Point. Die Laufzeitumgebung von Java reserviert so viel Speicher, dass ein Point-Objekt dort Platz hat. Anschließend ruft die Laufzeitumgebung den Konstruktor auf und gibt eine Referenz auf das Objekt zurück, die im obigen Beispiel der Variablen p zugewiesen wird. Kann das System nicht genügend Speicher bereitstellen, so wird der GC aufgerufen. Kann dieser keinen freien Platz finden, generiert die Laufzeitumgebung einen OutOfMemoryError.
1 Ich vermeide das Wort »Instanz« und verwende dafür durchgängig im Tutorial das Wort »Exemplar«. An die Stelle von »instanziieren« tritt das einfache Wort »erzeugen«. Instanz ist eine irreführende Übersetzung des englischen Ausdrucks »instance«.
2 Den Begriff »Feld« benutze ich im Folgenden nicht. Er bleibt für Arrays reserviert.
3 Es gibt auch den Fall, dass sich mehrere Objekte eine Variable teilen, so genannte statische Variablen. Diesen werden wir später betrachten.
4 Sprachlich wird diese Formulierung gerne abgekürzt zu »Rechts steht eine Referenz«.
5 Ein Konstruktor hat keinen Rückgabetyp und heißt auch immer so wie die Klasse.
|