Advanced   Java   Services Klassen und Objekte Back Next Up Home


Klassen als Funktionsbibliotheken sind im Grunde keine typischen Klassen. Die eigentlichen Fähigkeiten kommen dabei garnicht zum tragen. Das wird hiermit geändert.


Klassen als (nichtprimitive) Datentypen

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 :


Konstruktoren
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 ;

objektzeiger1.jpg
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:

ObjektZeiger2.jpg

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


Nichtstatische versus statische Methoden

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


Die import-Anweisung

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.*;

Valid XHTML 1.0 Strict top Back Next Up Home