[Teil 2: Alles muss in eine Klasse] [Teil 4: Ausnahmen und Ein-/Ausgabe]

Einstieg in Java
Teil 3: Felder und Vektoren

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.

Felder erzeugen - keine große Kunst

Java bietet zum Ablegen vieler gleichartiger Objekte die Sprachkonstruktion des Feldes. Das Feld wird durch einen einzigen Namen bezeichnet und auf seine Feldelemente kann mit einem Index zugegriffen werden.

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:
  1. die erste Anweisung in der Klammer dient immer der Initalisierung. Wir deklarieren hier die Zählvariable i vom Grundtyp int und setzten sie auf ihren Startwert 0;
  2. nach dem Semikolon folgt die Schleifenbedingung als logischer Ausdruck. Hier überprüfen wir, ob sich der Zähler noch im definierten Indexbereich befindet;
  3. als letztes folgt nach einem Semikolon, das die Schleifenbedingung beendet, die Zählanweisung. Hier erhöhen wir unsere Zählvariable i um eins.
Die drei Komponenten der 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.

Änderung der Feldgröße - recht aufwendig

Da uns die maximale Anzahl von Adressen noch nicht bekannt ist, wären kleine Feldgrößen zu einschränkend, sehr große hingegen nicht praktikabel, weil ja Speicher belegt wird. Was wir also brauchen ist eine bedarfsgesteuerte Anpassung der Feldgröße. Java erlaubt es nicht, die Größe eines Feldes während des Programmlaufes nachträglich zu ändern.

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) ... // Ausnahmebehandlung 
und dann eine explizte Ausnahmebehandlung durchführen. Die Ausnahmebehandlung wird in Teil 4 besprochen.

Erben einer Klasse

Wir werden jetzt Adressen so erweitern, dass zusätzlich eine Telefonnummer gespeichert werden kann. Allerdings soll es weiterhin auch Adressen geben, die keine Telefonnummer enthalten. Wir benutzen dabei die Vererbung, um alle Elemente und Methoden der Klasse 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.

Die Standardklasse Vector - komfortabel Daten sammeln

Die Klasse Vector 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.

Typumwandlung - mit Zwang

Generizität hat ihren Preis. Liest man ein Element des Vektors aus, so ist der tatsächliche Typ zur Übersetzungszeit unbekannt. In unserer Anwendung zum Beispiel legen wir nur Objekte des Typs 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);  // Typanpassung
Dadurch 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);  // Typanpassung
Typkonformitä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 ArrayIndexOutOfBoundsException
wird im folgenden Abschnitt behandelt.

Überschreitung der Feldgrenzen - Ausnahmen ohne Ende

Die Methode 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 >= 0
Die 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!


[Teil 2: Alles muss in eine Klasse] [Teil 4: Ausnahmen und Ein-/Ausgabe]