© APSIS GmbH extern.gif (1249 Byte), Polling, 2008


Kapitel 11

Implementierung der Persistenz

Unter dieser Voraussetzung ist die Persistenz von Klassen recht unkompliziert realisierbar, wie der folgende Auszug aus der Implementierung von SeqDatei zeigt:

import java.io.*;
public class SeqDateiImpl implements SeqDatei {
  private boolean lesemodus;
  private Object aktuellesObjekt;
  private String dateiname;
 private ObjectInputStream eingabe;
 private ObjectOutputStream ausgabe;
 private final static DateiFehler dateiFehler = new Error("Dateifehler");
 public SeqDateiImpl (String file) { dateiname = file; }
  public void neuBeschreiben() { // setzt Schreibmodus
   lesemodus = false; // ausgabe mit Datei verbinden:
   try { ausgabe = new ObjectOutputStream(new FileOutputStream (dateiname)); }
   catch (IOException e) { throw dateiFehler; } }
  public void zuruecksetzen() { // setzt Lesemodus + Anfang
   lesemodus = true; … } // eingabe ähnlich mit Datei verbinden
  public void eintragen(final Object element) throws SeqDatei.LesemodusError {
   if (lesemodus) throw new SeqDatei.LesemodusError();
   else try { ausgabe.writeObject(element); }
     catch (IOException e) { throw dateiFehler; } }
  public void naechstesElement()
     throws SeqDatei.SchreibmodusError, SeqDatei.DateiendeException {
   if (!lesemodus) throw new SeqDatei.SchreibmodusError ();
   else try { aktuellesObjekt = eingabe.readObject(); }
     catch (EOFException eof) { throw new SeqDatei.DateiendeException(); }
     catch (Exception e) { throw dateiFehler; } }
  public Object aktuellesElement()
     throws SeqDatei.SchreibmodusError, SeqDatei.DateiendeException { …
   return aktuellesObjekt; }
  …
  public class DateiError extends Error { } }

Zu beachten ist, dass die Methode readObject zwei weitere Ausnahmen auswerfen kann: OptionalDataException , falls der Eingabestrom primitive Werte enthält, und Class­Not­Found­Exception, falls dem anstehenden Objekt keine passende Klassendefinition existiert. Im obigen Programm haben wir sie – etwas primitiverweise – als Exception aufgefangen und als DateiError weitergeleitet.

Wollen wir z.B. Eimer-Objekte in einer SeqDatei speichern, so muss sowohl die Klasse Eimer als auch die Klasse Getraenk als serialisierbar markiert werden. Die in Eimer verwendeten Ausnahmen dagegen sind bereits serialisierbar als Unterklassen von Exception:

class Getraenk implements java.io.Serializable { … }
class Eimer implements java.io.Serializable { … }
  …
Eimer eimer = new Eimer();
SeqDatei datei = new SeqDateiImpl("Eimer.dat");
try {
  datei.neuBeschreiben();
  datei.eintragen(eimer);
  …

Standardein- und ausgabe

Wir haben schon erwähnt, dass die Tastatur und die Konsole aus der Klasse System über ihre Klassenreferenzen in, out und err vom Typ InputStream bzw. PrintStream erreicht werden; das Letztere ist eine Erweiterung von OutputStream, in der auch zeilenweise Ausgabe (wie println) implementiert wurde. Somit ist System.out.print­ln("Hallo") also ein Aufruf der PrintStream-Methode println über die Klassenreferenz Sys­tem.out.

Da eine Datei ebenfalls über ein In- bzw. OutputStream-Objekt erreicht wird, haben wir die Möglichkeit, durch polymorphe Verwendung der Methoden readLine und println zur Laufzeit zu entschieden, ob Dateien oder die Standardein- bzw. -ausgabe verwendet wird.

import java.io;
public class Echo { // liest Zeilen und schreibt sie wieder heraus
  private BufferedReader eingabe = new BufferedReader(
   new InputStreamReader(System.in)); // Vorbesetzung: Tastatur
  private PrintStream ausgabe = System.out ; // Vorbesetzung: Konsole
  private boolean tastatureingabe = true;
  private void eingabeSetzen(final String dateiname) throws FileNotFoundException {
   if (dateiname.length() != 0) {
     eingabe = new BufferedReader(new InputStreamReader(
      new FileInputStream(dateiname))); // throws FileNotFoundException
     tastatureingabe = false; } }
  private void ausgabeSetzen(final String dateiname) throws FileNotFoundException {
   if (dateiname.length() != 0)
     ausgabe = new PrintStream(new FileOutputStream(dateiname)); } // throws
  private String zeileLesen(String aufforderung) throws IOException {
   if (tastatureingabe)
     System.out.println(aufforderung);
è      return eingabe.readLine(); } // polymorph // throws IOException
  private void zeileSchreiben(final String zeile) {
è      ausgabe.println(zeile) ; } // polymorph
  public static void main(String[] kzp) throws IOException {
   Echo echo = new Echo();
   echo.ausgabeSetzen(echo.zeileLesen("Name der Ausgabedatei (oder nichts):"));
   echo.eingabeSetzen(echo.zeileLesen("Name der Eingabedatei (oder nichts):"));
   while (true) {
     String zeile = echo.zeileLesen("Bitte Zeile eingeben: ");
   if (zeile == null) break;
     echo.zeileSchreiben(zeile); } } }

Soll die Ein- bzw. Ausgabe an Stelle von Tastatur/Konsole auf Dateien erfolgen, so müssen die IO-Klassen FileInputStream FileOutputStream bzw. FileReader/FileWriter benutzt werden, die bei der Ausprägung als Konstruktorparameter ein Objekt der Klasse java.io.File erhalten.

Der Konstruktorparameter der Klassen FileInputStream, FileOutputStream, FileReader oder FileWriter kann also entweder ein Objekt der Klasse File sein, oder der Dateiname als Zeichenkette; das bedeutet, dass der Benutzer nicht unbedingt mit Objekten der Klasse File zu operieren braucht; seine Schnittstelle zum Dateisystem sind die FileReader/FileWriter- bzw. FileInputStream/FileOutputSteam-Objekte.

In die Version 6  wurde die Klasse java.io.Console eingeführt, die die Ein-/Ausgabe von und zur Konsole vereinfacht:

Console konsole = System.console();
… konsole.readLine(); // liest eine Zeile
konsole.printf(ausgabezeile, formatierung); // formatierte Ausgabe (s. Kapitel  kap_übr 10.1.3.)

Dateisystem

Mit Hilfe der Klasse java.io.File kann das lokale Dateisystem bearbeitet werden. Das fol­gende Beispiel zeigt, wie die Dateien des Verzeichnisbaumes (von einem gegebenen oder vom aktuellen Verzeichnis an) in einen Textstrom (Textdatei oder Kon­so­le) ausgegeben werden können:

import java.io.*;
public class Dateiliste {
  private static PrintStream ausgabe; // Konsole oder Datei
  private static String verzeichnis; // aktuelles oder gegebenes
  private static void dateiliste(File verzeichnis, String praefix) { // rekursiv
   File[] dateien = verzeichnis.listFiles(); // requires verzeichis.isDirectory();
   for (int i = 0; i < dateien.length; i++)
     if (dateien[i].isFile()) // ausgeben
      ausgabe.println(praefix + dateien[i].getName());
   for (int i = 0; i < dateien.length; i++)
     if (dateien[i].isVerzeichnis()) // rekursiv durchsuchen:
      dateiliste(dateien[i], praefix + dateien[i].getName() + '/'); }
  public static void main(String[] kzp) { //mit 0, 1 oder 2 Kommandozeilenparametern
   try {
     ausgabe = kzp.length > 0 ? // kzp[0] = Name der Ausgabedatei
      new PrintStream(new FileOutputStream(kzp[0])) : System.out;
     verzeichnis = kzp.length > 1 ? kzp[1] : "."; // kzp[1] = Startverzeichnis
     dateiliste(new File(verzeichnis), "/");
   } catch (FileNotFoundException ausnahme) { // von new FileOutputStream
     System.err.println("File " + kzp[0] + "cannot be opened"); } } } 

In der Java-Version 1.4 wurden Kanäle (channel) eingeführt. Ein Kanal ist ähnlich wie ein Strom, kann jedoch von nebenläufigen Vorgängen (Kapitel 12.) sicher benutzt werden. So kann z.B. für jeden Dateistrom (d.h. von einem FileInputStream-Objekt) mit der neuen Methode getChannel  der darunter liegende Kanal (ein FileChannel-Objekt) geholt werden, das eine vorgangsichere Position mitführt.

Die Klassen für Kanäle befinden sich im Unterpaket java.nio.channels des neuen Paket java.nio (new input/output).

Lesen XML mit SAX

In unserem nächsten Beispiel für SAX zeigen wir, wie weitere Methoden von De­fault­Hand­ler (z.B. startDocument, endDocument, startElement, endElement, usw.) überschrieben werden können, von denen jede bei bestimmten Ereignissen (am Anfang und am Ende des Dokuments bzw. eines Elements) aufgerufen wird:

import java.io.*; import org.xml.sax.helpers.DefaultHandler; // Adapterklasse //
public class XMLAusgeben {
  public static void main(String[] kzp) throws Exception {
   File einDatei = new File(kzp.length >= 1 ? kzp[0] : "Eingabe.xml");
   DefaultHandler handler = new DefaultHandler() { // anonyme Erweiterung
     public void startDocument() { // Ereignisbehandlungsmethoden
      System.out.println("<?xml version='1.0' encoding='UTF-8'?>"); }
     public void endDocument() {
      System.out.println(""); }
      public void startElement(String namespaceURI, String localName, 
        String qualifiedName, org.xml.sax.Attributes attrs) {
      String elementName = localName.equals("") ? qualifiedName : localName;
      System.out.print("<" + elementName);
      for (int i = 0; i < (attrs == null ? 0 : attrs.getLength()); i++) {
        String attribut = attrs.getLocalName(i);
        if (attribut.equals("")) attribut = attrs.getQName(i);
        System.out.print(" " + attribut + "=\"" + attrs.getValue(i) + "\""); }
      System.out.print(">"); }
     public void endElement(String namespaceURI, String simpleName, 
        String qualifiedName) {
      System.out.print("</" + qualifiedName + ">"); }
     public void characters(char puffer[], int offset, int laenge) {
      System.out.print(new String(puffer, offset, laenge)); } };
   javax.xml.parsers.SAXParserFactory.newInstance().newSAXParser().
     parse(einDatei, handler); } }

In diesem Programm erweitern wir die Adapterklasse DefaultHandler (die leere Methoden für alle Ereignisse des SAX-Parsers enthält) um die benötigten Ereignisbehandlungsmethoden start/endDocument (sie werden am Anfang und am Ende des Dokuments aufgerufen), start/endElement (am Anfang und am Ende eines Elements, d.h. beim öffnenden und schließendem Tag) sowie characters (sie wird beim Textelement aufgerufen). Die Methoden tun in diesem Beispiel nichts anderes, als ein XML-Dokument mit demselben Inhalt auf System.out schreiben.

Die zuletzt aufgerufene Methode parse liest das XML-Dokument aus dem gegebenen File-Objekt (erster Parameter) und aktiviert bei Ereignissen die Rückrufmethoden des Handlers (zweiter Parameter).

Interessant ist die Methode startElement. Hier wird zuerst der Name des Elements aus den Parmetern ermittelt, anschließend werden seine Attribute gelesen und auf out ausgeschrieben.


© APSIS GmbH extern.gif (1249 Byte), Polling, 2008