19.2 Dateisysteme und Pfade
Im Zentrum von NIO.2 stehen die Typen FileSystem und Path:
FileSystem beschreibt ein Datensystem und ist eine abstrakte Klasse. Es wird von konkreten Dateisystemen, wie dem lokalen Dateisystem oder einem ZIP-Archiv, realisiert. Um an das aktuelle Dateisystem zu kommen, deklariert die Klasse FileSystems eine statische Methode: FileSystems.getDefault().
Path repräsentiert einen Pfad zu einer Datei oder einem Verzeichnis, wobei die Pfadangaben relativ oder absolut sein können. Die Methoden erinnern ein wenig an die alte Klasse File, doch der große Unterschied ist, dass File selbst die Datei oder das Verzeichnis repräsentiert und Abfragemethoden wie isDirectory() oder lastModified() deklariert, während Path nur den Pfad repräsentiert und nur pfadbezogene Methoden anbietet. Modifikationsmethoden gehören nicht dazu; dazu dienen Extra-Typen wie BasicFileAttributes für Attribute.
19.2.1 FileSystem und Path
Ein Path-Objekt lässt sich nicht wie File über einen Konstruktor aufbauen, da die Klasse abstrakt ist. File und Path haben aber dennoch einiges gemeinsam, etwa dass sie immutable sind. Das FileSystem-Objekt bietet die entsprechende Methode getPath(…), und ein FileSystem wird über eine Fabrikmethode von FileSystems erfragt.
[zB] Beispiel
Baue ein Path-Objekt auf:
FileSystem fs = FileSystems.getDefault();
Path p = fs.getPath( "C:/Windows/Fonts/" );
Mit einer Abkürzung:
Path p = Paths.get( "C:/Windows/Fonts/" );
Da der Ausdruck FileSystems.getDefault().getPath(…) etwas unhandlich ist, gibt es drei kürzere Wege:
Es gibt die Utility-Klasse Paths, und ein Aufruf von Paths.get(…) liefert den Path (siehe Zwei Freunde müsst ihr werden – bidirektionale 1:1-Beziehungen).
Seit Java 11 gibt es in Path zwei statische of(…)-Methoden.
Auch aus einem File-Objekt lässt sich mit toPath() ein Path ableiten.
Wir werden die Vereinfachung mit Paths.get(…) im Folgenden nutzen. Die Path.of(…)-Variante ist noch etwas neu, und noch benutzt nicht jeder Java 11.
static Path get(String first, String... more)
Erzeuge einen Pfad aus Segmenten. Wenn etwa der Backslash \ der Separator ist, dann ist Paths.get("a", "b", "c") gleich Paths.get("a\\b\\c").static Path get(URI uri)
Erzeugt einen Pfad aus einem URI.
final class java.nio.file.Path
static Path of(String first, String... more) – Seit Java 11
static Path of(URI uri) – Seit Java 11
Jedes Path-Objekt hat auch eine Methode getFileSystem(), mit der wir wieder an das FileSystem kommen.
[»] Hinweis
Der Pfad-String darf unter Java kein \u0000 enthalten, andernfalls gibt es eine Ausnahme. So führt Paths.get("my.php\u0000.jpg") zur Ausnahme »java.nio.file.InvalidPathException: Illegal char < > at index 6: my.php«. Das ist ein wichtiges Sicherheitsmerkmal, um zum Beispiel einen Webserver davor zu schützen, falsche Dateien anzunehmen. Der Dateiname sieht über "my.php\u0000.jpg".endsWith(".jpg") wie eine JPG-Datei aus, aber würde alles nach dem Null-String abgeschnitten (was einige Dateisysteme machen), wäre plötzlich eine Datei my.php angelegt.
Path-Eigenschaften erfragen
Der Path-Typ deklariert diverse getXXX(…)-Methoden (und eine isAbsolute()-Methode), die eine gewisse Ähnlichkeit mit den Methoden aus File haben (siehe Abbildung 19.2). Listing 19.1 zeigt ein paar Anwendungsbeispiele:
Path p = Paths.get( "C:/Windows/Fonts/" );
System.out.println( p.toString() ); // C:\Windows\Fonts
System.out.println( p.isAbsolute() ); // true
System.out.println( p.getRoot() ); // C:\
System.out.println( p.getParent() ); // Fonts
System.out.println( p.getNameCount() ); // 2
System.out.println( p.getName(p.getNameCount()-1) ); // Fonts
Methoden wie getPath(), getRoot() und getParent() liefern alle wiederum Path-Objekte aus den Bestandteilen eines gegebenen Pfades. Es gibt drei Methoden, um das Ergebnis nicht als Path weiterzuverarbeiten:
toString() liefert eine String-Repräsentation,
die Methode toUri() liefert einen URI und
toFile() liefert ein traditionelles File-Objekt.
Dadurch, dass Path eine hierarchische Liste von Namen für den Pfad speichert, lässt sich jedes Segment des Pfades erfragen. Das ist die Aufgabe von getName(int n), das wiederum einen Path liefert. Die Methode subpath(int beginIndex, int endIndex) liefert einen Path mit den Segmenten des angegebenen Bereichs. Path implementiert die Iterable-Schnittstelle, was eine Methode iterator() vorschreibt – das wiederum bedeutet, dass Path rechts vom Doppelpunkt im erweiterten for auftauchen kann.
Praktisch sind die Prüfmethoden startsWith(Path other) und endsWith(Path other), die feststellen, ob der Pfad mit einem bestimmten anderen Pfad beginnt oder endet. Aus Object wird equals(…) überschrieben. Da Path die Schnittstelle Comparable<Path> realisiert, wird zudem compareTo(Path) implementiert. Die Methode equals(…) löst die Pfade nicht auf, sondern betrachtet nur den Namen; die statische Methode isSameFile(Path, Path) der Klasse Files macht diesen Test und löst relative Bezüge auf. Neben equals(…) überschreibt Path auch hashCode().
interface java.nio.file.Path
extends Comparable<Path>, Iterable<Path>, Watchable
String toString()
File toFile()
URI toUri()
Path getFileName()
Path getParent()
Path getRoot()
boolean isAbsolute()
int getNameCount()
Path getName(int index)
Iterator<Path> iterator()
Path subpath(int beginIndex, int endIndex)
boolean endsWith(Path other)
boolean endsWith(String other)
boolean startsWith(Path other)
boolean startsWith(String other)
boolean equals(Object other)
int compareTo(Path other)
int hashCode()
[»] Hinweis
Die Methode getFileName() liefert keinen String, sondern ein Path-Objekt nur mit dem Dateinamen, bei dem also getNameCount() == 1 ist.
Daher führt path.getFileName().endsWith(".xml") zum Testen, ob ein Dateiname wie trainings.xml auf ».xml« endet, nicht zum Ziel, denn endsWith(…) testet, ob das letzte Segment im Pfad (in diesem Fall der komplette Dateiname, nur ohne Verzeichnisangabe) exakt diesen Namen trägt. Als Lösung ist zum Beispiel path.getFileName().toString().endsWith(".xml") gültig.
[»] Hinweis
Unterschiedliche Dateisysteme haben ihre ganz eigenen Beschränkungen und Schreibweisen. Mal ist das Ordnertrennerzeichen ein »/«, mal ein »\«. Eine Wikipedia-Seite[ 264 ](https://en.wikipedia.org/wiki/Path_(computing)#Representations_of_paths_by_operating_system_and_shell) gibt einen Überblick, wie unterschiedliche Betriebssysteme Separatoren oder das Wurzelverzeichnis definieren. Java akzeptiert unter Windows auch den Slash als Ordnertrenner.
Neue Pfade aufbauen
Die resolveXXX(…)-Methoden bauen aus gegebenen Pfaden neue Pfade zusammen. Die Methoden akzeptieren die Parametertypen String und Path.
[zB] Beispiel
Hänge das Benutzerverzeichnis mit dem Bilderverzeichnis zusammen:
Path picturePath = Paths.get( System.getProperty("user.home") )
.resolve( "Pictures" )
.resolve( "Cora" );
System.out.println( picturePath ); // z. B. C:\Users\Chris\Pictures\Cora
Die Unterverzeichnisse lassen sich sonst auch direkt in get(…) eintragen.
Im gleichen Ordner wie stormyDaniels.jpg ist ein neuer Pfad für die Datei pee_tape.tar gefragt:
Path stormy = Paths.get( "d:/geheimakten/usa/stormyDaniels.jpg" );
Path peepee = stormy.resolveSibling( "pee_tape.tar" );
System.out.println( peepee ); // d:\geheimakten\usa\pee_tape.tar
Eine interessante Methode ist auch relativize(Path) – sie liefert aus einer Basisangabe einen relativen Pfad, der zu einem anderen Pfad führt.
[zB] Beispiel
Von c:/Windows/Fonts nach c:/Windows/Cursors führt der relative Pfad ..\Cursors:
System.out.println( Paths.get( "C:/Windows/Fonts" )
.relativize( Paths.get("C:/Windows/Cursors") )
); // ..\Cursors
interface java.nio.file.Path
extends Comparable<Path>, Iterable<Path>, Watchable
Path relativize(Path other)
Path resolve(Path other)
Path resolve(String other)
Path resolveSibling(Path other)
Path resolveSibling(String other)
Normalisierung und Pfadauflösung
Genauso wie die File-Klasse symbolisiert die Path-Klasse einen Pfad, aber dieser muss nicht auf eine konkrete Datei oder ein konkretes Verzeichnis zeigen. Daher liefern die vorgestellten Methoden lediglich Informationen, die sich aus dem vorgegebenen Namen erschließen lassen, ohne auf das Dateisystem zurückzugreifen. Bei relativen Pfaden liefern die Anfragemethoden daher wenig Spannendes:
Path p = Paths.get( "../.." );
System.out.println( p.toString() ); // ..\..
System.out.println( p.isAbsolute() ); // false
System.out.println( p.getRoot() ); // null
System.out.println( p.getParent() ); // ..
System.out.println( p.getNameCount() ); // 2
System.out.println( p.getName(p.getNameCount()-1) ); // ..
Um ein wenig Ordnung in relative Pfadangaben zu bringen, bietet die Path-Klasse die Methode normalize(), die ohne Zugriff auf das Dateisystem die Bezüge ».« und »..« entfernt.
Zum Auflösen der relativen Adressierung mit Zugriff auf das Dateisystem bietet die Path-Klasse die beiden Methoden toAbsolutePath() bzw. toRealPath(…) an:
Path p2 = Paths.get( "../.." );
System.out.println( p2.toAbsolutePath() );
// C:\Users\Christian\Documents\Insel\programme\2_06_Files\..\..
try {
System.out.println( p2.toRealPath( LinkOption.NOFOLLOW_LINKS ) );
// C:\Users\Christian\Documents\Insel
}
catch ( IOException e ) { e.printStackTrace(); }
Die erste Methode toAbsolutePath() normalisiert nicht, sondern löst einfach nur den relativen Pfad in einen absoluten Pfad auf. Die Auflösung vom ../.. erledigt toRealPath(LinkOption...), wobei das (optionale) Argument ausdrückt, ob Verknüpfungen verfolgt werden sollen oder nicht.
[»] Hinweis
Die Methode toRealPath(…) löst eine Ausnahme aus, wenn versucht wird, einen Pfad zu einer Datei aufzulösen, der nicht existiert. So führt zum Beispiel Paths.get("../0x").toRealPath() zur »java.nio.file.NoSuchFileException: C:\Users\Chris\0x«, egal ob mit LinkOption.NOFOLLOW_LINKS oder ohne. Paths.get( "../0x" ).toAbsolutePath() führt zu keinem Fehler.
interface java.nio.file.Path
extends Comparable<Path>, Iterable<Path>, Watchable
Path normalize()
Path toAbsolutePath()
Path toRealPath(LinkOption... options)
Es gibt noch zwei register(…)-Methoden in Path, doch die haben etwas mit dem Anmelden eines Horchers zu tun, der auf Änderungen im Dateisystem reagiert. Sie werden später vorgestellt.
19.2.2 Die Utility-Klasse Files
Da die Klasse Path nur Pfade, aber keine Dateiinformationen wie die Länge oder Änderungszeit repräsentiert, und da Path auch keine Möglichkeit bietet, Dateien anzulegen und zu löschen, übernimmt die Klasse Files diese Aufgaben.
Eine einfache Methode ist size(…), die die Länge der Datei liefert. Anders als bei java.io.File führen nahezu alle Files-Methoden zu einer IOException, wenn es Probleme bei den Ein-/Ausgabe-Operationen gibt.
static long size(Path path) throws IOException
Liefert die Größe der Datei.
Einfaches Einlesen und Schreiben von Dateien
Mit den Methoden readAllBytes(…), readAllLines(…), readString(…), lines(…), write(…) und writeString(..) kann Files einfach einen Dateiinhalt einlesen oder Strings bzw. ein Byte-Feld schreiben.
URI uri = ListAllLines.class.getResource( "/lyrics.txt" ).toURI();
Path p = Paths.get( uri );
System.out.printf( "Datei '%s' mit Länge %d Byte(s) hat folgende Zeilen:%n",
p.getFileName(), Files.size( p ) );
int lineCnt = 1;
for ( String line : Files.readAllLines( p ) )
System.out.println( lineCnt++ + ": " + line );
final class java.nio.file.Files
static byte[] readAllBytes(Path path) throws IOException
Liest die Datei komplett in ein Byte-Feld ein.static List<String> readAllLines(Path path) throws IOException
static List<String> readAllLines(Path path, Charset cs) throws IOException
Lesen die Datei Zeile für Zeile ein und liefern eine Liste dieser Zeilen. Optional ist die Angabe einer Kodierung, standardmäßig ist es StandardCharsets.UTF_8.static String readString(Path path) throws IOException
static String readString(Path path, Charset cs) throws IOException
Lesen eine Datei komplett aus und liefern den Inhalt als String. Ohne Kodierung gilt standardmäßig UTF-8. Beide Methoden sind neu in Java 11.static Path write(Path path, byte[] bytes, OpenOption... options) throws IOException
Schreibt ein Byte-Array in eine Datei.static Path write(Path path, Iterable<? extends CharSequence> lines, OpenOption...
options) throws IOExceptionstatic Path write(Path path, Iterable<? extends CharSequence> lines, Charset cs,
OpenOption... options) throws IOException
Schreiben alle Zeilen aus dem Iterable in eine Datei. Optional ist die Kodierung, die StandardCharsets.UTF_8 ist, so nicht anders angegeben.static Path writeString(Path path, CharSequence csq, OpenOption... options) throws IOException
static Path writeString(Path path, CharSequence csq, Charset cs, OpenOption... options) throws IOException
Schreiben eine Zeichenfolge in die genannte Datei. Der übergebene path wird zurückgegeben. Ohne Kodierung gilt standardmäßig UTF-8. Beide Methoden sind neu in Java 11.
Die Aufzählung OpenOption ist ein Vararg, und daher sind Argumente nicht zwingend nötig. StandardOpenOption ist eine Aufzählung vom Typ OpenOption mit Konstanten wie APPEND, CREATE usw.
[zB] Beispiel
Lies eine UTF-8-kodierte Datei ein:
String s = Files.readString( path );
Bevor die praktische Methode in Java 11 einzog, sah eine Alternative so aus:
String s = new String( Files.readAllBytes( path ), StandardCharsets.UTF_8 );
[»] Hinweis
Auch wenn es naheliegt, die Files-Methode zum Einlesen mit einem Path-Objekt zu füttern, das einen HTTP-URI repräsentiert, funktioniert dies nicht. So liefert schon die erste Zeile des Programms eine Ausnahme des Typs »java.nio.file.FileSystemNotFoundException: Provider ›http‹ not installed«.
URI uri = new URI( "http://tutego.de/javabuch/aufgaben/bond.txt" );
Path path = Paths.get( uri ); //
List<String> content = Files.readAllLines( path );
System.out.println( content );
Vielleicht kommt in Zukunft ein Standard-Provider von Oracle, doch es ist davon auszugehen, dass quelloffene Lösungen diese Lücke schließen werden. Schwer zu programmieren sind Dateisystem-Provider nämlich nicht.
Ein Strom von Zeilen
Die bisherigen Files-Methoden lesen und schreiben als Ganzes, was ein Speicherproblem werden kann. Soll das Einlesen zeilenweise erfolgen, so ist statt readAllLines(), was im Speicher alle Zeilen als String-Objekte vorhält, ein Stream<String> eine gute Alternative. Zwei Methoden liefern einen Strom von Zeilen:
static Stream<String> lines(Path path)
Stream<String> lines(Path path, Charset cs)
Liefern einen Stream von Zeilen einer Datei. Optional ist die Angabe der Kodierung, die sonst standardmäßig StandardCharsets.UTF_8 ist.
Datenströme kopieren
Sollen die Daten nicht direkt aus einer Datei in ein Byte-Array oder eine String-Liste gehen bzw. aus einem Byte-Array oder einer String-Sammlung in eine Datei, sondern von einer Datei in einen Datenstrom, so bieten sich zwei copy(…)-Methoden an:
final class java.nio.file.Files
static long copy(InputStream in, Path target, CopyOption… options)
Entleert den Eingabestrom und kopiert die Daten in die Datei. Die Anzahl kopierter Bytes ist die Rückgabe.static long copy(Path source, OutputStream out)
Kopiert alle Daten aus der Datei in den Ausgabestrom. Die Anzahl kopierter Bytes ist die Rückgabe.
Im Zusammenhang mit Datenströmen kommen wir noch einmal auf diese beiden Methoden zurück.