Kapitel 9 Ein- und Ausgabe In den bisherigen Programmbeispielen wurden die Benutzereingaben immer über die Kommandozeile und Ausgaben immer durch Aufruf der Methode System.out.println realisiert. Tatsächlich sind die Einund Ausgabemöglichkeiten von Java weit mächtiger. Dieses Kapitel behandelt die Grundlagen hiervon, Ströme und einige Beispiele für deren Verwendung. 9.1 Ströme Input und Output (kurz I/O) in Java basieren auf Strömen (engl. streams). Sie sind geordnete Folgen von Daten, die einen Ursprung (input stream) oder ein Ziel (output stream) haben. Vorteil: Der Programmierer wird von den spezifischen Details des zugrundeliegenden Betriebssystems befreit, indem Zugriffe auf Systemressourcen einheitlich mittels Strömen möglich sind. Die Standard-Ströme eines Java-Programms sind in Abbildung?? dargestellt. Standard Eingabestrom System.in Java Programm Standard Ausgabestrom System.out Standard Fehlerstrom System.err Abbildung 9.1: Standardströme 127
Das Paket java.io definiert abstrakte Klassen für grundlegende Ein- und Ausgabeströme. Diese abstrakten Klassen werden dann erweitert und liefern mehrere nützliche Stromtypen. Derzeit enthält das I/O-Paket 13 Unterklassen, die teilweise wiederum Unterklassen mit weiteren Unterklassen besitzen (Abbildung??). Object InputStream OutputStream FileInputStream...... FileOutputStream FilterInputStream DataInputStream... FilterOutputStream 3 PrintStream DataOutputStream... BufferedInputStream BufferedOutputStream Abbildung 9.2: I/O-Klassenhierarchie 9.1.1 Die Klasse InputStream Alle InputStream erweiternden Klassen können Bytes aus verschiedenen Quellen einlesen. Das Einlesen kann byteweise oder in Blöcken von Bytes beliebiger Größe durchgeführt werden. Wir listen hier nur die Methoden von InputStream auf und gehen nicht detailliert auf deren Unterklassen ein. public abstract int read() throws IOException; Es wird das nächste Byte aus dem Strom gelesen und (als Integerzahl im Bereich von 0 bis 255) zurückgegeben. Bei Erreichen des Stromendes wird 1 zurückgegeben. public int read(byte b[]) throws IOException; Es werden b.length Bytes aus dem Strom gelesen und in dem Feld b abgelegt. Der Rückgabewert ist die tatsächliche Anzahl der gelesenen Bytes oder bei Erreichen des Stromendes 1. 128
public int read(byte b[], int off, int len) throws IOException; Es werden len Bytes gelesen und ab der Position off im Feld b abgelegt. Die Anzahl der gelesenen Bytes wird wieder zurückgegeben. public long skip(long n) throws IOException; Die nächsten n Bytes werden überlesen. public int available() throws IOException; Es wird die Anzahl der Bytes zurückgegeben, die von dem Strom noch gelesen werden können. public void close() throws IOException; Der Strom wird geschlossen und die benutzten Ressourcen sofort wieder freigegeben. public boolean marksupported(); Es wird geprüft, ob der Strom die Methoden mark und reset unterstützt. public synchronized void mark(int readlimit); Die aktuelle Position im Strom wird markiert. public synchronized void reset() throws IOException; Es wird zu der Position zurückgesprungen, die mit mark markiert wurde. Analog zu InputStream und seinen Unterklassen gibt es die Klasse OutputStream und deren Erweiterungen, die zum Schreiben von Zeichen benötigt werden. 9.1.2 File-Ströme Um Dateien einlesen zu können, reicht es, ein FileInputStream-Objekt anzulegen. Beispiel 9.1.1 import java.io.*; class ReadDemo { public static void main(string[] args) throws FileNotFoundException, IOException { FileInputStream fileinputstream = new FileInputStream(args[0]); int ch, count=0; while((ch = fileinputstream.read())!= -1) { count++; 129
System.out.println("Die Datei enthält "+count+" Bytes."); Ausgaben werden durch die Klasse FileOutputStream ermöglicht, z.b. FileOutStream fos = new FileOuputStream("out.txt"); fos.write( a ); 9.1.3 Gepufferte Ströme Die Klassen BufferedInputStream und BufferedOutputStream unterstützen Objekte, die ihre Daten puffern. Damit verhindern diese, dass jedes Lesen oder Schreiben sofort an den nächsten Strom weitergegeben wird. Diese Klassen werden oft in Verbindung mit File-Strömen verwendet, denn es ist relativ ineffizient, auf eine Datei zuzugreifen, die auf der Festplatte gespeichert ist. Das Puffern hilft, diesen Aufwand erheblich zu reduzieren. Ein BufferedInputStream oder BufferedOutputStream entspricht in seinen Methoden dem InputStream bzw. OutputStream. Die Konstruktoren nehmen einen entsprechenden Strom als Parameter. Bei einem Methodenaufruf werden die Daten gepuffert und bei einem vollen Puffer mit den entsprechenden Methoden des Stroms geschrieben bzw. gelesen. Manuell lässt sich ein Puffer auch immer mit der Methode public void flush() leeren. Beispiel 9.1.2 import java.io.*; import java.util.*; class WriteDemo { private static float time(outputstream os, long j) throws IOException { Date start = new Date(); for(int i=0; i<j; i++) { os.write(1); os.close(); Date end = new Date(); return (float)(end.gettime()-start.gettime()); public static void main(string[] args) throws IOException { FileOutputStream unbufstream; 130
BufferedOutputStream bufstream; long iterate; iterate = Long.parseLong(args[0]); unbufstream = new FileOutputStream("f.unbuffered"); bufstream = new BufferedOutputStream( new FileOutputStream("f.buffered")); float t1 = time(unbufstream,iterate); System.out.println("Write file unbuffered: "+t1+" ms"); float t2 = time(bufstream,iterate); System.out.println("Write file buffered: "+t2+" ms"); System.out.println("Thus, the version with buffered streams is "+ (t1/t2)+" times faster."); > java WriteDemo 10000 Write file unbuffered: 102.0 ms Write file buffered: 4.0 ms Thus, the version with buffered streams is 25.5 times faster. > java WriteDemo 100000 Write file unbuffered: 964.0 ms Write file buffered: 14.0 ms Thus, the version with buffered streams is 68.85714 times faster. 9.1.4 Datenströme Man stellt bei häufigem Gebrauch von Strömen schnell fest, dass Byteströme allein kein Format bieten, in das alle Daten eingezwängt werden können. Vor allem die elementaren Datentypen von Java können in den bisher behandelten Strömen weder gelesen noch geschrieben werden. Die Klassen DataInputStream und DataOutputStream definieren Methoden zum Lesen und Schreiben, die komplexe Datenströme unterstützen. FileInputStream fis = new FileInputStream(args[0]); BufferedInputStream bis = new BufferedInputStream(fis); DataInputStream dis = new DataInputStream(bis); oder als ein Befehl 131
DataInputStream dis = new DataInputStream( new BufferedInputStream( new FileInputStream(args[0]))); Beispiel 9.1.3 import java.io.*; class DataIODemo { public static void main(string[] args) throws FileNotFoundException, IOException { DataOutputStream dataoutputstream = new DataOutputStream(new FileOutputStream(args[0])); double[] doubles = {16.57,15.44,9.99; for(int i=0; i<doubles.length; i++) { dataoutputstream.writedouble(doubles[i]); dataoutputstream.close(); DataInputStream dis = new DataInputStream(new FileInputStream(args[0])); double sum = 0.0; try { while(true) { //Read next double until EOF is reached and an EOFException //is thrown; add the double to sum sum += dis.readdouble(); catch(eofexception e) { System.out.println("Sum of doubles: "+sum); finally { dis.close(); > java DataIODemo test.dat Sum of doubles : 42.0 132
9.2 Stream Tokenizer Das Zerlegen von Eingabedaten in Token ist ein häufiges Problem. Java stellt eine Klasse StreamTokenizer für solche einfachen lexikalischen Analysen zur Verfügung. Dabei wird gegenwärtig nur mit den untersten 8 Bits von Unicode, den Zeichen aus dem Latin-1 Zeichensatz, gearbeitet, da das interne, die Zeichentypen markierende, Feld nur 256 Elemente hat. Erkennt nexttoken() ein Token, gibt es den Tokentyp als seinen Wert zurück und setzt das Datenfeld ttype auf denselben Wert. Es gibt vier Tokentypen: TT_WORD: Ein Wort wurde eingescannt. Das Datenfeld sval vom Typ String enthält das gefundene Wort. TT_NUMBER: Eine Zahl wurde eingescannt. Das Datenfeld nval vom Typ double enthält den Wert der Zahl. Nur dezimale Gleitkommazahlen (mit oder ohne Dezimalpunkt) werden erkannt. Die Analyse versteht weder 3.4e79 als Gleitkommazahl noch 0xffff als Hexadezimalzahl. TT_EOL: Ein Zeilenende wurde gefunden (nur wenn eolissignificant true ist). TT_EOF: Das Eingabeende wurde erreicht. Beispiel 9.2.1 import java.io.*; class TokenDemo { public static void main(string[] args) throws IOException { FileReader filein = new FileReader(args[0]); StreamTokenizer st = new StreamTokenizer(fileIn); int words = 0; // total word number. int numbers = 0; // total int number. int eols = 0; // total eols. int others = 0; // total others. st.eolissignificant(true); // to treat eols as a character while(st.nexttoken()!= StreamTokenizer.TT_EOF) { // to read next Token and check EOF if(st.ttype == StreamTokenizer.TT_WORD) words++; // if word then total words++. else if(st.ttype == StreamTokenizer.TT_NUMBER) numbers++; // if number then total numbers++. 133
else if(st.ttype == StreamTokenizer.TT_EOL) eols++; // if eols total eols++. else others++; // else others++. System.out.println("File "+args[0]+" contains\n\t"+ words+" words,\n\t"+ numbers+" numbers,\n\t"+ eols+" end-of-lines, and\n\t"+ others+" other characters."); > java TokenDemo TokenDemo.java File TokenDemo.java contains 61 words, 6 numbers, 31 end-of-lines, and 88 other characters. 134
Literaturverzeichnis [1] K. Arnold, J. Gosling: Java T M - Die Programmiersprache. Addison-Wesley, 1996. [2] T.H. Cormen, C.E. Leierson, R.L. Rivest: Introduction to Algorithms. MIT Press, 1990. [3] D. Flanagan: Java in a Nutshell. O Reilly & Associates Inc., 1996. [4] F. Jobst: Programmieren in Java. Hanser Verlag, 1996. [5] H. Klaeren: Vom Problem zum Programm. 2.Auflage, B.G. Teubner Verlag, 1991. [6] K. Echtle, M. Goedicke: Lehrbuch der Programmierung mit Java. dpunkt- Verlag, 2000. 135
136