Java-Dateien schnell und effizient lesen.

Es gibt viele verschiedene Möglichkeiten, Dateien in Java zu lesen. Bei der Suche nach einem Java-Beispiel für das einfache Laden von Dateien kannst du beispielsweise auf die folgenden Java-Klassen für die Arbeit mit Dateien stoßen InputStream, FileInputStream, DataInputStream, SequenceInputStream, Reader, InputStreamReader, FileReader, BufferedReader, FileChannel, SeekableByteChannel, Scanner, StreamTokenizer, Files und ähnliche. Wir sind uns ziemlich sicher, dass es noch mehr Klassen gibt und einige davon es nicht in diese Liste geschafft haben. Natürlich erwähnen wir noch nicht die externen Drittanbieter-Bibliotheken zur Dateiverarbeitung, die ebenfalls verfügbar sind. Die meisten der vorgefertigten Klassen zum Lesen und Schreiben von Dateien in Java befinden sich in den Paketen java.io und java.nio.file. Mit der Einführung der neuen API für Java-Dateien, Java NIO.2 (New I/O), wurde die Situation noch komplizierter. Selbst erfahrene Programmierer stellen sich oft auf Foren die Frage, wie man mit Dateien schnell und effizient arbeitet. Bei der Arbeit mit Dateien sollten wir im Voraus wissen, mit welchen Dateitypen wir arbeiten werden, also ob wir Binärdateien (z.B. Musik im MP3-Format) oder Textdateien lesen müssen. Ebenso, ob die Dateien klein genug sind, um sie vollständig in den Speicher zu laden, oder ob wir mit Dateien arbeiten, die nicht in den Speicher passen und eine andere Verarbeitungsweise erfordern, wie z.B. das Lesen und Verarbeiten zeilenweise. Jede der oben genannten Klassen hat ihre spezifische Verwendung für bestimmte Anwendungsfälle. Im Allgemeinen verwenden wir jedoch binäre Datenabrufe Stream Klassen und Text Reader Klassen. Klassen, die diese beiden Begriffe in ihrem Namen tragen, kombinieren den Abruf von Binär- und Textdaten. Zum Beispiel. Klassen InputStreamReader konsumiert InputStream, verhält sich aber selbst wie Reader. FileReader ist im Grunde eine Kombination aus FileInputStream mit InputStreamReader. Wie wir aus dem vorangegangenen Text ersehen können, kann das Laden von Daten in Java ziemlich kompliziert werden. In unserem Artikel konzentrieren wir uns daher auf drei grundlegende Szenarien zum Laden von Daten, die 90 Prozent der Anwendungsfälle abdecken:

  • Der einfachste Weg, die gesamte Textdatei in eine Variable vom Typ String bzw. Liste zu laden. Liste (und eine Binärdatei in ein Byte-Array).
  • Laden und Verarbeiten großer Dateien, die nicht vollständig in den Speicher passen.
  • Lesen von Dateien, deren Inhalt durch ein Trennzeichen geteilt werden kann, z.B. CSV-Dateien.

Eine kurze Geschichte des Ladens von Dateien in Java

Das Laden von Dateien mit Hilfe von Java-Bibliotheken war bis Java 7 recht umständlich. Die am häufigsten verwendete Klasse zum Laden einer Datei war die FileInputStreamKlasse, die neben der Behandlung von Ausnahmen dafür sorgen musste, dass der Stream sowohl im Falle eines erfolgreichen Ladens als auch im Falle eines Fehlers geschlossen wurde. Ein automatisches Schließen von verwendeten Ressourcen (wie wir es heute mit try-with-resources kennen) gab es damals noch nicht. Daher bevorzugten viele Java-Programmierer Bibliotheken von Drittanbietern wie Apache Commons oder Google Guava, die viel bequemere Optionen boten. Dies änderte sich mit der Einführung von Java 7, das die lang erwartete NIO.2 Datei-API die neben einer Vielzahl von Funktionalitäten auch die Hilfsklasse java.nio.file.Files eingeführt hat, mit der man eine gesamte Text- oder Binärdatei mit einer einzigen Methode einlesen kann.

Laden einer Binärdatei in ein Byte-Array

Mit der Methode Files.readAllBytes() können wir den Inhalt der gesamten Datei in das Byte-Array einlesen:

import java.nio.file.Files;
import java.nio.file.Path;

String fileName = "fileName.dat";
byte[] bytes = Files.readAllBytes(Path.of(fileName));

Die Klasse Path stellt eine Abstraktion einer Datei dar und enthält den Pfad zur Datei im Dateisystem.

Laden einer Textdatei in eine Variable vom Typ String

Seit Java 11 ist es möglich, den Inhalt einer ganzen Textdatei mit der Methode Files.readString() einfach in eine Variable vom Typ String einzulesen, wie folgt:

import java.nio.file.Files;
import java.nio.file.Path;

String fileName = "fileName.dat";
String text = Files.readString(Path.of(fileName));

Die Methode readString() verwendet intern die Methode readAllBytes() und konvertiert dann die Binärdaten in die gewünschte Zeichenkette vom Typ String.

Laden einer Textdatei Zeile für Zeile

Textdateien bestehen in der Regel aus mehreren Zeilen. Wenn wir den Text zeilenweise lesen und verarbeiten möchten, können wir eine seit Java 8 verfügbare Methode verwenden – readAllLines(), die dies automatisch tut.

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

String fileName = "fileName.dat";
List<String> lines = Files.readAllLines(Path.of(fileName));

Dann iterieren wir einfach klassisch durch die Liste und verarbeiten jede Zeile.

Laden einer Textdatei Zeile für Zeile mit String stream

Mit Java 8 wurden Streams als wichtige Sprachverbesserung eingeführt. In derselben Version wurde die Klasse Files um eine neue Methode lines() erweitert, die die gelesenen Zeilen einer Textdatei als Stream von Strings des Typs String zurückgibt. So können wir die Funktionalität von Streams z.B. nutzen Beim Filtern von Daten.

import java.nio.file.Files;
import java.nio.file.Path;

String fileName = "fileName.dat";
Files.lines(Path.of(fileName))
        .filter(line -> line.contains("ERROR"))
        .forEach(System.out::println);

In diesem Beispiel werden wir alle Zeilen der geladenen Datei, die die Zeichenfolge „ERROR“ enthalten, auf der Konsole ausgeben. Diese Methoden decken die gängigsten Szenarien für das Laden kleinerer Dateien ab und haben gemeinsam, dass sie vollständig in den RAM geladen werden. Bei großen Dateien ist es ratsam, sie in Stücken zu laden und sie sofort zu verarbeiten. Wir werden dies weiter unten demonstrieren.

Laden einer großen Binärdatei mit BufferedInputStream

Die Binärdatei wird über InputStream Byte für Byte geladen (bis zum Ende der Datei, wenn -1 zurückgegeben wird), was bei großen Dateien recht lang ist. Dies kann durch das Laden von Daten über BufferedInputStream beschleunigt werden. Diese Klasse umhüllt die Klasse FileInputStream und lädt die Daten vom Betriebssystem nicht mehr Byte für Byte, sondern in 8 KB-Blöcken, die im Speicher gepuffert werden. Das Lesen einer Datei erfolgt dann zwar auch Byte für Byte, ist aber viel schneller, weil es direkt aus dem Speicher erfolgt.

import java.io.BufferedInputStream;
import java.io.FileInputStream;

String fileName = "fileName.dat";
try (FileInputStream is = new FileInputStream(fileName);
     BufferedInputStream bis = new BufferedInputStream(is)) {
    int b;
    while ((b = bis.read()) != -1) {
        // TODO: process b
    }
}

Das blockweise Laden einer Datei (sog. buffering) ist wesentlich schneller als das byteweise Lesen.

Laden einer großen Textdatei mit BufferedReader

Klasse FileReader kombiniert FileInputStream a InputStreamReader. Zum schnelleren Laden von Dateien verwenden wir die Klasse BufferedReaderdie die Klasse FileReader umhüllt und die Verwendung eines 8 KB großen Puffers zusammen mit einem zusätzlichen Puffer für 8192 dekodierte Zeichen ermöglicht. Der Vorteil der Klasse BufferedReader ist, dass sie uns auch erlaubt, die Textdatei zeilenweise zu laden und zu verarbeiten (anstatt sie Zeichen für Zeichen zu laden und zu verarbeiten).

import java.io.BufferedReader;
import java.io.FileReader;

String fileName = "fileName.dat";
try (FileReader reader = new FileReader(fileName);
     BufferedReader bufferedReader = new BufferedReader((reader))) {
    String line;
    while ((line = bufferedReader.readLine()) != null) {
        System.out.println("Line: " + line);
    }
}

Laden einer Datei in Teilen mit Scanner

Manchmal müssen wir eine Datei in Teilen lesen, anstatt sie Zeile für Zeile zu lesen. Klasse Scanner teilt den Inhalt einer Datei mit Hilfe eines Trennzeichens, das ein beliebiger konstanter Wert sein kann, in Teile auf. Üblicherweise wird diese Klasse für CSV-Dateien (comma-separated values) verwendet, d.h. für Dateien, die ein bestimmtes Format haben, bei dem die Daten durch Kommas getrennt sind und die Datei als Tabellenkalkulation in Excel verwendet werden kann.

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

List<String> words = new ArrayList<>();
String fileName = "fileName.csv";
Scanner scanner = new Scanner(Path.of(fileName));
scanner.useDelimiter(",");

while (scanner.hasNext()) {
    String next = scanner.next();
    words.add(next);
}
scanner.close();

In diesem Beispiel haben wir die CSV-Datei nach einzelnen Token geladen (anstelle des klassischen kommagetrennten zeilenweisen Ladens) und diese in einer Liste zur weiteren Verarbeitung gespeichert. In diesem Artikel haben wir die gängigsten Szenarien für das Abrufen von Daten aus einer Datei gezeigt. In Java können kleine Dateien einfach durch den Aufruf einer einzigen Funktion in den Speicher geladen und gespeichert werden, und selbst das Lesen und Verarbeiten großer Dateien kann in Java mithilfe von Pufferklassen effizient durchgeführt werden. Wenn du ein Java Programmierer bist und nach Arbeit suchst, schau dir unsere Mitareiterbenefits an und reagiere auf die neuesten Stellenangebote.

Über den Autor

Jozef Wagner

Java Developer Senior

Viac ako 10 rokov programujem v Jave, momentálne pracujem v msg life Slovakia ako Java programátor senior a pomáham zákazníkom implementovať ich požiadavky do poistného softvéru Life Factory. Vo voľnom čase si rád oddýchnem v lese, prípadne si zahrám nejakú dobrú počítačovú hru.

Informieren Sie uns über sich