Javaschubla.de - Java als erste Programmiersprache

Aus Dateien lesen

Textdateien liest man mit Hilfe der Klasse java.io.FileReader. Zur Erinnerung: Reader und Writer sind zum Lesen und Schreiben von Zeichen da, während InputStream und OutputStream zum Lesen und Schreiben binärer Daten da sind.

Zudem benötigen wir einen BufferedReader, den wir über den FileReader legen, sonst müssten wir die Zeichen einzeln lesen und überprüfen, ob wir an ein Zeilenende gekommen sind. Mit dem BufferedReader können wir mit readLine() eine ganze Zeile einlesen. Außerdem puffert er den darunterliegenen Datenstrom, das ist effizienter.

Leg eine Datei mit dem Namen test.txt in demselben Ordner an, in dem du die java-Datei gleich speichern wirst. Ihr Inhalt soll sein:

Hallo Welt

da draußen

ohne einen Zeilenumbruch nach "da draußen"! Oder auch beliebiger anderer Text in den Zeilen 1 und 3. Die Zeile 2 soll ganz leer sein.

Hier ein erster Entwurf zum Einlesen der Datei und Ausgeben des Inhaltes auf dem Bildschirm:

import java.io.*;

class ReadFile1
{
  public static void main(String[] args) throws IOException
  {
    FileReader fr = new FileReader("test.txt");
    BufferedReader br = new BufferedReader(fr);

    String zeile1 = br.readLine();
    System.out.println(zeile1);
    String zeile2 = br.readLine();
    System.out.println(zeile2);
    String zeile3 = br.readLine();
    System.out.println(zeile3);

    br.close();
  }
}

Wir importieren alle Klassen und Interfaces aus dem Package java.io, in dem sich u.a. die Klassen FileReader und BufferedReader befinden.

Ignoriere für's Erste "throws IOException", Exceptions werden später erklärt.

Erst erzeugen wir einen FileReader, dem Konstruktor übergeben wir den Dateinamen als String. Dann erzeugen wir einen BufferedReader, dessen Konstruktor wir den FileReader übergeben.

Das hätte man auch in einer Zeile schreiben können:

    BufferedReader br = new BufferedReader(new FileReader("test.txt"));

Dann lesen wir mit readLine() die drei Zeilen und geben sie anschließend auf dem Bildschirm aus.

Alle Streams (also auch Reader und Writer) sollte man zum Schluss mit close() schließen. Bei InputStreams und Readern hat es normalerweise keine Auswirkung, wenn man es nicht tut. Bei OutputStreams und Writern kommt es aber zu Fehlern, wenn man es nicht tut, z.B. fehlen häufig die letzten Zeilen in der Datei. Wenn man einen BufferedReader schließt, wird auch der darunterliegende Stream geschlossen. (Man sollte das also nicht tun, wenn der darunter liegende Stream System.in ist.)

Die leere Zeile ist überhaupt kein Problem. In der Variable zeile2 steht dann einfach der leere String "". Und genau das wird auch wieder ausgegeben.

Unter Windows im DOS-Fenster wird das ß nicht richtig angezeigt, das liegt aber nicht an Java und es wurde auch korrekt eingelesen, nur DOS stellt es nicht korrekt dar. Unter Linux gibt es das Problem nicht.

Spielen wir etwas damit herum: Was würde passieren, wenn man versuchen würde, ein viertes Mal aus der Datei zu lesen, obwohl nur drei Zeilen da sind? Würde das Programm abstürzen? Würde einfach der leere String "" in der Variablen zeile4 stehen (wie in zeile2)?

import java.io.*;

class ReadFile1
{
  public static void main(String[] args) throws IOException
  {
    FileReader fr = new FileReader("test.txt");
    BufferedReader br = new BufferedReader(fr);

    String zeile1 = br.readLine();
    System.out.println(zeile1);
    String zeile2 = br.readLine();
    System.out.println(zeile2);
    String zeile3 = br.readLine();
    System.out.println(zeile3);
    String zeile4 = br.readLine();
    System.out.println(zeile4);

    br.close();
  }
}

Nun wird

Hallo Welt

da draußen
null

ausgegeben. Was ist denn das? "null" steht ja nun wirklich nicht in der Datei! null bedeutet so viel wie "nichts". Es hat nichts mit der Zahl 0 zu tun. Es wird englisch "nall" gesprochen (woran ich nie denke, ich sage immer "null").

null ist auch nicht dasselbe wie der leere String "". Beim leeren String "" steht in der Variablen zeile2 eine Adresse, ein Pointer, der auf ein ganz normales Stringobjekt irgendwo im Speicher zeigt. Dieses Stringobjekt hat eben als Wert "". Man kann alle Methoden darauf ausführen, z.B. zeile2.length() ist 0, "".length() ist auch 0 (auch Stringliterale sind ja Objekte). Bei null ist das ganz anders. Die Variable zeile4 ist sozusagen leer. Es steht keine Adresse darin, sondern ein Wert, der bedeutet, dass dieser Pointer nirgendwohin zeigt. zeile4.length() würde nicht 0 zurückgeben, sondern eine NullPointerException verursachen! Das Programm würde also an dieser Stelle mit einer Fehlermeldung abbrechen.

Was würde passieren, wenn wir ein fünftes Mal versuchen, aus der Datei zu lesen?

import java.io.*;

class ReadFile1
{
  public static void main(String[] args) throws IOException
  {
    FileReader fr = new FileReader("test.txt");
    BufferedReader br = new BufferedReader(fr);

    String zeile1 = br.readLine();
    System.out.println(zeile1);
    String zeile2 = br.readLine();
    System.out.println(zeile2);
    String zeile3 = br.readLine();
    System.out.println(zeile3);
    String zeile4 = br.readLine();
    System.out.println(zeile4);
    String zeile5 = br.readLine();
    System.out.println(zeile5);

    br.close();
  }
}

Wieder dasselbe, br.readLine() gibt beim fünften Aufruf auch null zurück.

Hallo Welt

da draußen
null
null

Nun wollen wir aus einer Datei alle Zeilen lesen und ausgeben (in einem echten Programm würde man sicherlich etwas Sinnvolleres damit tun, als sie auf dem Bildschirm auszugeben, man würde sie irgendwie verarbeiten). Natürlich wissen wir vorher normalerweise nicht, wie viele Zeilen die Datei enthält. Man liest also einfach so lange, bis null zurückgegeben wird.

Erster Versuch mit einer do-while-Schleife:

import java.io.*;

class ReadFile2
{
  public static void main(String[] args) throws IOException
  {
    FileReader fr = new FileReader("test.txt");
    BufferedReader br = new BufferedReader(fr);

    String zeile = "";

    do
    {
      zeile = br.readLine();
      System.out.println(zeile);
    }
    while (zeile != null);

    br.close();
  }
}

Die Ausgabe:

Hallo Welt

da draußen
null

Das ist natürlich nicht schön, null wollen wir da zum Schluss nicht stehen haben - in einem echten Programm wäre es noch schlimmer: wenn man null weiterverarbeitet, kann es je nachdem, was man damit tut, zu einer NullPointerException kommen, und überhaupt macht es ja keinen Sinn, null weiter zu verarbeiten, es ist ja gar keine Zeile der Datei.

Wir könnten nach zeile = br.readLine();

    if (zeile == null)
    {
      break;
    }

einfügen. Aber das ist nicht gerade schön. Die Bedingung while (zeile!=null) könnten wir dann auch gerade weglassen, weil die Schleife ja mit break verlassen wird, und stattdessen könnten wir while(true) schreiben. Schleifen in Sonderfällen vorzeitig durch break zu verlassen ist okay, aber sie einzig und allein durch break verlassen zu können ist unschön.

Wir brauchen also eine while-Schleife, denn diese überprüft schon am Anfang die Bedingung und führt den Schleifenkörper also nicht aus, wenn bereits null eingelesen wurde. Das könnte man so machen:

import java.io.*;

class ReadFile3
{
  public static void main(String[] args) throws IOException
  {
    FileReader fr = new FileReader("test.txt");
    BufferedReader br = new BufferedReader(fr);

    String zeile = br.readLine();

    while( zeile != null )
    {
      System.out.println(zeile);
      zeile = br.readLine();
    }

    br.close();
  }
}

Jetzt wird korrekterweise nur noch

Hallo Welt

da draußen

ausgegeben.

Üblicherweise macht man es aber so:

import java.io.*;

class ReadFile3
{
  public static void main(String[] args) throws IOException
  {
    FileReader fr = new FileReader("test.txt");
    BufferedReader br = new BufferedReader(fr);

    String zeile = "";

    while( (zeile = br.readLine()) != null )
    {
      System.out.println(zeile);
    }

    br.close();
  }
}

Das Ergebnis von br.readLine() wird der Variablen zeile zugewiesen und dann wird es gleich mit null verglichen. Man muss zeile=br.readLine() in Klammern setzen, denn der Zuweisungsoperator = hat die niedrigste Priorität - es würde also ansonsten br.readLine() ausgeführt werden, das Ergebnis mit null verglichen, und das Ergebnis dieses Vergleiches (true oder false) der Variablen zeile zugewiesen werden - was ohnehin nicht geht, da zeile nicht vom Typ boolean ist.

while( (String zeile = br.readLine()) != null ) hingegen geht nicht; man kann zwar im Schleifenkopf von while ohne Probleme eine Zuweisung machen (zeile=br.readLine()), aber man kann an dieser Stelle keine Variable definieren.

Wer die Variable nicht vor der Schleife definieren will, kann eine for-Schleife benutzen und den Inkrementteil (den dritten Teil im Schleifenkopf) leer lassen:

    for(String zeile = ""; (zeile = br.readLine()) != null; )
    {
      System.out.println(zeile);
    }

Oder analog zur ersten Variante von ReadFile3 oben:

    for(String zeile = br.readLine(); zeile != null; zeile = br.readLine())
    {
      System.out.println(zeile);
    }

Aber wie gesagt, die übliche Variante ist diese:

import java.io.*;

class ReadFile3
{
  public static void main(String[] args) throws IOException
  {
    FileReader fr = new FileReader("test.txt");
    BufferedReader br = new BufferedReader(fr);

    String zeile = "";

    while( (zeile = br.readLine()) != null )
    {
      System.out.println(zeile);
    }

    br.close();
  }
}

Man kann auch den absoluten Dateinamen benutzen, also z.B.

    FileReader fr = new FileReader("C:\\Eigene Dateien\\Java\\test.txt"); unter Windows oder
    FileReader fr = new FileReader("/home/name/java/test.txt"); unter Linux.

Zur Erinnerung: Da \ den nächsten Buchstaben "escaped" (maskiert), z.B. \n für newline (Zeilenumbruch) oder \t für Tab, muss man \\ schreiben, wenn man einen Backslash haben will. Erfreulicherweise funktioniert aber auch unter Windows

    FileReader fr = new FileReader("C:/Eigene Dateien/Java/test.txt"); 

Übung

Probier aus, was passiert, wenn man einen Dateinamen angibt, und es gibt diese Datei nicht. Finde heraus, wann der Fehler passiert.

(Lösung: Es gibt eine FileNotFoundException, was eine spezielle Art von IOException ist. Der Fehler passiert schon bei der Zeile mit new FileReader(dateiname), nicht erst, wenn man versucht, aus der Datei zu lesen. Das kann man herausfinden, indem man vor und nach jeder Zeile ein System.out.println einbaut oder mit einem Debugger, z.B. dem, der in Eclipse eingebaut ist.)


In der nächsten Lektion wird das Schreiben in Dateien erklärt.