Advanced Java Services | Klassen und Objekte |
Klassen als Funktionsbibliotheken sind im Grunde keine typischen Klassen. Die eigentlichen
Fähigkeiten kommen dabei garnicht zum tragen. Das wird hiermit geändert.
Man kann Klassen sehr unterschiedlich erklären. Eine einfache Möglichkeit ist es, eine Klasse als Datentyp aufzufassen. Eine Klasse ist ein nichtprimitiver Datentyp. Demzufolge kann man von einer Klasse Variablen anlegen. In Analogie etwa zum Datentyp int oder zu einem int-Array kann man folgende Variablen anlegen. Das folgende Beispiel läßt sich problemlos compilieren.
public class NeueDatentypen { public static void main(String[] args) { int a ; char arr[] ; String st; StringBuffer sb ; java.awt.Frame wnd ; java.util.Vector vec ; java.io.FileReader fr ; Math m ; Stdin std; } }
Ebenso wie die Variable a vom primitive Datentyp int sind auch alle anderen Variablen noch nicht initialisiert. Wir haben auch bereits erkannt, daß eine Arrayvariable, hier also arr, eine Referenz darstellt. Dies gilt grundsätzlich für alle nichtprimitiven Datentypen.
Wir wissen bereits, wie man eine Arrayreferenz initialisiert. Wie aber schaut es mit den anderen
Referenzen aus. Den Datentyp String können wir uns noch recht gut vorstellen, "Hello Java" etwa
ist String und vielleicht kann man einen String einfach mit st = "Aha" initialisieren.
Es gibt jedoch mit der Klasse StringBuffer eine zusätzliche Möglichkeit Zeichenketten zu
speichern. String und StringBuffer sind zwar thematisch verwandte Klassen, aber konzeptionell
sehr unterschiedlich. Wie initialisiert man einen Objekt vom Typ StringBuffer ? Ein Frameobjekt ist
ein Fenster, das zunächst im Hauptspeicher realisiert wird und dann bei Bedarf auf dem Bildschirm
angezeigt wird. Ein Vectorobjekt ist eine Art dynamisches Array, in das man (praktisch) beliebig
viele Elemente aufnehmen kann und das sich im Hintergrund bei Bedarf automatisch vergrößert. Ein
FileReaderObjekt stellt nach der Initialisierung eine zum Lesen geöffnete Datei dar. Jedes dieser
Objekte hat einen eigenen Datentyp und damit eine eigene Initialisierungssituation. Und wie diese
Initialisierung genau ausschaut weiß nur derjenige, der die Klasse geschrieben hat.
Wie initialisiert man Objekte, von denen man garnicht weiß, wie man sie initialisieren soll ?
Die Antwort ist verblüffend einfach. Der Entwickler der Klasse muß für seine Klasse einen entsprechenden
Initialisierungsdienst bereitstellen. Nach einem genau festgelegten Schema stattet er seine Klasse
mit speziellen Methoden aus, mit denen der Anwendungsprogrammierer Objekte der Klasse anlegen kann.
Diese speziellen Methoden heißen Konstruktoren. Wir müssen also garnicht Bescheid wissen über das
Innenleben der Objekte, wir initialisieren die Objekte mit Hilfe dieses Dienstes.
Das sieht folgendermaßen aus :
import java.io.*; import java.awt.*; import java.util.*; public class NeueDatentypen2 { public static void main(String[] args) { int a ; char arr[]; String st1, st2 ; StringBuffer sb ; Frame wnd ; Vector vec ; FileReader fr ; Math m ; Stdin std; arr = new char[] { 'a' , 'g' , 'a' , 'p' , 'i' } ; // Array arr wird initialisiert st1 = new String(arr) ; // String st1 wird mit Hilfe von arr initialisiert sb = new StringBuffer(st1); // StringBuffer sb wird mit Hilfe von st1 initialisiert wnd = new Frame("Ich bin ein Fenster"); // ein FensterObjekt mit einem Titel wird initialisiert vec = new Vector(); // ein VectorObjekt wird mit Hilfe eines Defaultkonstruktors initialisiert // fr = new FileReader("klassen3.html") ; // ein FileReaderObjekt wird mit Hilfe eines Konstruktors initialisiert, der // einen Dateinamen als String erhält (d.h. die Datei klassen3.html wird geöffnet) st2 = "Hier brauchen wir ausnahmsweise keinen Konstruktor" ; } }
Im obigen Beispiel ist der Aufruf des Konstruktors der Klasse FileReader auskommentiert. Der Aufruf an sich ist
zwar korrekt, aber in diesem Falle erzwingt der Compiler eine Ausnahmebehandlung und das ist hier noch nicht
unser Thema.
Eine Klasse kann keinen, einen oder mehrere Konstruktoren zur Verfügung stellen. Stellt eine
Klasse keinen Konstruktor bereit, so kann man von ihr keine Objekte anlegen. Allerdings gibt es eine
Ausnahme von dieser Regel. Es gibt Klassen, die an Stelle eines Konstruktors eine statische Methode
bereitstellen, die ein Objekt vom Typ der Klasse zurückliefert. Die Klasse Toolkit etwa ist so eine
Klasse. darauf gehen wir hier aber nicht näher ein. Bietet eine Klasse einen oder mehrere Konstruktoren
an, so werden diese zur Initialisierung verwendet. Wie man oben sieht, erinnert die Syntax stark an die
Initialisierung von Arrays. Nach dem new Operator kommt der Name des
Datentyps bzw. der Klasse. Im Gegensatz zu Arrays werden dann aber runde Klammern, also Methodenklammern
verwendet. Den Konstruktoren können Parameter übergeben werden, aber es gibt auch Konstruktoren
ohne Parameter. Ein parameterloser Konstruktor wird Defaultkonstruktor genannt.
Aus der Dokumentation (API) können wir z.B. entnehmen, daß ein Aufruf wie
new Frame("Ich bin ein Fenster") ein Fenster mit dem übergebenen String als Titel anlegt.
Wie der Konstruktor das im Detail macht, bleibt uns verborgen. In Analogie zu Arrays kann man sich
jedoch folgendes Bild machen.
Im ersten Schritt wird nur die Referenz angelegt.
Frame wnd ;
Nun legen wir das FrameObjekt mit Hilfe eines Konstruktors an:
wnd = new Frame("Ich bin ein Fenster") ;
Nach dem Konstruktoraufruf haben wir die folgende Situation:
Da wir nicht wissen, wie der Konstruktor ein Objekt anlegt, kennen wir auch den Inhalt eines Objekts
nicht. Anders als bei einem Array können wir hier nicht direkt auf den Speicher zugreifen.
Stattdessen bietet eine Klasse Methoden an. Mit diesen Methoden kann man nun auf das Objekt zugreifen.
Je nachdem, mit welchen Methoden der Entwickler die Klasse ausgestattet hat, kann man etwa auf Teile
des Objektes lesend oder schreibend zugreifen. So gibt es für ein FrameObjekt etwa Methoden, um den
Titel zu ändern, oder die Größe des Fensters oder die Farbe. Ganz andere Methoden stellt die Klasse
String bereit. Mit diesen kann man etwa die Länge des StringObjekts erfahren, oder ein bestimmtes
Zeichen im String suchen oder einen StringObjekt mit einem anderen (lexikalisch) vergleichen.
Wie die obige Graphik veranschaulicht, gibt es einen Unterschied zwischen der Referenz (dem Zeiger)
und dem eigentlichen Objekt bzw. Instanz. Es ist gut, sich das einmal klarzumachen. In der Praxis
werden die Begriffe Referenz, Objekt und Instanz aber synonym verwendet, ohne daß das zu
Mißverständnissen führt.
Zusammenfassung
Eine Klasse kann sowohl statische als auch nichtstatische Methoden anbieten. Von den statischen
Methoden wissen wir bereits, daß sie mit dem Namen der Klasse verwendet werden, sie haben also
nichts mit den Objekten der Klasse zu tun. Ganz anders verhält es sich mit den nichtstatischen
Methoden. Will man Eigenschaften eines Objektes darstellen oder verändern, so braucht man Methoden,
die auf genau dieses Objekt wirken. Dazu dienen nichtstatische Methoden. Nichtstatische Methoden
werden immer zu einem Objekt aufgerufen und können auf dieses Objekt wirken. Die gleiche Methode
kann dann bei unterschiedlichen Objekten (der gleichen Klasse natürlich) auch unterschiedliche
Wirkung haben.
Zusammenfassung
Es gibt in der Standardklassenbibliothek JFC mittlerweile über 2500 Klassen. Mal abgesehen davon,
daß man diese nicht alle im Kopf haben kann und deswegen eine Dokumentation unumgänglich ist,
ist es auch notwendig, diese große Anzahl zu ordnen. Dazu haben die Entwickler die Klassen in
verschiedene Pakete gepackt. Ein Paket (package) enthält eine Reihe von Klassen und kann auch
selbst wieder ein Paket enthalten. Die Namen der Pakete sind Teil des Klassennamens, so heißt
die Klasse String genau genommen java.lang.String und die Klasse Random heißt
mit ihrem vollständigen Namen java.util.Random . Spricht man eine Klasse mit ihrem
vollständigen Namen an, so wird sie der Compiler immer als Standardklasse erkennen. Es ist jedoch
sehr umständlich, mit diesen langen Klassennamen zu hantieren. Glücklicherweise ist das auch
fast nie notwendig. Klassen, die im Paket java.lang liegen, findet der Compiler immer, wie man
schon an den obigen Beispielen erkennt. In den anderen Fällen hilft man sich mir einer sog.
import-Anweisung. In der Dokumentation erfährt man den vollständigen Klassennamen
und schreibt vor dem Beginn der Klasse an den Anfang der Datei etwa
import java.util.*;
Damit kann man nun alle Klassen im Package util mit ihrem Hauptnamen ansprechen. Die Paketordnung
ist aufgebaut wie eine Verzeichnisstruktur und * ist ein Paltzhalter für alle Klassen in diesem
Paket. Der *-Mechanismus ist aber nur anwendbar auf der Ebene, in der die Klassen liegen, ein
import java.*; funktioniert nicht. Der * ist also nur ein Platzhalter für Klassen
und nicht für Unterverzeichnisse bzw. Unterpackages. Dazu zwei Beispiele. Will man etwa zur
Bildbearbeitung die Klassen java.awt.Image und java.awt.image.PixelGrabber verwenden,
so braucht man zwei import-Anweisungen:
import java.awt.*;
import java.awt.image.*;