Von Enno Runne und Hendrik C.R. Lock
Als durchgängiges Programmierbeispiel verwendeten wir bereits in Teil 2
eine kleine Adresskartei. Dazu wurde eine
Datenklasse Adresse
entworfen, durch die wir Namen und Alter von Personen abspeichern können.
Diesmal beschäftigen wir uns damit,
wie wir mehrere Adressen als Datensätze ablegen,
um zu einer Adresskartei zu kommen.
Dabei werden wir Javas Sprachkonstruktion
der Felder und die Standardklasse Vector
kennenlernen.
Außerdem führen wir das Konzept der Vererbung ein, mit dessen Hilfe
z.B. Adresssen um Telefonnummern erweitert werden können.
Unser bisheriges Programm ist in der Lage, eine einzige Adresse zu speichern
und wieder auszugeben. Eine solche Adresse wird in Java als ein Objekt dargestellt.
Das Objekt enthält Datenelemente, die Namen und Alter speichern,
und Methoden, durch die ein Objekt angelegt und ausgegeben werden kann.
Im folgenden geben wir die Definition der Klasse Adresse
und ein Hauptprogramm an, das ein Adressobjekt anlegt und ausgibt.
public class AdressProgramm { public static void main(String argv[]) { Adresse adr = new Adresse("Sabine", "Müller", 13); adr.ausgeben(); System.out.println(adr); } } 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; } void ausgeben() { System.out.println(vorname + " " + name); System.out.println("Alter: " +alter ); } public String toString() { return "Adresse (" + name + " " + vorname + " " + alter + ")" ; } }
Wir könnten jetzt im Hauptprogramm für jede neue Adresse eine weitere Variable bereitstellen. Dies wäre natürlich unflexibel, weil wir im Voraus die Anzahl der zu speichernden Adressen nicht wissen. Wir führen Felder ein, mit deren Hilfe sich viele gleichartige Objekte gemeinsam speichern lassen.
Unsere Adressobjekte speichern bisher nur Namen und Alter von Personen. Damit haben wir uns ein Grundgerüst geschaffen, aber in der praktischen Anwendung wollen wir vielleicht auch Telefonnummern oder den Wohnort speichern. Wir werden zeigen, wie sich das vorhandene Grundgerüst mit Hilfe der sogenannten Vererbung erweitern läßt.
Wir erhalten damit die Möglichkeit, eine beliebige Anzahl von Adressen zu speichern. Ihre Anzahl ist allerdings durch die Feldgröße beschränkt. Im folgenden Hauptprogramm werden zwei Adressobjekte erzeugt und in einem Feld abgelegt.
public class AdressProgramm { public static void main(String argv[]) { Adresse adr[]; adr = new Adresse[20]; adr[0] = new Adresse("Sabine", "Müller", 13); adr[1] = new Adresse("Gustav", "Becker", 24); adr[1].ausgeben(); adr[0].ausgeben(); } }Zuerst wird der Bezeichner
adr
als Feld über Objekten des Typs
Adresse
vereinbart.
Danach wird das Feld erzeugt und dabei seine Größe festgelegt,
hier sind das 20 Elemente. Diese
zwei Anweisungen können auch zusammengefaßt werden:
Adresse adr[] = new Adresse[20];Mit der Anweisung
adr[0] = new Adresse("Sabine", "Müller", 13);erzeugen wir ein Adressobjekt und weisen es dem ersten Feldelement zu, das durch
adr[0]
indiziert wird.
Es ist zu beachten, dass (wie in C) der Zugriff auf das erste
Feldelement immer mit Index 0
erfolgt.
Weil die Größe unseres Feldes 20
ist,
ist der Index des letzten verfügbaren Elements 19
.
Ein Index ist immer eine positive ganze Zahl (einschließlich der Null).
Greift man auf ein Feld durch einen Index außerhalb des definierten Bereichs zu,
in unserem Fall ist das der Bereich 0..19
, so wird ein
Laufzeitfehler ausgelöst.
Eine Zuweisung legt in Java immer eine Referenz auf ein Objekt ab. Das heißt, dass das Adressobjekt nicht in das Feldelement kopiert wird, sondern seine Referenz abgelegt wird. Weisen wir also ein Objekt an verschiedene Feldelemente zu, so wirkt sich eine Änderung des Objekts auf alle diese Feldelemente aus. Das folgende Beipiel zeigt dies:
adr[0] = new Adresse("Sabine", "Müller", 13); adr[1] = adr[0]; adr[1].name = "Bär";
Nach der zweiten Zuweisung steht in Element 0
und Element
1
eine Referenz auf das gleiche Objekt des Typs Adresse
.
Die Änderung der Namenskomponente des Objekts
über adr[1]
wird damit auch über adr[0]
sichtbar.
Dies wäre nicht der Fall, wenn das Objekt kopiert worden wäre.
Felder lassen sich sehr elegant durch Schleifenkonstruktionen bearbeiten.
Im Folgenden führen wir eine for
-Schleife ein und benutzen
sie zur Ausgabe aller im Feld gespeicherten Adressen.
for (int i = 0; i < adr.length; i++) { if (adr[i] != null) adr[i].ausgeben(); }Eine
for
-Schleife wird durch eine Initialisierung,
eine Schleifenbedingung und eine Zählanweisung definiert:
i
vom Grundtyp int
und setzten sie auf ihren Startwert 0
;
i
um eins.
for
-Schleife sollten aufeinander abgestimmt sein.
Fehlte in unserem Beispiel die Zählanweisung, so würde
die Schleife nicht anhalten.
Wir können das Feld auch von oben nach unten durchlaufen, das sieht dann so aus:
for (int i = adr.length-1; 0 <= i; i-- ) { if (adr[i] != null) adr[i].ausgeben(); }
Die for
-Schleife wird damit fast genauso wie in C benutzt.
Im Unterschied zu C ist die Schleifenbedingung wirklich ein
logischer Ausdruck, der als Ergebnis also nur die Werte true
oder false
zurückliefert.
Im Gegensatz zu C können außerdem
Variablendeklarationen im Initialisierungsteil auftreten.
Durch diese Deklaration können Zählvariable eingeführt werden,
die auschließlich in Schleifenkopf und Rumpf sichtbar sind.
Das heißt, dass diese Variable nach der Schleife nicht mehr benutzt werden kann.
Diese Form der Deklaration hat den Vorteil,
dass Bezeichner dort vereinbart werden können,
wo sie benutzt werden.
In unserem Schleifenrumpf, begrenzt durch die geschwungenen
Klammern {
und }
,
müssen wir undefinierte Referenzen ausschließen, weil
sonst der Methodenaufruf undefiniert ist.
Eine undefinierte Referenz wird dabei durch den speziellen Wert null
dargestellt.
Wir hatten ja in Teil 2 erwähnt, dass alle
Variablen automatisch
mit null
initialisiert werden. Dies gilt auch für Felder.
Ein Zugriff über einen undefinierten Wert, z.B.
adr[18].ausgeben();würde in unserem Beispiel zu einem Fehler während der Programmausführung führen. Solche Laufzeitfehler werden in Java durch Ausnahmen (engl. exception) realisiert. Die dem obigen Beispiel zugeordnete Ausnahmebehandlung heißt
NullPointerException
.
Sie wird immer dann ausgelöst,
wenn ein Bezeichner den Wert null
besitzt
und versucht wird, über ihn eine Methode aufzurufen.
Den Umgang mit Ausnahmen werden wir im nächsten Kursteil besprechen,
da sie bei der Ein- und Ausgabe von großer Bedeutung sind.
Wir entwerfen deshalb eine Klasse, die ein Feld veränderbarer Größe realisiert. Die wichtigste Methode der Klasse dient zum Einfügen von Adressen. Wird durch Einfügen die verfügbare Feldgröße überschritten, so wird durch eine weitere Methode das Feld verdoppelt. Diese Methode wird versteckt, d.h. sie ist von außerhalb der Klasse nicht sichtbar und benutzbar. Damit wird auch die Verwaltung des Feldes versteckt und ist von außen nicht beinflußbar. Dies erhöht die Programmsicherheit und macht es möglich, später die Verwaltung des Feldes zu ändern, ohne dass die Einfügeoperation beeinflußt wird. So ist es z.B. denkbar, nachträglich eine Operation zu realisieren, die die Feldgröße verkleinert, um Speicher einzusparen.
class AdressKartei_1 { Adresse adr[] = new Adresse[4]; int frei = 0; // erstes freies Element private protected Adresse[] verdoppeln(Adresse alt[]) { Adresse neu[] = new Adresse[alt.length * 2]; System.arraycopy(alt, 0, neu, 0, alt.length); return neu; } /* Neue Adresse einfügen */ void einfügen(Adresse a) { if ( frei <= adr.length-1 ) adr[frei++] = a; // einfügen und frei erhöhen else { adr = verdoppeln(adr); adr[frei++] = a; } } }Die Klasse
AdressKartei_1
besitzt keinen Konstruktor.
Wird nun ein Objekt dieser Klasse durch new AdressKartei_1()
erzeugt,
so wird von Java automatisch der Konstruktor der Oberklasse aufgerufen;
In diesem Fall heißt die Oberklasse Object
.
Das erzeugte Objekt enthält ein Feld der Größe 4
und den Index frei = 0
des ersten freien Feldelements.
Es gibt auch die Möglichkeit, den Konstruktor zu definieren und dabei
die erzeugende Methode aus der Oberklasse zu verwenden.
Die Methode wird über
super()
angesprochen.
Dieses Vorgehen hat den Vorteil, dass man auf bereits existierende
Methoden zurückgreifen kann und dann nur neu definiert bzw. initialisiert,
was in der neuen Klasse wichtig ist.
In unserem Beispiel könnte das so aussehen:
AdressKartei_1() { super(); frei = 0; }
Die Methode einfügen
ruft verdoppeln
immer dann auf,
wenn beim Einfügen der Füllstandzeiger
frei
hinter dem größten gültigen Index steht,
d.h. wenn gilt: frei==adr.length
.
Die Methode verdoppeln
erzeugt
zuerst ein neues Feld neu
mit der doppelten Größe des
alten Feldes, das in Parameter alt
übergeben wird.
Anschließend werden alle
Elemente des alten in das neue Feld kopiert. Dazu
benutzen wir eine speziell hierfür vorgesehene Methode
der Klasse System
:
System.arraycopy(alt, 0, neu, 0, alt.length);Die Bezeichner
alt
und neu
sind die Referenzen
auf die Quelle und das Ziel der Kopieroperation, bei der
alt.length
die Anzahl der zu kopierenden Elemente angibt.
Weiterhin definieren das zweite und vierte Argument der Funktion die Indizees
der Quelle und des Ziels, ab denen kopiert werden soll.
Die Zeile
private protected Adresse[] verdoppeln(Adresse alt[])vereinbart durch
private protected
, dass die
Funktion ausschließlich in der Klasse AdressKartei
und in
ihren Unterklassen sichtbar ist, nicht jedoch in anderen Klassen.
Dadurch verhindern wir, dass das Feld ohne Aufruf der Einfügeoperation
erweitert werden kann. Dies entspricht dem Prinzip der Kapselung:
weitergegeben wird nur
diejenige Information aus der Klasse, die außen benötigt wird.
Außerdem vereinbart dieselbe Zeile,
dass die Funktion
ein Feld von Adressen zurückgibt. Der Typ Adresse[]
heißt auch Rückgabetyp der
Funktion verdoppeln
.
Man beachte, dass in Argumenten und Resultaten von Funktionen
die Referenzen von Feldern
übergeben werden. Felder werden also nicht kopiert.
Sie verhalten sich genau wie Objekte:
sie können z.B. dort zugewiesen werden, wo der Typ Object
auftritt.
Zuletzt gibt die Methode verdoppeln
mit return neu
das neue Feld als Funktionsergebnis zurück.
Durch den Aufruf
adr = verdoppeln(alt);wird also der vorhandene Platz zum Speichern von Adressen jeweils verdoppelt.
Wir definieren eine weitere
Methode der Klasse
AdressKartei
zum Löschen
von Elementen.
Adresse löschen(int i) // nehme i < frei { Adresse a = adr[i]; for (int j=frei; i<j; j--) adr[j-1] = adr[j]; // kopieren frei--; return a; }Wenn wir mitten im Feld eine Adresse löschen, so entsteht dort eine Lücke, also ein Element ohne Inhalt. Wir verhindern dies durch Verschieben der Referenzen. Dabei nutzen wir aus, dass alle Feldelemente automatisch mit Wert
null
initialisiert worden sind.
Also gilt nach dem ersten Schleifendurchlauf:
adr[frei-1] == adr[frei] == null
Da nach dem Schleifenende zudem Variable frei
um
1
erniedrigt wird, enhalten alle Einträge des Feldes ab
Position frei
den Wert null
.
Weiterhin stellt sich die Frage, wie wir beim Löschen enstehende
undefinierte Situationen behandeln. Die erste Situation betrifft das Löschen
aus einem leeren Feld, die zweite das Löschen
ab Position frei
.
Beide Fälle werden zwar durch die Schleifenbedingung
abgefangen, d.h. die Schleife wird nicht ausgeführt,
aber der zurückgegebene Wert ist der Initialwert der Variable
a
, nämlich null
.
Seine weitere Verwendung kann an anderer Stelle
eine NullPointerException
auslösen, und die Ursache kann dann unter Umständen
schwer nachvollziehbar sein. Wir könnten beide Situationen
mit folgender Bedingung abfangen,
if ( i >= frei) ... // Ausnahmebehandlungund dann eine explizte Ausnahmebehandlung durchführen. Die Ausnahmebehandlung wird in Teil 4 besprochen.
Adresse
auf die neue Klasse zu übertragen, d.h. zu vererben.
BITTE BILD MALEN:
Adresse AdresseMitTelefon -------------------------------- -------------------------------- | String name | | String name | | String vorname | | String vorname | | String alter | | String alter | | | | String telefonnummer | | | | | | void ausgeben() | | void ausgeben() | | public String toString() | | public String toString() | -------------------------------- --------------------------------
Zunächst führen wir die Vererbung ein. Bei der Vererbung gehen alle Elemente und Methoden einer Klasse auf die erbende Unterklasse über. Die Klasse, von der geerbt wird, heißt auch Oberklasse. In der Unterklasse können neue Elemente und Methoden eingeführt werden. Darüber hinaus können auch Methoden der Oberklasse umdefiniert werden. Dieser Vorgang heißt auch Überschreiben.
class AdresseMitTelefon extends Adresse { }Damit haben wir die Unterklasse
AdresseMitTelefon
definiert. Sie besitzt jetzt alle Eigenschaften von Adresse
.
Das Schlüsselwort extends
ist wichtig, um die Oberklasse zu
kennzeichnen. Wird es wie in der Definition von Adresse
weggelassen, so ist implizit Object
die Oberklasse.
In Java gibt keine Möglichkeit, mehr
als eine Oberklasse zu vereinbaren, was die sogenannte
Mehrfachvererbung (d.h. dass von mehreren Klassen geerbt wird,
siehe auch Teil 2) ausschließt.
Unser nächster Schritt besteht darin, die Unterklasse um ein Element für
Telefonnummern zu erweitern. Entsprechend definieren wir einen Konstruktor
und zwei neue Methoden ausgeben
und toString
.
Da diese beiden Methoden bereits in der Oberklasse existieren,
werden sie durch die neue Definition überschrieben.
class AdresseMitTelefon extends Adresse { String telefonnummer; AdresseMitTelefon(String vorname, String nachname, int alter, String telefon) { super(vorname, nachname, alter); telefonnummer = telefon; } void ausgeben() { super.ausgeben(); System.out.println("Tel.: " + telefonnummer); } public String toString() { return "AdresseMitTelefon (" + name + " " + vorname + " " + alter + " " + telefonnummer + ")" ; } }Der Konstruktor initialisiert die Namen, das Alter und die Telefonnummer. Hierzu verwenden wir den bereits existierenden Konstruktor der Oberklasse:
Adresse(String vorname, String nachname, int alter)Um ihn aufrufen zu können, müssen wir ihn allerdings über den Bezeichner
super
ansprechen. Der Aufruf sieht dann so aus:
super(vorname, nachname, alter);Die Situation wird durch die Möglichkeit kompliziert, dass Konstruktoren, wie auch andere Methoden, überladen sein können. Die Überladung wird aufgelöst, indem die Argumenttypen des Aufrufs mit denen der Definition verglichen werden, und daraufhin die passende Definition ausgewählt wird. Um die Überladung eindeutig auflösen zu können, wird von Java vorausgesetzt, dass die Typen der Definitionen verschieden sind.
In unserem Fall ist der Konstruktor Adresse
überladen.
Den Regeln der Überladung entsprechend wird also mit
super()
der argumentlose
Konstruktor Adresse()
ausgewählt.
Der Bezeichner super
hat aber auch noch eine andere Bedeutung:
er wird für den Zugriff auf ein Element oder eine Methode der Oberklasse
verwendet.
Bei den vererbten Elementen kann wahlweise
der Bezeichner super
oder this
verwendet werden, weil die Elemente identisch sind.
Für Methoden hingegen gilt dies im Allgemeinen nicht,
weil die Unterklasse Methoden überschreiben kann.
So bezeichnen z.B. super.ausgeben()
und this.ausgeben()
unterschiedliche Methoden.
Objekte unserer neuen Klasse können wir nun in derselben Weise behandeln
wie diejenigen der Klasse Adresse
:
Adresse adr; adr = new AdresseMitTelefon("Oma", "Mustermann", 87, "0180/362606"); adr.ausgeben();In diesem Beispiel deklarieren wir eine Variable des Typs
Adresse
,
an die wir ein Objekt der Klasse AdresseMitTelefon
zuweisen.
Generell gilt die Regel, dass wir an eine Variable Werte eines jeden Untertyps
zuweisen dürfen.
Dies ist möglich, da die Vererbung ja garaniert, dass
AdresseMitTelefon
alle Eigenschaften von Adresse
erbt. Das Programm "weiß" zwar nicht, welcher Programmcode mit
adr.ausgeben()
auszuführen ist, aber das Objekt "weiß"
es, da es seinen eigenen Typ kennt.
Deshalb ruft das Programm Methoden über die Objekte auf, die auf den
richtigen Programmcode verweisen.
Im obigen Beispiel wird deshalb
die Methode ausgeben()
der Klasse AdresseMitTelefon
aufgerufen.
Diese "Typautomatik", die immer die Verwendung aller Unterklassen mit einschließt, heißt Polymorphie oder auch Generizität. Methoden, die unabhängig vom Typ arbeiten, heissen deshalb auch polymorph oder generisch.
Unser Feld speichert also beide Adressarten, ohne dass das Programm
modifiziert werden musste.
Es ist allerdings nicht möglich, Objekte anderer Klassen
im Feld zu speichern, die nicht einer Unterklasse von
Adresse
angehören.
Wir können dies nur erreichen, indem wir Object
als Feldtyp wählen.
Weil flexible Felder in vielen Anwendung benötigt werden, stellt die
Java-Standardbibliothek
bereits ein wahrhaft generisches Feld mit
automatischer Größenanpassung
zur Verfügung: die Klasse Vector
.
Vector
- komfortabel Daten sammelnVector
ist für die Sammlung von veränderlichen
Mengen von Objekten gedacht.
Sie erlaubt den wahlfreien Zugriff auf beliebig viele Elemente einer
beliebigen Klasse, sorgt für die automatische
Anpassung des Speicherplatzbedarfs, und
enthält Methoden zum Einfügen und Löschen
von Elementen.
Die Verwendung der Klasse Vector
erlaubt uns damit eine alternative und
wesentlich kürzere Realisierung der Klasse AdressKartei
.
Insbesondere ersparen wir uns den Aufwand, die automatische
Vergrößerung des Feldes auszuprogrammieren.
Dies ist auch ein Beispiel dafür, wie durch die Objektorientierung
existierende Programme wiederverwendet werden können.
import java.util.Vector; class AdressKartei { Vector adrVector; AdressKartei() { adrVector = new Vector(16); } void einfügen(Adresse adr) { adrVector.addElement( adr ); } void löschen(int index) { adrVector.removeElementAt( index ); } }Zuerst wird die Standardbibliothek
java.util.Vector
importiert,
um die Klasse Vektor bekanntzumachen.
Der Konstruktor AdressKartei
legt einen Vektor
der Größe 16
an.
Die Methoden einfügen
und löschen
reichen den Aufruf an die entsprecheden Methoden von Vector
weiter.
Mit der Methode einfügen
wird ein Objekt der
Klasse Adresse
in den Vektor
adrVector
eingefügt. Dazu benutzen wir die Methode
addElement
, deren Typ in der Standardbibliothek
durch addElement(Object)
vereinbart ist.
Damit darf das Argument Objekt einer beliebigen Klasse sein.
Das heißt, dass
der Grundtyp des Vektors
ebenfalls die Klasse Object
ist,
und dass Vektoren beliebige Objekte speichern.
Wir können Vektoren auf zweierlei Weise benutzen: als Behälter ganz verschiedener Objektarten, und als Behälter über einer Klasse und ihren Unterklassen. Wir wählen die zweite Variante, in der wir Adressen und ihre Erweiterung abspeichern.
Nun zeigen wir noch, wie die neue Klasse innerhalb eines Hauptprogramms benutzt werden kann.
public class AdressProgramm { public static void main(String argv[]) { AdressKartei kartei = new AdressKartei(); kartei.einfügen( new Adresse("Sabine", "Müller", 13) ); kartei.einfügen( new AdresseMitTelefon("Oma", "Mustermann", 87, "0180/362606") ); } }
Mit dem Konstruktor AdressKartei()
erzeugen wir ein Objekt
der Klasse Vector
mit einer Anfangsgröße
von sechzehn Objekten. Doch dies ist nur ein Anfangswert! Sollten
mehr als 16 Objekte in adr
eingefügt werden, so
vergrößert sich der verfügbare
Speicherplatz automatisch. Wie die
Klasse Vector
dies genau realisiert brauchen wir dabei nicht zu wissen.
Wir können uns ja vorstellen, daß es ähnlich wie in der
Realisierung von AdressKartei_1
abläuft.
Adresse
ab,
aber der Übersetzer kann diese Tatsache nicht entdecken.
Aus diesem Grund hat die Methode, die ein Element des Vektors liest, den allgemeinsten
Rückgabetyp Object
:
Object elementAt(int index)Eine Schwierigkeit besteht nun darin, dass in der folgenden Anweisung der Rückgabetyp (
Object
) nicht mit dem
benötigten Typ (Adresse
) übereinstimmt:
Adresse adr = vec.elementAt(0);Dies wird vom Übersetzer nicht zugelassen, weil dann ein Zugriff wie
adr.name
undefiniert sein könnte: Objekte des
allgemeineren Typs besitzen dieses Element nicht.
Es gibt nun zwei Lösungsmöglichkeiten. Die erste ist eine erzwungene Typanpassung (engl. cast):
Adresse adr; adr = (Adresse) vec.elementAt(0); // TypanpassungDadurch wird gewissermaßen die strenge Typisierung zur Übersetzungszeit umgangen. Der Benutzer muss nun zusichern können, dass das zugewiesene Objekt auch wirklich vom Typ
Adresse
oder einer Unterklasse von Adresse
ist.
Allerdings wird zugleich ein Typtest im Code abgesetzt, der diese Zusicherung
zur Laufzeit überprüft. Sollte dann die Zusicherung nicht gelten, so wird
eine Ausnahme der Art java.lang.ClassCastException
ausgelöst.
Das Programm wird dann abgebrochen, sofern diese Ausnahme nicht explizit behandelt wird
(wie das geschieht, wird im nächsten Kursteil besprochen).
Wir sehen also, dass erzwungene Typanpassungen durch Typtests zur
Laufzeit abgesichert werden, dadurch aber zusätzlichen Aufwand verursachen.
Dieser Aufwand ist unseren Erfahrungen nach in großen Anwendungen
nicht vernachlässigbar.
Ist Effizienz ein wichtiger Gesichtspunkt, so kann dieser Aufwand
durch spezialisierte Programmversionen vermieden werden.
In unserem Fall würden wir dann auf die speziellere Klasse
AdresseKartei_1
zurückgreifen.
Es ist also unter Umständen zwischen
der Allgemeinheit der Programmkonstruktion
und der Effizienz abzuwägen.
Eine sichere Lösung besteht in einer expliziten Abfrage,
ob das gelesene Element typkonform mit Adresse
ist:
Adresse adr; if (obj instanceof Adresse) adr = (Adresse) vec.elementAt(0); // TypanpassungTypkonformität bedeutet, dass der Typ des Objektes von der Klasse
Adresse
oder einer Unterklasse sein muss.
Diese Variante stellt uns die Möglichkeit einer gezielten Ausnahmebehandlung
zur Verfügung.
Zum Schluss möchten wir noch alle abgespeicherten Adressen
nacheinander auf dem Bildschirm ausgeben.
Dazu fügen wir folgende Methode in die Klasse AdressKartei
ein:
void ausgeben() throws ArrayIndexOutOfBoundsException { for (int i = 0; i < adrVector.size(); i++) { Object obj = adrVector.elementAt(i); if (obj instanceof Adresse) { ((Adresse)obj).ausgeben(); }; } }Die Methode
size()
liefert uns die Anzahl der eingetragenen
Objekte. Mit elementAt(int)
liefert uns Vector
das Element an der angegebenen Position.
Die Bedeutung der Zeile
throws ArrayIndexOutOfBoundsExceptionwird im folgenden Abschnitt behandelt.
size()
liefert immer die aktuelle
Anzahl der Elemente in einem Vektor.
Aber grundsätzlich kann es vorkommen,
dass wir z.B. durch eine Programmierfehler
eine noch nicht existierende Elementnummer anfordern.
Dies führt bei der Programmausführung
zu einer Ausnahme. Die Sprachbeschreibung von Java macht
uns darauf aufmerksam, dass in der Methode elementAt(int)
eine solche Ausnahme entstehen kann. Wir müssen
diese Ausnahme entweder in der aktuellen Klasse
behandeln oder aber weitergeben. Dieses Weitergeben wird
mit Hilfe des Schlüsselwortes throws
vereinbart.
In unserem Beispiel geben wir sie der Einfachheit halber
mit der Deklaration
throws ArrayIndexOutOfBoundsException
an die aufrufende Methode weiter, die dann eine Fehlerbehandlung vorsieht
(oder ihrerseits weitergibt).
Wird eine Ausnahme nicht abgefangen, so wird sie bis zur Aufrufebene
des Java-Laufzeitsystems weitergereicht. Dort erfolgt dann ein Programmabbruch
mit Angabe der Ursache der Ausnahme.
So würde z.B. der Zugriff auf einen leeren Vektor:
Vector vec = new Vector(4); Object o = vec.elementAt(2);folgende Fehlermeldung verursachen:
java.lang.ArrayIndexOutOfBoundsException: 2 >= 0Die Fehlermeldung sagt aus, dass auf das zweite Feldelement zugegriffen worden ist, obwohl noch keine Elemente eingefügt worden sind. Wie man sieht, sind die Fehlermeldungen im Vergleich zu den meisten anderen Programmiersprachen ausführlich ud hilfreich.
Wie weiter oben gezeigt wurde, können wir in einem Vektor ein Objekt an beliebiger Stelle löschen, ohne dass wir uns um die entstehende Lücke kümmern müssen. Da es auch hierbei wieder zu einer Verletzung gültiger Indexbereiche kommen kann, muss die Behandlung der entsprechenden Ausnahme vereinbart werden. Wie zuvor geben wir die Ausnahme an die aufrufende Routine weiter.
void löschen(int i) throws ArrayIndexOutOfBoundsException { adr.removeElementAt(i); }
Wir haben also gesehen, wie man in Java Felder und Vererbung einsetzen kann. Beide Themen sind miteinander verwoben, denn ohne Vererbung wäre es sehr schwer, Felder über beliebigen Typen flexibel zu benutzen. Beim nächsten mal geht's dann darum, wie wir die Ausnahmen behandeln. Außerdem werden wir lernen, Daten einzugeben und in Dateien abzulegen. Bis dahin viel Spaß mit Java!