slider_1.jpeg
Hochwertige Anforderungen sind die Voraussetzung für gute Softwarequalität
slider_2.jpeg
Individuelle Konzepte statt Standardlösungen
slider_3.jpeg
Review- und Bewertungstechniken sind das Fundament für den Erfolg des IT-Projekts
slider_4.jpeg
Den Blick immer über den Tellerrand hinaus!
previous arrow
next arrow

Java Artikel2 Title NO2

Bereits mit dem Java SE Development Kit (JDK) 7 wurde in Java die Art und Weise der Datei-verarbeitung grundlegend neu implementiert. Seitdem existiert neben der altbekannten API java.io.file auch das Package java.nio.file, welches Zugriffe auf Dateisysteme ermöglicht. Mit der in Java 8 neu eingeführten Stream-API bieten sich nun neue Möglichkeiten im Umgang mit Dateien und Verzeichnissen. Nachdem wir Ihnen in den vorangegangenen Artikeln unserer Reihe [1-3] bereits die Lambda-Expressions und die Stream-API vorgestellt haben, wollen wir nun auf die Verbesserungen der New Input Output API (NIO) in dem JDK 8 eingehen.

 

Review der NIO mit dem JDK 7

Mit dem im Juli 2011 veröffentlichten JDK 7 und der darin eingeführten API java.nio.file wurde das Konzept zur Dateiverarbeitung erstmals komplett überarbeitet. Dateizugriffe auf entfernte Systeme (beispielsweise per FTP) ließen sich bis dahin mit der API java.io.File nicht realisieren. Seit Java 7 musste man für solche Operationen nun nicht mehr auf externe Frameworks wie z.B. Apache Commons VFS ausweichen. Mit der im Package enthaltenen Klasse java.nio.file.FileSystem ließen sich Zugriffe auf entfernte Dateisysteme über eigene Klassen realisieren.

Die Abbildungen 1 und 2 zeigen Beispiele für eine byte- und zeilenorientierte Dateiverarbeitung mit Hilfe der Klassen aus dem Package java.io.file vor Java 8. In Abbildung 1 verwenden wir einen FileInputStream, der Dateiinhalt wird dabei byte-weise gelesen.

File file = new File("E:/dev/ku021.txt");
InputStream inStream = new FileInputStream(file);
int data = inStream.read();
while(data != -1){
    // do something
}
inStream.close();

Abb. 1: Byte-orientiertes Lesen einer Datei mittels java.io.FileInputStream

Im Gegensatz zum FileInputStream können unter Verwendung eines BufferedReader Dateien auch zeilenorientiert gelesen werden. Die Abbildung 2 zeigt, wie über die Methode readLine() eine komplette Zeile eingelesen und als String zurückgegeben wird. Wie der Klassenname bereits verrät, werden die Daten in einen Zwischenspeicher geladen. Daraus ergibt sich, dass weniger Zugriffe auf die Datei erfolgen und eine bessere Performance erzielt wird. InputStream-Klassen werden meist dazu verwendet Dateien binär zu lesen.

BufferedReader in = new BufferedReader(new
FileReader("E:/dev/ku021.txt"));
String line = null;
while ((line = in.readLine()) != null) {
    // do something
}
in.close();

Abb. 2: Zeilenorientiertes Lesen einer Datei mittels java.io.BufferedReader

Reader-Klassen hingegen werden zum Lesen von Textdateien oder genauer gesagt von Unicode-Zeichen verwendet. Im Grunde werden Dateien immer byte-weise gelesen. Um nun Bytes in Text zu konvertieren, benötigt man ein Kodierungsschema. Die Methode readline() der Klasse BufferedReader verwendet einen solchen Kodierungsmechanismus, um die gelesenen Bytes zu dekodieren und als Zeichen zum Aufrufer zurückzugeben.

 

NIO mit Java 8 - Was ist neu?

Wie bereits im dritten Teil dieser Reihe [3] beschrieben, ermöglichen die neue Stream-API sowie die Erweiterung des Collection-Frameworks neue Funktionen und eine Parallelisierung bei der Verarbeitung von Prozessen. So kann beispielsweise die Funktion forEach() eines Streams dazu verwendet werden, um über Elemente einer Collection zu iterieren. Die Iteration wird dabei intern gesteuert, sodass sich der Entwickler nicht mehr selbst um den Durchlauf einer Liste kümmern muss. 

Bei der Verarbeitung der Streams unterscheidet man  zwischen intermediate- und terminal-Operationen. Als intermediate-Operationen werden die Aufrufe auf einer Collection bezeichnet, die wieder einen Stream zurückliefern. Sie werden „lazy“ ausgeführt und erst durch eine terminal-Stream-Operation angestoßen, die das Ende einer Stream-Operation bildet. Erst durch beispielsweise den Aufruf der Methode forEach() erfolgt die Ausführung der Intermediates und die Verarbeitung der Daten. Solche Operationen werden als terminal-Operationen bezeichnet.

Was ist nun neu? Die NIO-API in Java 8 wurde nicht  wesentlich überarbeitet, sondern vielmehr um die Funktionalitäten der Lambda Expressions und Streams erweitert. Somit ergeben sich neue Möglichkeiten im Umgang mit den Dateien und der Art und Weise, wie diese verarbeitet werden können. Um Dateien ausfindig zu machen, deren Inhalte zu verarbeiten oder Dateihierarchien abzubilden, kann ab Java 8 auf neue Methoden zurückgegriffen werden.

 

Neue Methoden in java.nio.File

Bei der Dateiverarbeitung in Java 8 spielt die Klasse java.nio.file.Files eine wichtige Rolle. Diese Klasse wurde im JDK 8 um neue statische Methoden erweitert, die mit Hilfe des java.util.stream.Stream-Interface eine auf Streams basierende Verarbeitung ermöglichen. Dabei handelt es sich um die folgenden Methoden, welche unterschiedliche Aufgaben erfüllen: 

  • list()
  • find()
  • lines()
  • walk()

Über die Methode list() können alle in dem angegebenen Verzeichnis enthaltenden Elemente aufgelistet und weiterverarbeitet werden. Der Rückgabewert definiert sich als ein „lazy Stream“ der jeweiligen Elemente. Die Abbildung 3 zeigt den Aufruf der Methode list(). In Kombination mit der Stream-Methode forEach() wird hier eine terminal-Operation angestoßen und die Daten werden verarbeitet. Die Datei- und Verzeichnisnamen werden auf der Konsole ausgegeben.

// listet alle Datein im angegeben Pfad
Files.list(new File("E:/dev").toPath()).forEach(System.out::println);

// Ausgabe der list()-Methode
E:\dev\ku021.txt
E:\dev\ku047.txt
E:\dev\ku214.txt
E:\dev\ku991.txt
E:\dev\log
E:\dev\opt

Abb. 3: Aufruf der Methode list()

Mit der Methode find() lassen sich Dateien, Verzeichnisse oder Links im angegeben Pfad ausfindig zu machen. Ähnlich wie auch die Methode walk() wird rekursiv nach Dateien oder Verzeichnissen gesucht. Die Methode walk() wird im Abschnitt zu Dateihierarchien detaillierter betrachtet.

Die Abbildung 4 zeigt ein Beispiel für die Verwendung der Methode find(). Diese wird auf den Pfad E: angewendet und soll alle Dateien auflisten, die sich im Verzeichnis dev befinden. Des Weiteren kann über den Methodenparameter BiPredicate eine Art Filter angegeben werden, der es ermöglicht, die Suche z.B. auf Dateien, Ordner oder Links einzugrenzen. Um beispielsweise den Typ eines Elements zu definieren, wird für das Objekt BasicFileAttributes eine der folgenden Methoden aufgerufen: isDirectory(), isSymbolicLink(), isRegularFile(), isOther()

Files.find(new File("E:/dev").toPath , Integer.MAX_VALUE,
      (p,attr) -> Files.isReadable( p ) && attr.isRegularFile()).forEach(System.out::println);

// Ausgabe der find()-Methode
E:\dev\ku021.txt
E:\dev\ku047.txt
E:\dev\ku214.txt
E:\dev\ku991.txt
E:\dev\log\2014-12-22T134311.log
E:\dev\log\2015-01-05T081632.log
E:\dev\log\2015-01-05T183054.log
E:\dev\opt\nioTool.exe
E:\dev\opt\resources\settings.xml
E:\dev\opt\security\security.files.dll

Abb. 4: Verwendung der Methode find()

Um Dateiinhalte zu lesen, kann nun zusätzlich die Methode lines() über die Klasse java.nio.file.Files verwendet werden. Diese Methode erwartet einen java.nio.file.Path als Parameter, der den systemabhängigen Dateipfad beinhaltet. Wie auch schon in Abbildung 3 zu sehen ist, rufen wir hier ebenfalls die Methode forEach() auf und geben die gelesenen Zeilen auf der Konsole aus.

Betrachtet man die Implementierung der Methode lines() der Klasse java.nio.Files näher, zeigt sich, dass die eigentliche Funktionalität dahinter, nämlich das Lesen der Zeilen, durch die Methode readline() des BufferedReader realisiert ist. Als Schnittstelle dient die neu hinzugefügte,  gleichnamige Methode lines() (Abbildung 5) innerhalb der Klasse BufferedReader.

Files.lines(new File("E:/dev/ku021.txt")
.toPath()).forEach(System.out::println);

// Ausgabe der lines()-Methode
Zeile eins
Zeile zwei
Zeile drei

Abb. 5: Lesen einer Datei mittels der Methode lines()

 

Dateihierarchien

Anders als bei der Methode find() lassen sich mit walk() nicht nur Dateien auffinden, sondern komplette Dateihierarchien abbilden. Dabei wird der gesamte Verzeichnisbaum rekursiv, ausgehend von dem angegebenen Pfad, in einem Stream gespeichert. Die Abbildung 6 zeigt den Aufruf der Methode walk() in dem Verzeichnis E:/dev.

Files.walk(new File("E:/dev").toPath())
.forEach(System.out::println);

// Ausgabe der walk()-Methode
E:\dev
E:\dev\ku021.txt
E:\dev\ku047.txt
E:\dev\ku214.txt
E:\dev\ku991.txt
E:\dev\log
E:\dev\log\2014-12-22T134311.log
E:\dev\log\2015-01-05T081632.log
E:\dev\log\2015-01-05T183054.log
E:\dev\opt
E:\dev\opt\nioTool.exe
E:\dev\opt\resources
E:\dev\opt\resources\settings.xml
E:\dev\opt\security
E:\dev\opt\security\security.files.dll

Abb. 6: Verwendung der Methode walk()

Dieser Stream lässt sich wie gewohnt mit allen in dem Interface java.util.stream bereitgestellten Funktionen manipulieren und weiterverarbeiten. Im Beispiel von Abbildung 7 wird zusätzlich die Methode filter() verwendet, um nur die Dateien mit der Endung .log in einem Stream zu speichern. Die eigentliche Funktionalität der aufgerufenen Methode filter() wird hier „lazy“ ausgeführt. Die Daten werden erst mit Aufruf der terminal-Operation forEach() verarbeitet.

Files.walk(new File("E:/dev").toPath()).filter(p -> p.toFile().isFile()
      && p.toFile().toString().endsWith(".log")).forEach(System.out::println);

// Ausgabe der walk()-Methode mit filter()
E:\dev\log\2014-12-22T134311.log
E:\dev\log\2015-01-05T081632.log
E:\dev\log\2015-01-05T183054.log

Abb. 7: Aufruf von walk() mit der Methode filter()

 

Fazit

Gerade in Kombination mit Streams und den Lambda Expressions erweisen sich die Neuerungen in der NIOAPI als sehr nützlich und komfortabel. Dateien lassen sich schnell ausfindig machen und deren Inhalte bearbeiten. Des Weiteren können die meisten der vorgestellten Methoden Filterkriterien entgegennehmen, welche die Suche nach bestimmten Dateien, Verzeichnissen, Links, versteckten Dateien oder sogar bestimmten Dateiinhalten erheblich vereinfachen.

 

Bildnachweis: www.freepik.com

 [zum Seitenanfang]

Quellen

  • [1] http://docs.oracle.com/javase/7/docs/api/java/nio/file/Files.html
  • [2] http://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html
sgr

Sebastian Grimm

Gast-Autor