Von Enno Runne und Hendrik C.R. Lock
In diesem Kursteil wagen wir den Sprung in die
Java-Programmierung. Die Sprachkonzepte werden anhand eines
durchgängigen Programmierbeispiels eingeführt - einer
interaktiven Adreßkartei, die das Abspeichern, Laden, Suchen und
Ausgeben von Adressen erlaubt.
Java ist eine objektorientierte Programmiersprache. Die Idee der Objektorientierung ist es, Probleme so zu darzustellen (zu modellieren), daß Daten und zugehörige Prozeduren geschickt gekapselt werden. Kapselung heißt dabei, daß Daten und ihre Prozeduren eine entwurfs- und programmtechnische Einheit bilden. Diese Einheit bezeichnet man als Klasse; ihre Variablen werden Elemente und ihre Prozeduren Methoden genannt. Eine Klasse ist ein Typ in der Programmiersprache, beschreibt also Eigenschaften von Objekten. Ein Objekt ist in dieser Sprechweise eine Ausprägung einer bestimmten Klasse - die Klasse "Auto" könnte als Ausprägungen etwa die Objekte "mein Golf" und "Nachbars Benz" haben. Alle Objekte einer Klasse besitzen dann dieselben Methoden und Elemente, aber ihre Elemente dürfen unterschiedliche Werte enthalten - beide "Auto"-Objekte haben z.B. die Methode "Anlassen" und das Element "Verbrauch", aber die Verbrauchswerte unterscheiden sich natürlich. Werte sind wiederum Objekte, die ganze Struktur läßt sich also schachteln.
Das klingt nach viel Theorie, wird in der Praxis aber schnell klar:
Bezogen auf unser Beispiel können wir die gesamte Adresskartei
als eine Klasse AdressKartei
einführen, die
über ihre Methoden eine Reihe von Objekten einer Klasse
Adresse
verwaltet. Die Adresskartei ist dann ein
einzelnes Objekt der Klasse AdressKartei
. Ihre Methoden
realisieren das Einfügen und Wiederauffinden von Adressen.
In Java sind Werte entweder Objekte oder Daten der sogenannten
Grundtypen, z.B. ganze Zahlen. Alle Klassen bauen auf den
Grunddatentypen und auf einer Reihe von Standardklassen auf,
z.B. System
, String
und
File
. Die wichtigsten Standardklassen betreffen die
Programmierung von Oberflächen, Datenhaltung und
Textverarbeitung.
Wie bereits erwähnt, bietet Java mehrere elementare Datentypen,
die im folgenden tabellarisch aufgeführt werden.
Schlüsselwort | Größe | Bedeutung |
(Integertypen)byte short int long (Fließkommatypen) float double (andere) char boolean |
8 Bit 16 Bit 32 Bit 64 Bit 32 Bit 64 Bit 16 Bit keine Angabe |
ganze Zahl (-128 bis 127) ganze Zahl (-32768 bis 32767) ganze Zahl (ca. +/- 2 Mrd.) ganze Zahl Fließkommazahlen Fließkommazahlen doppelter Genauigkeit ISO-Zeichen Wahrheitswert |
Wie die Tabelle zeigt, ist die Maschinendarstellung aller
Grunddatentypen festgelegt und damit unabhängig von der
jeweiligen Zielmaschine. Dadurch werden Portierungsprobleme, wie sie
z.B. von C her bekannt sind ("wie groß ist mein int
heute?"), ausgeschlossen und die Netzfähigkeit ganz wesentlich
unterstützt.
Im Vergleich zu C werden im Datentyp char
über den
Standardzeichensatz ASCII hinaus eine Reihe weiterer Zeichensätze
und Umlaute im ISO Standard angeboten. Weil Java-Quelltexte ebenfalls
in diesem Zeichensatz dargestellt werden, dürfen auch deutsche
Umlaute in den Bezeichnern verwendet werden.
Die Syntax von Java folgt im wesentlichen der Syntax von C, z.B. bei
Zuweisungen, logischen und arithmetischen Operatoren sowie bei
Definition von Funktionen und Prozeduren. Außerdem gibt es
einen neuen booleschen Typ bool
, dessen Werte mit
true
und false
bezeichnet werden. Weiterhin
kennt Java keine strukturierten Daten wie C (union
&
Co), diese werden durch die Klassen abgelöst.
Daß sich Java syntaktisch sehr stark an C angelehnt hat, macht einen Umstieg von C her einfacher - wer sich in C auskennt, braucht sich nur die neuen Konzepte anzueignen. Aus diesem Grund gehen wir in diesem Java-Kurs auch nicht näher auf die grundlegenden Programmierkonzepte ein; wir setzen also voraus, daß der Begriff einer Fallunterscheidung oder einer Schleife bekannt ist. Durch die Ähnlichkeit mit C ist es mit gewissen Einschränkungen sogar möglich, C-Programme relativ einfach nach Java umzusetzen. Die größten Einschränkungen treten hierbei im Bereich des Datenaustausches zwischen Programm und Umgebung auf. Aber auch, wenn Sie noch keine Programmiersprache perfekt beherrschen, können Sie einfach die Beispiele dieses Kurses ausprobieren und durch Experimentieren und Verändern des Codes eigene Programme erstellen.
Object
, auf die wir im nächsten Abschnitt zurückkommen werden. Die Standard-Klassen sind in Form von
Paketen (engl. packages) organisiert:
Paket | Funktionalität |
java.applet |
Programmierung von Applets |
java.awt |
Plattformunabhängige, graphische Oberflächenprogrammierung |
java.lang |
Basisklassen der Sprache Java, z.B. Object und String |
java.io |
Ein- und Ausgabe |
java.net |
Netzprogrammierung (Sockets und URLs) |
java.util |
Wiederverwendbare Elemente, z.B. dynamische Felder, Hashtabellen und Zerteiler |
Object
Object
. Jede Klasse ist eine direkte oder über
mehrer Stufen indirekte Unterklasse von Object
und erbt
deren Eigenschaften. Vererbung heißt hier, daß eine
Unterklasse dieselben Elemente und Methoden wie ihre Oberklasse
besitzt, aber daß die Unterklasse durch neue Elemente und
Methoden erweitert werden darf. Java bietet nur Einfachvererbung,
d.h. jede Klasse (mit Ausnahme von Object
) hat genau eine
Oberklasse. Mehrfachvererbung gibt es im Gegensatz zu anderen
objektorientierten Programmiersprachen nicht - die Definition von
"Omnibus" aus den Klassen "Auto" und "Öffentliches
Verkehrsmittel" zusammen wäre also nicht möglich. Ob dies in
der Praxis zu nennenswerten Einschränkungen führt, wird unter
Fachleuten durchaus noch kontrovers diskutiert. Durch den Verzicht
auf Mehrfachvererbung ist Java im Vergleich zu C++ einfacher und
konzeptuell klarer. Mehrfachvererbung ist nämlich recht
kompliziert, wenn die Vererbung gleicher Methoden aus zwei oder mehr
Oberklassen zu regeln ist. Eine wichtige Funktion der
Mehrfachvererbung wird in Java allerdings von sogenannten
Schnittstellen (engl. interfaces) wahrgenommen, die in einem
späteren Kursteil eingeführt werden.
Die Ur-Klasse Object
ist Teil des Pakets
java.lang
und vererbt 11 Methoden, darunter
equals()
und toString()
, die uns im Laufe
des Kurses noch mehrmals begegnen werden. Die vererbte Methode
equals()
testet zwei Objekte auf Gleichheit, indem sie
ihre Verweise auf Gleichheit testet. Die Methode
toString()
stellt ein Objekt als eine Zeichenkette dar,
damit es zum Beispiel für Testzwecke ausgegeben werden kann. Die
von Object
vererbte Methode gibt in der Zeichenkette den
Typ und die Speicheradresse des Objektes an.
In jeder Klasse kann eine geerbte Methode individuell neu definiert
werden; dieser Vorgang wird als Überschreiben bezeichnet.
Außerdem kann eine geerbte Methode überladen
werden. Der Unterschied ist der folgende: Beim Überschreiben
wird in der Unterklasse eine Methode mit denselben Paremetern (und
Typen) wie in der Obeklasse definiert. Beim Überladen hingegen
wird eine Methode definiert, die in der Anzahl der Parameter oder in
ihren Typen von der ursprünglichen Methode abweicht.
Im folgenden Beispiel wird die Methode equals()
überladen. Für Objekte einer Klasse
Koordinate
, die (x,y)-Paare beinhaltet, wird damit
anstelle der Gleichheit der Verweise, wie sie Java normalerweise
verwendet, die Gleichheit aller Elemente getestet, die in diesem Falle
erwünscht ist. Ansonsten würden nur Koordinaten als gleich
erkannt, die sich auf dasselbe Objekt beziehen, aber nicht solche,
deren eigentliche (x,y)-Werte übereinstimmen.
class Koordinate { int x,y; boolean equals(Koordinate k) { return (x == k.x && y == k.y); } }Um die Methode
equals()
zu überschreiben, statt sie
zu überladen, müßte der Parametertyp
Object
sein. Eine mögliche Realisierung und eine
eingehendere Diskussion dieser Thematik wird später erfolgen.
String
Java deckt mit seinen Standardklassen vieles von dem ab, was in
einfachen Programmen benötigt wird. Die Standardklassen sind
in Form von Paketen organisiert und über das Internet
verfügbar.
Zeichenketten sind in Java nicht als Grunddatentyp, sondern als Klasse
vorhanden. Es wird dabei zwischen zwei Arten von Zeichenketten,
nämlich zwischen konstanten und veränderbaren,
unterschieden:
Konstante Zeichenketten sind von der Klasse String
. Sie
bietet diverse Methoden, wie z. B. die Bestimmung der Länge oder
den Vergleich zweier Zeichenketten. Allerdings können Objekte der
Klasse String
im Programmlauf nur erzeugt, nicht aber
verändert werden. Veränderbare Zeichenketten werden als
Objekte der Klasse StringBuffer
dargestellt; sie bietet
Methoden zum Einfügen und Löschen von Zeichen und ist somit
für Textanwendungen geeignet. Im Gegensatz zu
String
können Objekte dieser Klasse jedoch nicht
direkt in Ein- und Ausgabeoperationen benutzt werden, sie müssen
deshalb zuvor nach String
konvertiert
werden. String
und StringBuffer
bieten also
nur in Kombination eine umfassende Zeichenkettenfunktionalität,
aber in vielen einfacheren Fällen reicht String
alleine schon aus, wie folgende Beispiele verdeutlichen:
String str; str = new String("Sabine");Diese Anweisung erzeugt ein Objekt des Typs
String
, das
die Zeichenkette speichert, und legt einen Verweis auf das Objekt in
der Variablen str
ab. Die konkrete Speicherdarstellung
der Zeichenkette, d.h. wie der Rechner die Bits und Bytes ablegt,
bleibt dabei dem Benutzer verborgen. Eine kürzere Schreibweise,
die den selben Effekt hat, ist:
String str = "Sabine";
Die Ausgabe einer Zeichenkette auf den Bildschirm kann mit der Methode
println
der Klasse System
erfolgen:
System.out.println(str);Die Methode ist bezüglich der Grunddatentypen und der Klasse
String
überladen: im Falle eines Grunddatentyps wird
der Wert zuvor in eine Zeichenkette umgewandelt und dann erst
ausgegeben, wohingegen er im Fall eines Arguments vom Typ
String
direkt ausgegeben werden kann.
Eine wichtige Grundoperation auf Zeichenketten ist die Verkettung, die
durch den Operator +
ausgedrückt wird - dieser
Operator ist somit überladen, da er natürlich auch die
gewohnte Addition von Zahlen ausdrückt. Für seine Anwendung
gilt: ist eines der Argumente von +
vom Typ
Object
oder einer beliebigen Unterklasse, so wird das
Objekt mittels seiner Methode toString
in eine
Zeichenkette umgewandelt, und der Operator bezieht sich dann auf eine
Verkettung. Sind beide Argumente jedoch numerische Typen, so bezieht
er sich auf eine Addition. Auch die Addition ist überladen, da
hier unterschiedliche numerische Typen kombiniert werden dürfen.
Hierzu ein Beispiel:
System.out.println("" + 1 + 2 ); System.out.println( 1 + 2 ); System.out.println("" + 1 + (1 + 2) );ergibt jeweils die Ausgaben
12
, 3
, und
13
. Im letzten Fall bewirkt die innere Klammerung die
Interpretation von +
als Addition, in
Übereinstimmung mit der Regel zur Auflösung der
Überladung.
new
und kann danach nur noch an Variable (und
Elemente) zugewiesen werden. Technisch gesehen ist ein Verweis eine
Speicheradresse, aber Adressen werden in Java systematisch vor dem
Benutzer verborgen: Die Bildung von Adressen, Adressarithmetik wie in
C, und Zugriffe auf den Speicher über Adressen existieren
nicht. Die Verbannung von Adressen ist Teil des Sicherkeitskonzepts
von Java - ansonsten wären beliebige Zugriffe im Speicher
möglich. Ein Objekt ist nicht mehr erreichbar, wenn keine Variable eines erreichbaren Objektes oder einer aufgerufenen Methode diesen Verweis speichert. Die virtuelle Java Maschine besitzt eine Speicherverwaltung, die nichterreichbare Objekte aufsammelt und den von ihnen bisher belegten Speicherplatz für die Erzeugung neuer Objekte verfügbar macht. Der Programmierer muß sich also um die sogenannte Garbage Collection nicht mehr selbst kümmern.
Adresse
, die wir für die Adressverwaltung
benötigen. Eine Adresse soll zunächst nur drei Felder
für den Vornamen, den Namen und das Alter enthalten:class Adresse { String vorname, name; int alter; Adresse(String vn, String nn, int a) { vorname = vn; name = nn; alter = a; } }Der Name der Klasse folgt dem Schlüsselwort
class
,
die Definition der Elemente und Methoden (in dieser Reihenfolge) wird
durch geschweifte Klammern umschlossen. Die Klasse
Adresse
enthält damit drei Elemente und eine Methode
Adresse
, die der Erzeugung dient.
Methoden einer Klassendefinition, die denselben Namen wie die Klasse
tragen, heißen Konstruktoren und werden für die Erzeugung
neuer Objekte benutzt, hier z. B. Adresse
. Durch die
explizite Programmierung des Konstruktors einer Klasse bestimmt der
Benutzer die Initialisierung der Elemente. Die Initialwerte werden
dabei durch die Parameter der Konstruktormethode übergeben.
Ein neues Objekt wird durch Verwendung des Operators new
angelegt, dabei wird implizit immer der Konstruktor der angesprochenen
Klasse aufgerufen.
Adresse adr = new Adresse(new String("Sabine"), new String("Müller"), 13);Durch
Adresse adr
wird eine neue Variable mit Bezeichner
adr
vereinbart. Das Argument von Operator
new
ist ein Konstruktor.
Enthält die Klasse keine explizite Konstruktordefinition durch
den Programmierer, so verfügt sie automatisch über einen
impliziten parameterlosen Konstruktor, der das Objekt anlegt und
dessen Elemente initialisiert. Damit stellt sich die Frage, wie
Elemente einer Klasse überhaupt initialisiert werden, deren
Initialisierung nicht explizit programmiert wurde. Dies betrifft im
einzelnen auch Elemente, die nicht explizit durch einen vorhandenen
Konstruktor initialisiert werden. In Java gilt hierfür die
Regel, daß grundsätzlich alle Variablen (und Elemente von
Objekten) initialisiert werden. Die initialen Werte sind
typabhängig: Verweise werden mit null
, numerische
Variablen mit 0
, und boolesche Variablen mit
false
initialisiert. Der Sonderwert null
signalisiert, daß die Variable keinen gültigen Verweis
enthält. Der Zugriff über eine solche Variable löst
eine Ausnahme aus, die über eine Ausnahmebehandlung abgefangen
werden kann (Genaueres hierzu wird in einem späteren Teil
behandelt).
Es ist durchaus möglich, ein Objekt auch wie im folgenden zu
initialisieren. Dabei sieht man auch, daß man auf die Elemente
eines Objekts direkt mit Objekt.Element zugreifen kann:
adr = new Adresse(); adr.name = "Sabine"; adr.vorname = "Müller"; adr.alter = 13;Die Initialisierung durch den Konstruktor ist jedoch die kompaktere und systematischere Variante, da sie wirklich nur die notwendige Initialisierung durchführt - im obigen Beispiel werden die Elemente erst implizit initialisiert und dann auf die gegebenen Werte gesetzt, anstatt sie gleich mit dem gewünschten Wert zu belegen.
Java bietet die Möglichkeit, Namen von Methoden zu überladen, also auch den Konstruktor. Damit die Überladung eindeutig bestimmt werden kann, wird gefordert, daß sich Anzahl und Typ der Argumente von solchen Definitionen unterscheiden. Im folgenden Beispiel unterscheiden sich die zwei Konstruktoren in ihrer Argumentzahl:
class Adresse { String vorname, name; int alter; Adresse(String vorname, String nachname, int alter) { this.vorname = vorname; name = nachname; this.alter = alter; } Adresse(String vorname, String nachname) { this.vorname = vorname; name = nachname; } }
Hier wurde zusätzlich mit this
ein neues Konzept
eingeführt, nämlich der Verweis auf das aktuelle Objekt,
also der Verweis auf dasjenige Objekt, dessen Methode gerade
ausgeführt wird. Im Beispiel benötigten wir
this
, weil die Namen der Funktionsparameter - vorname
und alter
- die gleichnamigen Klassenelemente verdecken.
Um eine Adresse formatiert ausgeben zu können, erweitern wir die
Klasse nun um eine Ausgabemethode. Deren Definition liegt wie oben
innerhalb der geschweiften Klammern der Klassendefinition von
Adresse
.
void Ausgeben() { System.out.println(vorname + " " + name); System.out.println("Alter: " +alter ); }Die Bezeichner
name
, vorname
und
alter
beziehen sich hier natürlich auf die
gleichnamigen Elemente der Klasse.
Weiterhin definieren wir die Standardmethode toString
,
mit deren Hilfe das Objekt z.B. zu Testzwecken ausgegeben werden kann.
public String toString() { return "Adresse (" + name + " " + vorname + " " + alter + ")" ; }Wie in C unterscheidet Java zwischen Prozeduren und Funktionen: Prozeduren sind Funktionen vom Ergebnistyp
void
, d.h. sie
geben kein Resultat zurück. Im Gegensatz zu C ist das
Schlüsselwort void
nicht optional.
main
main
bezeichnet. Die Hauptklasse ist dementsprechend die Klasse, die
main
definiert. Im allgemeinen wird kein Objekt dieser
Klasse explizit gebildet, sie dient nur als statische Kapselung des
Hauptrogramms und seiner Daten. Nur Java-Programme, die eine solche
Hauptklasse enthalten, können auch als Java-Applikation
aufgerufen werden - ohne Hauptklasse wäre ja nicht klar, welcher
Programmcode überhaupt aufzurufen ist. Wird eine virtuelle Java
Maschine mit einer solchen Hauptklasse gestartet, erzeugt sie ein
Objekt dieser Hauptklasse und startet deren main
-Methode.
Bei der Definition der Methode main
müssen einige
Randbedingungen beachtet werden: diese Methode muß immer als
statisch und öffentlich deklariert werden. Das
bedeutet das Folgende:
Eine Methode wird durch Verwendung des Schlüsselworts
static
als statisch vereinbart. Damit gehört sie
fest zu dieser Klasse und nicht zu einzelnen Objekten. Eine statische
Methode kann deshalb auch ohne Objekt aufgerufen werden, analog zu
einer Prozedur in C. Außerdem kann eine statische Methode
niemals überschrieben werden.
Wichtig ist weiterhin, daß main
öffentlich
ist, ihm also das Schlüsselworts public
vorangestellt wird. Eine öffentliche Methode ist für alle
anderen Klassen und deren Methoden sichtbar. Dies ist notwendig, weil
normalerweise Methoden einer Klasse nur für diese, alle ihre
Unterklassen, und für Klassen im selben Paket sichtbar sind. Alle
anderen Klassen wissen nichts von ihrer Existenz.
Die eben genannten Schlüsselwörter sind sogenannte Modifikatoren und als solche auch auf Klassen und Variablen anwendbar. Modifikatoren und die Thematik der Sichtbarkeitsbereiche werden in einem späteren Teil noch ausführlicher behandelt.
Unser Hauptprogramm sieht nun folgendermaßen aus:
public class AdressProgramm { public static void main(String argv[]) { Adresse adr = new Adresse("Sabine", "Müller", 13); adr.Ausgeben(); System.out.println(adr); } }Es erzeugt ein Objekt Adresse und gibt es formatiert aus. Die Anweisung
System.out.println(adr)
übergibt an
println
ein Objekt. Die Methode println
besorgt sich deshalb implizit mittels adr.toString()
die
Zeichenkettendarstellung des Objekts.
Die Datei mit dem Hauptprogramm enthält zusätzlich noch die
Definition der Klasse Adresse
, die für das
Hauptprogramm sichtbar ist. Es ist zu beachten, daß der Name
dieser Datei AdressProgramm.java
lauten sollte,
d.h. daß der Name der Hauptklasse mit dem der Datei
übereinstimmt (erweitert um die Endung .java
).
Der Grund für diese Namenskonvention ist, daß alle als
öffentlich vereinbarten Klassen in einer Quelldatei gleichen
Namens abgelegt werden müssen. Das bedeutet auch, daß jede
Quelldatei höchstens eine öffentliche Klasse enthält.
Diese Konvention erfordert der Übersetzer javac
.
Der Übersetzer espresso
hingegen verhält sich
hier mit Absicht liberaler, weil die Konvention eine Flut von
Quelldateien zur Folge haben kann.
Wir verwenden die Übersetzer javac
oder
espresso
. Der Aufruf der Übersetzer sieht wie folgt
aus:
javac AdressProgramm.javabzw.
espresso AdressProgramm.java
Der Übersetzer legt im aktuellen Dateiverzeichnis für jede
Klasse getrennt eine Klassendatei an, in unserem Fall die Dateien
AdressProgramm.class
und Adresse.class
. Aus
diesem Grund spielt es zunächst auch keine Rolle, ob
Hauptprogramm und Klassendefinitionen in einem gemeinsamen oder in
getrennten Quelldateien abgelegt werden. Die erzeugten Klassendateien
enthalten Typinformationen und den Bytecode für die virtuelle
Java Maschine.
Der Start unserer kleinen Anwendung erfolgt mit dem Kommando
java AdressProgrammmit dem die virtuelle Maschine gestartet wird, auf der die Applikation dann abgearbeitet wird. Es erzeugt erwartungsgemäß folgende Ausgabe:
Sabine Müller Alter: 13 Adresse (Müller Sabine 13)
Das war also unser erstes Java-Programm! In der nächsten Ausgabe von Internet online geht es unter anderem um Felder und Vektoren (d.h. dynamisch vergrößerbare eindimensionale Felder). Bis dann wünschen wir frohes Experimentieren!