3 Die Programmiersprache Java
3.1 Java als OOP-Umgebung
Java ist eine objektorientierte Sprache.
Im Gegensatz zur prozeduralen Programmierung, wo versucht wird,
ein komplexes Problem in immer kleinere Teilaufgaben zu zerlegen,
um diese dann als Prozeduren zu implementieren, wird bei der objektorientierten
Programmierung versucht, mit Hilfe von Objekten eine Abstraktion
der Realität zu gewinnen.
3.1.1 Klassen und Objekte
Wenn wir uns umsehen, entdecken wir die verschiedensten Objekte wie Autos,
Häuser oder Kaffeemaschinen. Sie alle besitzen ein Reihe
von Eigenschaften und bieten uns auf der anderen Seite eine bestimmte
Funktionalität. Genauso ist das mit Objekten in einem Programm.
Deshalb wird beim Programmieren versucht die Realität durch
Programmobjekte abzubilden, Objekte werden zu Klassen zusammengefaßt.
Das hat u. a. den Vorteil das man bei umfangreichen Programmen
den Überblick über die zahlreichen Objekte behält.
Eine Klasse definiert die gemeinsamen Eigenschaften und Funktionalitäten
dieser Objekte. Ein Objekt ist dann eine Instanz einer Klasse,
bzw. eine spezielle Ausprägung, und wird erst während
der Abarbeitung erzeugt, und besitzt genau die beschriebenen Eigenschaften,
die sich wiederum in Verhalten und Zustand gliedern.
Erzeugen von Objekten
Wie man ein Objekt erzeugt, zeigt das folgende Beispiel:
Date today = new Date();
In diesem Beispiel wird eine Variable namens today mit dem Datentyp Date
erzeugt. Durch new Date() wird ein neues Objekt initialisiert.
Nach dem oben aufgeführten Beispiel würden Zeichenketten und
Felder folgendermaßen dargestellt:
String x = new String ("Das ist eine Zeichenkette");
int [] [] y = new int [ 5 ] [];
for (int i = 0; i < 5; i++) y[i] = new int [i+1];
Die letzte Schleife erzeugt ein dreieckiges Feld.
Für Zeichenketten und Felder existiert jedoch eine besondere Variante der Objekterzeugung,
nämlich die Angabe eines Literals. Das obige Beispiel abgewandelt:
String x = "Das ist eine Zeichenkette";
int[] [] y = {{1}1, {1,2}, {1,2,3}, {1,2,3,4}};
In diesem Beispiel ist zu erkennen, daß die Feldkomponenten von y
schon mit Werten vorbelegt sind.
Löschen von unbenutzten Objekten
Viele objektorientierte Sprachen verlangen, daß man alle
Objekte verfolgt, und diese dann löscht sobald sie nicht
mehr benötigt werden. Ein Programm zu schreiben womit man
Speicher verwalten möchte, ist auf diesem Weg doch recht
ermüdend und fehlerträchtig. Java bewahrt uns davor,
indem es erlaubt so viele Objekte zu erzeugen wie man möchte
(nur begrenzt durch Systemeigenschaften), die man aber nicht löschen
muß. Die Java-Laufzeitumgebung löscht die Objekte
wenn es Sicher ist, daß diese nicht mehr länger benötigt
werden. Dieser Vorgang ist bekannt als Garbage Collection (Müll-Sammlung).
Ein Objekt wird dann als geeignet für die Garbage Collection
bezeichnet, wenn zu dem Objekt keine Beziehungen mehr bestehen.
Erzeugen von Klassen
In Java deklariert der Programmierer Klassen
mit dem Schlüsselwort class. Danach folgt der Name der Klasse
die man definieren möchte. Der folgende Text deklariert eine
neue Klasse mit dem Namen ImaginaryNumber:
class ImaginaryNumber{
. . .
}
Deklarieren einer Superklasse
In Java hat jede Klasse eine Superklasse. Um eine Superklasse zu
deklarieren ist das Schlüsselwort extends plus dem Namen
der Superklasse erforderlich. Angewandt auf das Beispiel mit der
Klasse ImaginaryNumber:
class ImaginaryNumber extends Number {
. . .
}
Jetzt ist die Klasse Number die Superklasse von
ImaginaryNumber (bzw. ImaginaryNumber die Subklasse von Number).
Wichtig zu erwähnen ist noch, daß die Klasse Number
teil des java.lang-Paketes und die Basisklasse für Natürliche- (Integer), Fließkommazahlen (Float) und andere Werte ist.
Wenn keine Superklasse angegeben wird, ist die Superklasse automatisch
die Objektklasse. Die Objektklasse ist die oberste Klasse im Hierarchie-Baum
in der Java-Entwicklungsumgebung. Jede Klasse im Javasystem
ist ein direkter oder indirekter Abkömmling von der Objektklasse.
Die Objektklasse definiert den Basiszustand und das Verhalten,
die alle Objekte haben müssen wie z. B. die Fähigkeit
sich selbst mit einem anderen Objekt zu vergleichen, eine Zeichenkette
zu konvertieren, auf eine Zustandsvariable zu warten, andere Objekte
zu benachrichtigen wenn sich eine Zustandsvariable verändert
hat.
Der Rumpf einer Klasse
Der Rumpf einer Klasse kann zwei verschiedene
Bereiche enthalten: Variablendeklarationen und Methoden. Eine
Variable der Klasse (Membervariable) repräsentiert den Zustand
der Klasse, und seine Methode implementiert das Verhalten der Klasse.
An einem Beispiel soll dies deutlich werden:
class beispiel{
/*Membervariable*/
static String text = "Dies ist ein Beispiel";
/*Methode*/
static public void main (String args[]) {
System.out.println(text);
}
}
3.1.2 Vererbung
Wenn eine Klasse nur wenige abstrakte
Eigenschaften beschreibt, lassen sich ihr sehr viele Objekte zuordnen.
Als Beispiel sei hier die Klasse Fahrzeug angegeben. In dieser
Klasse befinden sich alle Fahrzeuge wie Zweirad, PKW und LKW.
Die zunehmende Präzisierung der Eigenschaften führt
dazu, daß man zu neuen Klassen kommt. Diese haben zwar die
gleichen Grundeigenschaften, unterscheiden sich aber trotzdem
in Bezug auf andere Eigenschaften. Eine hierarchische Struktur
ist dann die Folge. Die untergeordneten Klassen übernehmen
Eigenschaften und Funktionalität der übergeordneten
Klasse. Untergeordnete Klassen werden als Unter- oder Subklassen,
übergeordnete als Ober- oder Superklassen bezeichnet. Als
Beispiel dient hier folgende Abbildung:

Bild 3.1: Klassenhierachie
Der Vorteil dieses Konzepts ist,
daß man beim Erstellen einer abgeleiteten Klasse nur die
Unterschiede zu deren Superklasse spezifizieren muß. Außerdem
können bei den Subklassen, zu den bereits von der Superklasse
geerbeten, Variablen und Methoden hinzugefügt werden. Auch
das Überschreiben (override) von geerbten Methoden ist möglich.
Zusammenfassend gesagt, mit dem Konzept der Vererbung ist man
in der Lage, möglichst viel Programmcode wiederverwendbar
zu machen, und damit die Programmierung insgesamt effizienter
zu gestalten. In Java kennzeichnet das Schlüsselwort extends
die Vererbungsbeziehung (siehe 3.1.1; Deklarieren einer Superklasse).
Mehrfachvererbung und Schnittstellen
Unter Mehrfachvererbung versteht man, daß
eine Klasse von mehreren Klassen (Elternklassen) gleichzeitig
erben kann. Die Mehrfachvererbung ist als solches in Java nicht
vorhanden. In Java gibt es einen Ersatzmechanismus, die sogenannten
Interfaces, welche als Zusammenfassung von Konstanten und Methodendeklarationen
verstanden werden können. Die Schnittstellen entsprechen
in etwa einer abstrakten Klasse. In einer Klasse können mehrere
Schnittstellen implementiert werden, indem man ihren abstrakten
Methoden eine Funktionalität zuweist. Somit regeln die Schnittstellen
das Verhalten der Klassen nach außen. Dies ist besonders
wichtig, um eine vereinheitlichte Sprache für alle Klassen
zu finden, welche in einem Programm vorkommen. Sollten nun alle
Klassen eines Programms eine bestimmte Schnittstelle implementieren,
und deren Methodendeklaration mit Code ausfüllen, können
die Daten sämtlicher Objekte aller Klassen mit dem gleichen
Befehl angesprochen werden. In Java werden Schnittstellen mit
dem Schlüsselwort interface deklariert und sie werden mit
implements in anderen Klassen eingebunden.
3.1.3 Abstraktion, Kapselung, Polymorphismus
Abstraktion
Zur Bewältigung komplexer Sachverhalte
wendet man das Prinzip der Abstraktion an. Durch Vernachlässigung
von Details, das heißt dem Herausheben des Wesentlichen,
schafft man ein überschaubares System, dessen grundlegende
Merkmale und Funktionsweisen leichter verständlich sind.
Abstrakte Klassen
Manchmal repräsentiert eine Klasse,
die man definiert hat, ein abstraktes Konzept, und als solches
kann es nicht instanziert werden. Als Beispiel dient hier die
Nahrung. Eine Instanz der Nahrung gibt es nicht, jedoch eine Instanz
der Karotten, Äpfel, Schokolade, usw. Nahrung repräsentiert
das abstrakte Konzept von Dingen die man essen kann. Ähnlich
ist es in der objektorientierten Programmierung, in der man ein
abstraktes Konzept bilden möchte, jedoch nicht in der Lage
ist eine Instanz von ihr zu erzeugen. Wie z.B. die NumberClass
in dem java.lang Paket, die das abstrakte Konzept von Zahlen repräsentiert.
Die NumberClass macht nur als Superklasse von Klassen wie Integer
und Float sinn. Klassen, wie die NumberClass, welche abstrakte
Konzepte implementiert, die nicht instanziert werden sollten,
werden abstrakte Klassen genannt. Um eine Klasse in Java als abstrakte
Klasse zu deklarieren wird das Schlüsselwort abstract benötigt:
abstract class Number {
. . .
}
Abstrakte Methoden
Eine abstrakte Klasse kann abstrakte Methoden
enthalten. Abstrakte Methoden sind Methoden ohne Ausführung.
Kapselung
Wie bei der Vererbung dient das Konzept der Kapselung
der Erstellung wiederverwendbaren Programmcodes. Kapselung bedeutet,
daß nicht alle Bestandteile eines Objekts nach außen
sichtbar sind. Vielmehr unterscheidet man zwischen der Schnittstelle
eines Objekts (bzw. der Klasse) und deren Implementierung. Ziel
ist es, alle, von anderen Objekten nicht benötigten Daten
und Methoden einer Klasse, zu verbergen. Dieses Konzept wird auch
"information hiding" genannt. Daraus ergeben sich zwei
Vorteile:
Ein Programmierer, der die Klasse nicht implementiert hat,
sie aber verwenden will, muß deren Implementierungsdetails
nicht kennen und verstehen.
Der Compiler sorgt bei entsprechender Unterstützung dieses
Konzepts dafür, daß ein versehentliches Manipulieren
interner Daten nicht möglich ist.
Die Definition der Klassen kann nun in zwei Bereiche gegliedert werden:

Bild 3.2: Schnittstelle und Implementierung
Im strengen Sinne darf die Schnittstelle eines Objekts nur Methoden
enthalten. Daten dürfen dann nur durch das Zustellen von
Nachrichten (dem Aufruf von Methoden) verändert werden. Java
gibt dem Programmierer eine ganze Reihe von Modifizierern zur
Hand, um eine saubere Trennung zwischen Schnittstelle und Implementierung
verwirklichen zu können. Für die Definition von Variablen
und Methoden stehen drei Modifizierer zur Verfügung, die
den Zugriff auf diese Elemente regeln:
public:
Elemente, die public deklariert sind, unterliegen keiner
Zugriffsberechtigung. Überall dort, wo die Klasse bekannt
ist, können diese Elemente angesprochen werden.
protected:
Um auf ein protected-deklariertes Element zugreifen
zu können, muß mindestens eine der folgenden Bedingungen
erfüllt sein:
- der Zugriff erfolgt innerhalb einer Klasse, welche von der Klasse, die dieses
Element deklariert, abgeleitet ist,
- die Definition der zugreifenden Klasse befindet sich im gleichen
Paket (package) wie die Klasse, die dieses Element beinhaltet.
private:
Derart deklarierte Objekte unterliegen der strengsten
Zugriffsbeschränkung. Nur Elemente der gleichen Klasse können
auf ein private-deklariertes Objekt zugreifen.
Diese drei Modifizierer schließen sich gegenseitig aus. Es kann je
Element immer nur eine der drei Varianten gewählt werden. Ist ein
Element weder public noch protected noch private deklariert, entspricht
dies einer public-Deklaration.
Polymorphismus
Der Begriff Polymorphismus kommt aus dem Griechischen und bedeutet soviel
wie "viele Formen haben". Als Polymorphismus bezeichnet
man die Möglichkeit, mit derselben Nachricht verschiedene
Aktionen auszulösen. Oder anders ausgedrückt, unterschiedliche
Objekte können auf die gleiche Botschaft mit unterschiedlichen
Methoden reagieren. Dieses Konzept korrespondiert direkt mit dem
Konzept der Abstraktion. So kann die gleiche Nachricht bei verschiedenen
Objekten unterschiedlich bearbeitet werden. Im Alltag verwenden
wir gleiche Beziehungen für unterschiedliche Aktivitäten.
Wir fahren mit dem Auto, aber wir fahren auch mit dem Fahrrad,
obwohl die beiden Tätigkeiten ziemlich verschieden sind.
Polymorphismus spielt bei der Vererbung eine große Rolle.
Abgeleitete Klassen implementieren Methoden häufig anders
als ihre Superklasse.
3.1.4 Aufbau eines einfachen Java-Programms
/* HelloWorld.java */
class HelloWorld {
static public void main (String args[]){
System.out.println ("Hello World");
}
}
Bild 3.3: Programm "Hello World"
An diesem Beispiel sind bereits die wichtigsten Bestandteile eines Java-Programms zu erkennen. Der eigentliche Programmcode besteht nur aus einer
Klassendefinition. Zwar kann man in einer Datei auch mehrere Klassen
definieren, es ist aber üblich, bei nicht trivialen Klassen,
für jede Klassendefinition eine eigene Datei, auch Übersetzungseinheit
genannt, anzulegen. Am Anfang der Datei steht immer ein Kommentar.
Neben
den Klassendefinitionen und Kommentaren kann eine Übersetzungseinheit
noch weitere Elemente enthalten. Sie sollen hier nur der Vollständigkeit
wegen erwähnt werden:
- Importdeklarationen teilen dem Compiler mit, welche anderen
Klassen noch benötigt werden,
- Interfacedefinitionen sind Klassendefinitionen sehr ähnlich,
weisen aber einige Besonderheiten auf,
- Package-Deklarationen dienen dazu, mehrere Übersetzungseinhei-
ten zu einer Gruppe zusammenzufassen.
In dem obigen Beispiel erkennt man als erstes Element einen Kommentar.
Java kennt verschiedene Arten von Kommentaren, die sich auch in
ihrer Syntax unterscheiden.
Kommentare, wie in der Programmiersprache C:
Diese Kommentare beginnen mit den Zeichen /* und enden mit den Zeichen */. Zwischen
Kommentarbeginn und -ende dürfen alle Zeichen mit Ausnahme
von /* und */ auftreten. Sie können sich aber durchaus über
mehrere Zeilen erstrecken.
Kommentare zur automatischen Dokumentationsgenerierung:
Im Vergleich zu den eben erwähnten Kommentaren gibt es hier
syntaktisch nur einen kleinen Unterschied. Anstelle der Zeichen
/* dienen die Zeichen /** zur Definition des Kommentarbeginns.
Derartige Kommentare stehen vor einer Deklaration und werden von
einem Werkzeug zur automatischen Dokumentationsgenerierung verwendet.
einzeilige Kommentare wie sie in C++ verwendet werden:
Analog zu C++ beginnen diese Kommentare mit den Zeichen // und enden
mit der Zeile (Kommentarende ist das Zeichen 'Newline').
Für alle drei Kommentararten gilt, daß sie nicht geschachtelt
werden können und innerhalb von Stringkonstanten keine Bedeutung
haben. Die einzelnen Konstrukte haben folgende Bedeutung:
class HelloWorld
Implizit wird von der Superklasse aller Klassen
Object eine Subklasse mit Namen HelloWorld erzeugt.
static public void main
Die Methode ist nur einmalig (static)
im System vorhanden. Die Methode steht zur Verfügung unabhängig
davon, ob es auch Objekte zu dieser Klasse gibt. Demgegenüber
sind virtuelle Methoden den einzelnen Instanzen zugeordnet. Mit
public wird der Zugriff auch aus anderen Objekten auf die aktuelle
Klasse freigeschaltet. Der Bezeichner void gibt wie bei C++/C
an, daß für diese Methode kein Rückgabewert vorgesehen
ist.
String args[]
Anstelle über die Parameter argc und argv
wie bei C und C++ werden die Argumente für das Programm über
ein Feld von Zeichenketten übergeben.
System.out.println ("Hello World");
Hinter System verbirgt sich die Klasse, die alle direkten Schnittstellen zur
aktuellen Plattform verwaltet. Mit System.out wird der Standardausgabekanal
(vergleichbar mit stdout) referenziert. Die Methode println wird
für die Ausgabe von Zeichenketten mit abschließendem
Zeilenvorschub verwendet.
3.2 Java im Detail
3.2.1 Variablen
Variablendeklarationen halten sich an die C-Syntax und bestehen im wesentlichen aus der Typangabe und einem Bezeichner, gefolgt von einem Semikolon.
Man kann innerhalb einer Deklaration mehrere Variablen des gleichen
Typs anlegen und diese auch direkt mit einem Anfangswert initialisieren.
Variablen, die bei der Deklaration nicht initialisiert worden
sind, bekommen automatisch den Standardwert zugewiesen (z.B. bei
ganzzahligen Datentypen den Wert `0`). Konstanten werden mit dem
Schlüsselwort final deklariert.
3.2.2 Typen
Bei den Datentypen unterscheidet Java zwischen Standardtypen und Referenztypen. Standardtypen sind: byte(8-Bit), char(16-Bit), short(16-Bit),
int(32-Bit), long(64-Bit), float(32-Bit), double(64-Bit), boolean,
die fast alle (außer boolean) in C bekannt sind. Zeichen
werden im Unicode(16-Bit) gespeichert und nicht im gewohnten ASCII-Code
(8-Bit). Zu den Referenztypen gehören Klassen-, Schnittstellen- (werden
später erklärt) und Feldtypen. Klassen und Schnittstellen
werden in Java auch als Typen bezeichnet, wobei diese aus den
bereitgestellten, oder aus eigenen Bibliotheken importiert werden
können. In Java gibt es z.B. den Klassentyp String, mit dem
Zeichenketten verwaltet werden.
Feldtypen sind Felder und werden mit eckigen Klammern gekennzeichnet. Die Referenztypen kann man
mit den Zeigern in C vergleichen. Eine Deklaration einer Referenzvariablen
sorgt im Allgemeinen nicht dafür, daß für diese
Variable ein entsprechendes Objekt oder Vektor angelegt wird.
Siehe Abschnitt "Dynamische Speicherverwaltung".
3.2.3 Operatoren
Es folgt eine Auflistung der Operatoren in der Rangfolge,
in welcher sie ausgewertet werden und mit den Datentypen, auf
die sie angewendet werden können [5] [5]:
Rang: Operator: Datentyp: Assoz. Beschreibung
1 ++ numerisch R unäre Pre- bzw. Postinkrementierung
-- numerisch R unäre Pre- bzw Postinkrementierung
+ numerisch R unäres Plus (Vorzeichen positiv)
- numerisch R unäres Minus (Vorzeichen negativ)
ganzzahlig R unäres bitweises Komplement (NOT)
! boolean R unäres logisches Komplement (NOT)
(Typ:) alle R casten in einen anderen (passenden)
Typ
2 * numerisch L Multiplikation
/ numerisch L Division
% ganzzahlig L Rest einer Division (Modulo)
3 + numerisch L Addition
- numerisch L Subtraktion
+ String L Addition von Strings
4 << ganzzahlig L bitweise nach links schieben
>> ganzzahlig L bitweise nach rechts schieben mit
Vorzeichen
>>> ganzzahlig L bitweise nach rechts schieben ohne
Vorzeichen
5 < numerisch L kleiner als
> numerisch L größer als
<= numerisch L kleiner oder gleich
>= numerisch L größer oder gleich
instanceof Objekte L Typüberprüfung
6 = = einfach L gleicher Wert
!= einfach L ungleicher Wert
= = Objekte L referenziert gleiches Objekt
!= Objekte L referenziert nicht das gleiche Objekt
7 & ganzzahlig L bitweises und (AND)
& boolean L logisches und (OR)
8 ^ ganzzahlig L bitweises exklusives oder (XOR)
^ boolean L logisches exklusives oder (OR)
9 | ganzzahlig L bitweises oder (OR)
| ganzzahlig L logisches oder (OR)
10 && boolean L logisches und (AND)
11 !! boolean L logisches oder (OR)
12 ?: boolean R bedingte Ausführung eines beliebigen
Ausdrucks
13 = Variable R Zuweisung eines passenden Wertes
*= numerisch R Zuweisung mit Multiplikation
/= numerisch R Zuweisung mit Division
+= numerisch R Zuweisung mit Addition
-= numerisch R Zuweisung mit Subtraktion
<<= ganzzahlig R Zuweisung mit Linksverschiebung
>>= ganzzahlig R Zuweisung mit Rechtsverschiebung
>>>= ganzzahlig R Zuweisung mit Rechtsverschiebung (s. o.)
&= ganzzahlig R Zuweisung mit bitweisem und (AND)
&= boolean R Zuweisung mit logischem und (AND)
^= ganzzahlig R Zuweisung mit bitweisem exklusiven oder
(XOR)
^= boolean R Zuweisung mit logischem exklusiven oder
(XOR)
!= ganzzahlig R Zuweisung mit bitweisem oder (OR)
!= boolean R Zuweisung mit logischem oder (OR)
Typkonvertierung: Der neue Typ wird in runden Klammern vor die
Variable oder vor das Objekt geschrieben. Wichtig: boolean kann
nicht umgewandelt werden; Referenztypen können umgewandelt
werden, wenn der Ausgangstyp vom Zieltyp abgeleitet wurde; numerische
Werte können umgewandelt werden.
Standard-Operatoren: Mit dem Punkt-Operator (.) können Elemente einer Klasse angesprochen
werden. Der linke Operand muß ein Referenztyp sein, der
rechte ein Bezeichner. Der Zugriff auf ein Element eines Vektors
wird mit [] formuliert. Der linke Operand muß eine Variable
sein, der Ausdruck in den Klammern muß einen numerischen
Wert ergeben.
3.2.4 Kontrollstrukturen
Java hat im Wesentlichen die gleichen Kontrollstrukturen wie C bzw. C++.
Hier nur eine kurze Syntax-Auflistung:
Blöcke:
{} (Blöcke repräsentieren eine Anweisung; hier kann beliebiger
Code untergebracht werden; Variablendeklarationen gelten nur lokal für
diesen Block im Gegenstatz zu C++.)
Mehrfachauswahl:
switch (Ausdruck) { case Konstantenausdruck : Anweisung; ... ;
default : Anweisung}
while-, do-while-Schleife:
while (Bedingung) Anweisung
do Anweisung while (Ausdruck)
Zählschleife:
for (Initialisierung; Bedingung; Reinitialisierung) Anweisung
Bedingte Anweisung:
if (Bedingung) Anweisung1 [else Anweisung2]
Sprunganweisungen:
break bricht eine Schleife oder eine Mehrfachauswahl ab. Folgt hinter
dem break ein Label, so wird zu dieser Stelle gesprungen.
continue springt an das Ende der Schleife. Mit einem zusätzlichen
Label wird an das Ende der mit dem Label benannten Schleife gesprungen.
return verläßt die aktuelle Methode. Erwartet die Methode
eine Rückgabe, so muß dieser Parameter dahinter folgen.
3.3 Klassenkonzept
3.3.1 Klassen
Eine Klasse beschreibt die Eigenschaften
gleichartiger Objekte, ein Objektmuster. Ein Objekt ist eine Instanz
einer Klasse; eine spezielle Ausprägung, wird erst während
der Abarbeitung erzeugt und besitzt genau die beschriebenen Eigenschaften.
Diese Eigenschaften gliedern sich in Verhalten und Zustand.
Daneben definiert eine Klasse auch Schritte, die zur Erzeugung
und zur Beseitigung ihrer Objekte abgearbeitet werden müssen.
Genauso wie den Objekten kann auch der Klasse ein Zustand und
ein Verhalten zugeordnet werden. Objekte werden erzeugt und wieder
gelöscht. Eine Klassendefinition besteht grob aus dem Schlüsselwort
class, einem Bezeichner und dem Klassenrumpf, der innerhalb der
geschweiften Klammern spezifiziert wird.
3.3.2 Methoden
Methoden sorgen für die Funktionalität einer Klasse und können
deren Daten modifizieren. Die wichtigsten Bestandteile einer Methodendefinition sind wie bei gewöhnlichen Funktionen: der Rückgabetyp,
der Bezeichner, die Parameterliste und der Rumpf. Der Methodenrumpf
ist ein sogenannter Block. Die hier deklarierten Variablen gelten
nur für diesen Bereich. Wichtige Modifizierer von Methoden
sind:
static: Eine static-Methode betrachtet man als zur Klasse
gehörend. Sie ist unabhängig einer Instanz dieser Klasse.
Diese Methoden können nur auf Variablen dieser Klasse zugreifen,
die ebenfalls als static deklariert wurden.
abstract: Wird eine Methode mit diesem Modifizierer versehen, darf sie
keinen normalen Rumpf haben. Statt dessen muß auf die Parameterliste
ein Semikolon folgen.
3.3.3 Konstruktoren und Destruktoren
Der Konstruktor ist die Methode, die beim Instanzieren automatisch aufgerufen wird und hat den gleichen Bezeichner wie die Klasse selbst. Diese
spezielle Methode darf keinen Wert zurückgeben und darf nur
mit public, protected und private modifiziert werden. Durch den
Konstruktor werden wichtige Voreinstellungen vorgenommen, um das
Objekt in einem gewünschten Zustand zu bringen.
Das Gegenstück ist der Destruktor, der mit void finalize (); deklariert
wird. Diese Methode wird automatisch beim Entfernen des Objektes aufgerufen
und kann z.B. benötigte Ressourcen freigeben.
3.3.4 Abgeleitete Klassen und Superklassen
In Java kennzeichnet das Schlüsselwort
extends die Vererbungsbeziehung (vergl. in C++ "::").
Dahinter folgt der Name der Superklasse (in Java werden die "Eltern"
der vererbten Klassen Superklassen genannt). Durch die Vererbung
werden alle Methoden und Variablendeklarationen auf die neue Klasse
übertragen. In Java erben alle Klassen automatisch die Klasse
Object, sie ist also die Superklasse aller Klassen.
3.3.5 Abstrakte Klassen und Methoden
Der Modifizierer abstract kann sowohl auf
Klassen-, als auch auf Methodendeklarationen angewandt werden.
Nur Klassen, die abstract deklariert wurden, dürfen auch
abstrakte Methoden enthalten. Eine abstrakte Klasse kann niemals
instanziert werden, denn sie dient immer nur als Superklasse für
andere Klassen. Davon abgeleitete Klassen können nur dann
instanziert werden, wenn alle abstrakten Methoden implementiert
worden sind. Abstrakte Methoden haben keinen Rumpf, denn sie werden
später erst von abgeleiteten Klassen implementiert.
3.3.6 `this` und `super`
Die beiden Bezeichner this und super referenzieren
Objekte im Körper einer Objektmethode, nur dort dürfen
sie verwendet werden. Der Bezeichner this hat als Typ die Klasse
des Objekts, super hat den Typ der Superklasse.
3.3.7 Statische Initialisierungsblöcke
Um statische Variablen beim Anlegen
eines Objektes automatisch zu initialisieren, wird ein statischer
Block innerhalb der Klasse angelegt. Innerhalb dieses Blocks kann
nur auf statische Methoden und Variablen zugegriffen werden. Syntax:
static {...}
3.4 Interfaces, Packages und Importanweisungen
3.4.1 Interfaces (Schnittstellen)
Ein Interface ist grob eine abstrakte
Klasse, welche nur abstrakte Methoden enthält. Da in Java
Klassen nicht direkt von mehreren Klassen gleichzeitig erben können
(im Gegensatz zu C++), gibt es die Möglichkeit mehrere Interfaces
zu erben. Beispiel einer solchen Vererbung:
class C extends A implements B;
Dies bedeutet, daß die Klasse C die Klassen A und B erbt,
wobei Klasse B ein Interface ist, das später noch implementiert
werden muß. Eine Interface-Deklaration beginnt allgemein
mit dem Schlüsselwort interface gefolgt von dem Bezeichner
der Schnittstelle. Dahinter kann mit extends die Schnittstelle
erweitert werden.
3.4.2 Packages
Sie dienen zur Kapselung und Sammlung
von Klassen. Da Java-Programmteile in Netzwerken weit verteilt
sein können, benutzt man in Java für diese Packages
Internet-Domainnames, um weltweiten Zugriff, ohne Konflikte bei
der Bezeichnung, zu ermöglichen. Für die Bezeichnung
wird der Domainname in der Reihenfolge umgedreht, sowie globale
Bezeichner groß und lokale Bezeichner klein geschrieben
(Bsp.: aus sunsoft.sun.com wird com.sun.sunsoft). Um ein Package
auf einem Rechner zu finden, müssen nur die Punkte unter
Unix durch /, auf dem PC durch \ ersetzt werden. Es sollte eine
Umgebungsvariable CLASSPATH angelegt werden, um dem Compiler mitzuteilen,
wo sich mögliche Packages befinden. Soll eine Quelldatei
zu einem bestimmten Package gehören, wird am Anfang der Datei
das Schlüsselwort package mit der Bezeichnung angegeben.
Die Klassenbibliothek umfaßt neun Pakete:
java.applet für die Implementierung von Applets und für die Soundverarbeitung,
java.awt ist für die Gestaltung graphischer Benutzeroberflächen,
java.awt.image enthält Klassen für die Bildverarbeitung,
java.awt.peer ist für die Abbildung des AWT auf das Wirtfenstersystem,
java.io beinhaltet ein umfangreiches Angebot von Ein- und Ausgaberoutinen,
java.lang `Hauptteil` der Sprache (z.B. Klassen: String, Thread, ... ),
java.net beinhaltet Netzwerkroutinen, um z.B. Interaktionen im WWW zu tätigen,
java.util enthält wichtige Datenstrukturen (Hashtable, Stack) und z.B. Zufallsroutinen,
sun.tools.debug für die Inspektion von Programmen.
3.4.3 Imports
Java benutzt Importanweisungen,
um zusätzlichen Code aus Packages einzubinden. Durch den
Schlüsselbegriff import und einem Package-Bezeichner erkennt
der Compiler, wo Definitionen verwendeter Klassentypen zu finden
sind. Als Erweiterung kann auch ein Typbezeichner getrennt durch
einen Punkt folgen, damit auch einzelne Referenztypen eingebunden
werden können. Wird als Typbezeichner ein `*` angegeben,
so durchsucht der Compiler bei Bedarf dieses Package nach Definitionen
(import on demand).
3.5 Dynamische Speicherverwaltung
Variablen und instanzierte Klassen bekommen bei direkter Initialisierung
automatisch Speicher zugeordnet (Bsp.: int a=5; oder int steps[]={1,2,3,4};).
Anderenfalls muß durch das Schlüsselwort new Speicher
für das Objekt angefordert werden (Bsp.: int[] feld; später:
feld = new int[3];). Bei einer solchen Allokierung folgt nach dem
new ein Typname und wenn der Konstruktor des Typs Übergabeparameter
erwartet, müssen diese in Klammern als Liste in der richtigen
Reihenfolge übergeben werden. Bei Vektoren gibt es allerdings
keine Parameterliste, denn hier wird an dieser Stelle eine Dimensionsangabe
verlangt. Die Anzahl der anzulegenden Felder steht in eckigen
Klammern. Hier ein Beispiel für das Erstellen eines zweidimensionalen
3x4 Feldes vom Typ int: int[][] xy=new int[3][4];
3.6 Threads
Java unterstützt parallel ablaufende Steuerflüsse innerhalb
eines Programms (Nebenläufigkeit). Für die Parallelität
sorgt der Java-Scheduler, der jedem Prozeß eine bestimmte
Zeit zuordnet. Der Scheduler läßt einen aktiven Thread
so lange laufen, bis er entweder selber die Kontrolle abgibt,
bis er bei einer Ein- oder Ausgabe blockiert, bis er auf die Freigabe
einer Ressource wartet oder bis ein Thread mit höherer Priorität
abgearbeitet werden soll.
Die erste mögliche Realisierung
der Nebenläufigkeit ist die Benutzung der Klasse thread.
Dabei geht man wie folgt vor. Zuerst wird eine Klasse definiert,
die von thread abgeleitet ist. Diese Klasse besitzt jetzt eine
Methode void run(), die nur noch mit eigenem Code überschrieben
werden muß. Im nächsten Schritt wird die Klasse instanziert.
Es muß noch die Methode void start() aufgerufen werden,
und der Thread ist gestartet. Er läuft solange, bis die run-Methode
abbricht, oder die Methode void stop() aufgerufen wird. Die Klasse
thread besitzt natürlich noch weitere Methoden, die aber
hier nicht weiter aufgeführt werden.
Vor allem für Animationen
von Applets wird eine anderes Verfahren für Nebenläufigkeit
benutzt. Die Klasse Applet unterstützt selbst ein Interface
runnable, wobei die Methoden start, run und stop implementiert
werden müssen.
Für Kenner sei noch erwähnt, daß Java-Threads meist auf die Threads des
Betriebssystems projiziert werden. Aus diesem Grund sind Java-Anwendungen MP-Hot,
d. h. sie werden auf Multi-Prozessor-Computern schneller ausgeführt.
3.7 Synchronisation
Nebenläufige Prozesse
sind nicht immer leicht zu verwalten, vor allem, wenn mehrere
Threads die gleichen Daten und Ressourcen benutzen; dann müssen
sie ihren Ablauf aufeinander abstimmen bzw. synchronisieren. Hierfür
gibt es in Java das Schlüsselwort synchronized, daß
vor eine bestimmte Methode gesetzt werden kann, um an dieser Stelle
dafür zu sorgen, daß diese Methode nicht gleichzeitig
abgearbeitet wird. So kann die Änderung von Daten nicht von
anderen Threads gestört werden.
3.8 `Daemonen`
Daemonen sind Threads, die für andere Threads Dienste leisten. Ein
Daemon ist natürlich überflüssig, wenn kein anderer Thread
mehr vorhanden ist. Deshalb sollte er automatisch aus dem Speicher
entfernt werden. Dies passiert aber nur dann automatisch, wenn
ein Thread als Daemon bezeichnet wurde (durch die Methode setDaemon(true)).
3.9 Ausnahmebehandlung
Oft können während der Ausführung
Fehler entstehen, die in irgendeiner Art abgefangen werden müssen.
Es sind dabei nicht die Programmierfehler gemeint. Typische Fehler
sind Ein-, Ausgabefehler oder Übertragungsfehler in Netzwerken,
die jederzeit auftreten können und mit speziellen Routinen
bearbeitet werden müssen. In Java gibt es für diesen
Zweck Exceptions (deutsch: Ausnahmen), die folgende Vorteile haben:
- Normaler Programmcode und Fehlerbehandlung lassen sich in separate Teile gliedern,
- Beim Auftreten eines Fehlers kann eine Methode genauere Informationen über den Fehler übermitteln,
- Aufräumarbeiten lassen sich einfacher organisieren.
An einem kleinen Beispiel läßt sich die Ausnahmebehandlung am
besten erklären:
static int teile(int x, int y) throws ArithmeticException {
if(y==0)
throw(new ArithmeticException("Division durch 0"));
else return x/y;
}
public static void main(String[] arg){
int x=1, y=0;
try {
x=teile(x, y);
}
catch(ArithmeticExceptione){
System.out.println("Ausnahme bei der Division"
+e.getMessage());
}
}
Bild 3.4: Ausnahmebehandlung
Die Methode main soll eine Division ausführen und den Fehler
"Division durch Null" durch eine Fehlermeldung abfangen. Zuerst
wird x mit 1 und y mit 0 initialisiert. Dann folgt ein sogenannter
try-Block, der angibt, wo ein Fehler auftreten kann. Hier steht die Methode
"teile(x,y)". Die Methode wird aufgerufen und erkennt, das y=0
ist und ruft die throw-Methode auf, die jeder Klasse zur Verfügung
steht. Sie sendet eine mit new erzeugte Fehlermeldung an eine untere
Ebene, wo sie von einem catch-Block abgefangen werden kann. Alle
bekannten Fehlerbezeichnungen befinden sich in dem Package java.lang.
Am Ende erscheint mit println und mit Hilfe von getMessage() die
genaue Meldung auf dem Bildschirm.
Dieses kleine Beispiel erklärt zwar nicht alle Probleme der Ausnahmebehandlung, es zeigt aber schon das Prinzip.