© APSIS GmbH , Polling, 2008
Kapitel 11
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 ClassNotFoundException, 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);
…
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.println("Hallo") also ein Aufruf der PrintStream-Methode println über die Klassenreferenz System.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.)
Mit Hilfe der Klasse java.io.File kann das lokale Dateisystem bearbeitet werden. Das folgende Beispiel zeigt, wie die Dateien des Verzeichnisbaumes (von einem gegebenen oder vom aktuellen Verzeichnis an) in einen Textstrom (Textdatei oder Konsole) 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).
In unserem nächsten Beispiel für SAX zeigen wir, wie weitere Methoden von DefaultHandler (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 , Polling, 2008