Verzeichnisse auslesen, Dateien verschieben, kopieren, löschen - Feature-Bild

Dateien in Java, Teil 4: Verzeichnisse auslesen, Dateien verschieben, kopieren und löschen

In den bisherigen Artikeln dieser Reihe ging es um das Lesen von Dateien mit Java, das Schreiben von Dateien und die Konstruktion von Verzeichnis- und Dateipfaden mit den Klassen File und Path. Dieser vierte Teil beschreibt die wichtigsten Verzeichnis- und Dateioperation. Folgende Fragen werden beantwortet:

  • Wie listet man alle Dateien eines Verzeichnisses auf?
  • Wie sucht man nach Dateien, auf die bestimmte Kriterien zutreffen, innerhalb eines Verzeichnisbaumes?
  • Wie findet man das aktuelle Verzeichnis?
  • Wie findet man das Home-Verzeichnis des Users?
  • Wie findet man das temporäre Verzeichnis, und wie erstellt man eine temporäre Datei?
  • Wie verschiebt man Dateien mit Java?
  • Wie benennt man eine Datei um?
  • Wie kopiert man eine Datei?
  • Wie löscht man eine Datei?
  • Wie erstellt man einen symbolischen Link mit Java?

Die Fragen werden anhand der NIO.2 File API beantwortet, die mit dem JSR 203 in Java 7 eingeführt wurde.

Verzeichnisoperationen

Für die folgenden Verzeichnisoperationen benötigst du ein Path-Objekt, welches das Verzeichnis repräsentiert. Dieses generierst du beispielsweise über die statische Methode Path.of() (bzw. vor Java 11 über Paths.get()).

Ein ausführliches Tutorial zur Konstruktion von Verzeichnispfaden mit Path, Paths und File findest du im dritten Teil dieser Serie.

Inhalt eines Verzeichnisses auflisten mit Files.list()

Die einfachste Variante den kompletten Inhalt eines Verzeichnisses aufzulisten, ist die Files.list()-Methode. Sie liefert einen Stream von Path-Objekten zurück, die wir im folgenden Beispiel auf System.out ausgeben:

Path currentDir = Path.of(System.getProperty("user.home"));
Files.list(currentDir).forEach(System.out::println);

Verzeichnis rekursiv durchsuchen mit Files.list()

Kommen wir zu einem etwas komplexeren Fall. In folgendem Beispiel wollen wir alle regulären Dateien ausgeben, die sich im Home-Verzeichnis oder einem Unterverzeichnis beliebiger Tiefe davon befinden und deren Name mit „settings“ beginnt.

public class FindFilesRecursivelyExample {

    public static void main(String[] args) throws IOException {
        Path currentDir = Path.of(System.getProperty("user.home"));
        findFileRecursively(currentDir, "settings");
    }

    private static void findFileRecursively(Path currentDir, String fileNamePrefix)
            throws IOException {
        Files.list(currentDir).forEach(child -> {
            if (Files.isRegularFile(child)
                    && child.getFileName().toString().startsWith(fileNamePrefix)) {
                System.out.println(child);
            }

            if (Files.isDirectory(child) ) {
                try {
                    findFileRecursively(child, fileNamePrefix);
                } catch (AccessDeniedException e) {
                    System.out.println("Access denied: " + child);
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            }
        });
    }

}

Mit den Methoden Files.isRegularFile() und Files.isDirectory() kannst du prüfen, ob es sich bei einer Datei um eine reguläre Datei bzw. ein Verzeichnis handelt. Ein weiterer Typ ist der symbolische Link – diesen erkennst du mit Files.isSymbolicLink(). Es ist auch möglich, dass alle drei Methoden false zurückliefern; in dem Fall ist die Datei vom Typ „other“ (was das genau sein könnte, ist unspezifiziert).

Im Beispiel oben müssen wir innerhalb des Lambdas IOException abfangen und mit einer UncheckedIOException wrappen, da der forEach-Consumer des Streams keine checked Exception werfen darf.

Verzeichnis rekursiv durchsuchen mit Files.walk()

Das vorherige Beispiel lässt sich deutlich kürzer und eleganter schreiben – mit Files.walk():

private static void findFileWithWalk(Path currentDir, String fileNamePrefix)
        throws IOException {
    Files.walk(currentDir).forEach(child -> {
        if (Files.isRegularFile(child)
                && child.getFileName().toString().startsWith(fileNamePrefix)) {
            System.out.println(child);
        }
    });
}

Diese Variante hat jedoch den Nachteil, dass sich eine AccessDeniedException nicht, wie zuvor, einzeln abfangen lässt. Tritt hier eine solche Exception auf, bricht die gesamte Files.walk()-Methode ab. Falls das für deine Anwendung vertretbar ist, ist dieser Weg deutlich schöner als der vorherige.

Verzeichnis rekursiv durchsuchen mit Files.walkFileTree()

Eine weitere Variante ist Files.walkFileTree(). Diese Methode implementiert das Visitor-Pattern. Sie übergibt jede Datei innerhalb der Verzeichnisstruktur an einen FileVisitor, den du der Methode mit übergibst. In folgendem Beispiel verwenden wir die Klasse SimpleFileVisitor, die alle Methoden von FileVisitor implementiert. Wir überschreiben lediglich die visitFile()-Methode:

private static void findFileWithWalkFileTree(Path currentDir, String fileNamePrefix)
        throws IOException {
    Files.walkFileTree(currentDir, new SimpleFileVisitor<Path>() {
        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                throws IOException {
            if (Files.isRegularFile(file)
                    && file.getFileName().toString().startsWith(fileNamePrefix)) {
                System.out.println(file);
            }
            return FileVisitResult.CONTINUE;
        }
    });
}

Der Rückgabewert der Methode, FileVisitResult.CONTINUE, gibt an, dass walkFileTree() den Verzeichnisbaum weiter durchlaufen soll. Alternative Rückgabewerte wären:

  • TERMINATE – beendet die walkFileTree()-Methode.
  • SKIP_SIBLINGS – überspringt alle weiteren Dateien des aktuellen Verzeichnisses.
  • SKIP_SUBDIR – überspringt das aktuelle Verzeichnis – dieser Rückgabewert kann jedoch nicht von visitFile() zurückgegeben werden, sondern von FileVisitor.preVisitDirectory().

Auch die walkFileTree()-Variante würde im Fall einer AccessDeniedException die Bearbeitung komplett abbrechen. Hier gibt es allerdings eine Möglichkeit das zu verhindern. Dazu musst du zusätzlich die Methode FileVisitor.visitFileFailed() überschreiben:

private static void findFileWithWalkFileTree(Path currentDir, String fileNamePrefix)
        throws IOException {
    Files.walkFileTree(currentDir, new SimpleFileVisitor<Path>() {
        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                throws IOException {
            if (Files.isRegularFile(file)
                    && file.getFileName().toString().startsWith(fileNamePrefix)) {
                System.out.println(file);
            }
            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
            if (exc instanceof AccessDeniedException) {
                System.out.println("Access denied: " + file);
                return FileVisitResult.CONTINUE;
            } else {
                return super.visitFileFailed(file, exc);
            }
        }
    });
}

Verzeichnis durchsuchen mit Files.find()

Ein alternativer Ansatz ist die Files.find()-Methode. Dieser übergibst du als dritten Parameter einen „Matcher“: das ist eine Funktion, die einen Path und BasicFileAttributes als Eingabeparameter hat und ein boolean zurückgibt, welches anzeigt, ob die jeweilge Datei im Ergebnis enthalten sein soll oder nicht.

private static void findFileWithFind(Path currentDir, String fileNamePrefix)
        throws IOException {
    Files.find(currentDir, Integer.MAX_VALUE,
            (path, attributes) -> Files.isRegularFile(path)
                    && path.getFileName().toString().startsWith(fileNamePrefix))
            .forEach(System.out::println);
}

Auch hier würde eine AccessDeniedException wieder die gesamte Suche vorzeitig beenden. Mir ist keine Möglichkeit bekannt, das bei Files.find() zu umgehen. Erwartest du also Unterverzeichnisse, auf die der Zugriff verwehrt wird, dann solltest du entweder Files.walkFileTree() verwenden und die Methode visitFileFailed() überschreiben, um die Exception abzufangen. Oder, alternativ, Files.list() verwenden und die Rekursion selbst implementieren.

Spezielle Verzeichnisse finden

Achtung: Wenn Du eine ältere Version als Java 11 benutzt, musst Du in den folgenden Code-Beispielen Path.of() durch Paths.get() ersetzen.

Aktuelles Verzeichnis

Das aktuelle Verzeichnis findet man über die System Property „user.dir“:

Path currentDir = Path.of(System.getProperty("user.dir"));

Home-Verzeichnis des Users

Das Home-Verzeichnis des aktuellen Users findest du über die System Property „user.home“:

Path homeDir = Path.of(System.getProperty("user.home"));

Temporäres Verzeichnis

Und das temporäre Verzeichnis findest du über die System Property „java.io.tmpdir“:

Path tempDir = Path.of(System.getProperty("java.io.tmpdir"));

Möchtest du eine temporäre Datei im temporären Verzeichnis erstellen, brauchst du hierfür nicht zuerst das temporäre Verzeichnis auszulesen. Es gibt hierfür eine Abkürzung. Diese findest Du zu Beginn des folgenden Kapitels „Dateioperationen“.

Dateioperationen

Eine temporäre Datei erstellen

Nachdem das letzte Kapitel mit dem Auslesen des temporären Verzeichnisses geendet hat, beginnt dieses mit einem Shortcut, um temporäre Dateien anzulegen:

Path tempFile = Files.createTempFile("happycoders-", ".tmp");

Die zwei Parameter sind ein Prefix sowie ein Suffix. Dazwischen wird automatisch eine Zufallszahl eingefügt. Wenn ich die Methode wiederholt auf meinem Windows-System ausführe und die Variable tempFile auf der Konsole ausgebe, erhalte ich folgende Ausgabe:

tempFile = C:\Users\svenw\AppData\Local\Temp\happycoders-7164892815754554616.tmp
tempFile = C:\Users\svenw\AppData\Local\Temp\happycoders-3557939636108137420.tmp
tempFile = C:\Users\svenw\AppData\Local\Temp\happycoders-16515581992479122220.tmp
tempFile = C:\Users\svenw\AppData\Local\Temp\happycoders-4078166990204004103.tmp

Unter Linux sieht es so aus:

tempFile = /tmp/happycoders-6859515894563322081.tmp
tempFile = /tmp/happycoders-3688163816397144832.tmp
tempFile = /tmp/happycoders-2576679508175526427.tmp
tempFile = /tmp/happycoders-8074586277964353976.tmp

Wichtig zu wissen ist, dass createTempFile() nicht nur das jeweilige Path-Objekt erzeugt, sondern tatsächlich jeweils eine leere Datei anlegt.

Wie verschiebt man eine Datei in Java?

Eine Datei wird mit der Methode Files.move() verschoben. Das folgende Beispiel legt eine temporäre Datei an und verschiebt diese ins Home-Verzeichnis des angemeldeten Users:

Path tempFile = Files.createTempFile("happycoders-", ".tmp");
Path targetDir = Path.of(System.getProperty("user.home"));
Path target = targetDir.resolve(tempFile.getFileName());
Files.move(tempFile, target);

Wichtig ist hierbei, dass der zweite Parameter der move()-Methode die Zieldatei repräsentiert, nicht das Zielverzeichnis! Würde man hier Files.move(tempFile, targetDir) aufrufen, würde das zu einer FileAlreadyExistsException führen. Daher wird im Beispiel mit der resolve()-Methode das targetDir mit dem Dateinamen der zu kopierenden Datei verkettet.

Wie verschiebt man ein Verzeichnis mitsamt aller Unterverzeichnisse?

Ein Verzeichnis kann genau wie eine Datei verschoben werden. In folgendem Beispiel werden zwei temporäre Verzeichnisse angelegt, sowie im ersten Verzeichnis eine Datei. Das erste Verzeichnis wird dann ins zweite verschoben:

Path tempDir1 = Files.createTempDirectory("happycoders-");
Path tempDir2 = Files.createTempDirectory("happycoders-");
Path tempFile = Files.createTempFile(tempDir1, "happycoders-", ".tmp");
Path target = tempDir2.resolve(tempDir1.getFileName());
Files.move(tempDir1, target);

Datei umbennenen mit Java

Letztendlich ist das Umbenennen einer Datei (oder eines Verzeichnisses) ein Spezialfall des Verschiebens, bei dem das Zielverzeichnis dem Quellverzeichnis gleicht und sich lediglich der Dateiname ändert. In folgendem Beispiel wird eine temporäre Datei nach „happycoders.tmp“ umbenannt:

Path tempFile = Files.createTempFile("happycoders-", ".tmp");
Path target = tempFile.resolveSibling("happycoders.tmp");
Files.move(tempFile, target);

Der Aufruf tempFile.resolveSibling("happycoders.tmp") ist dabei ein Shortcut für tempFile.getParent().resolve("happycoders.tmp"): Aus der Quelldatei wird das Verzeichnis extrahiert und dieses mit dem neuen Dateinamen verkettet.

Wie kopiert man eine Datei in Java?

Das Kopieren einer Datei funktioniert ähnlich wie das Umbenennen. Im folgenden Beispiel wird eine temporäre Datei ins Home-Verzeichnis kopiert:

Path tempFile = Files.createTempFile("happycoders-", ".tmp");
Path targetDir = Path.of(System.getProperty("user.home"));
Path target = targetDir.resolve(tempFile.getFileName());
Files.copy(tempFile, target);

Diese Methode hat einen großen Vorteil gegenüber Eigenimplementationen mit FileInputStream und FileOutputStream, wie sie vor Java 7 und der NIO.2 File API nötig waren: Files.copy() delegiert den Aufruf an betriebssystemspezifische – und damit jeweils optimierte – Implementierungen.

Wie löscht man eine Datei in Java?

Gelöscht wird eine Datei (oder ein Verzeichnis) mit Files.delete():

Path tempFile = Files.createTempFile("happycoders-", ".tmp");
Files.delete(tempFile);

Wird Files.delete() auf einem Verzeichnis aufgerufen, muss dieses leer sein, ansonsten wird eine DirectoryNotEmptyException geworfen. Dies kannst du mit folgendem Code ausprobieren:

Path tempDir = Files.createTempDirectory("happycoders-");
Path tempFile = Files.createTempFile(tempDir, "happycoders-", ".tmp");
Files.delete(tempDir);

Hier wird ein temporäres Verzeichnis angelegt und darin eine temporäre Datei. Dann wird versucht das (nicht leere) Verzeichnis zu löschen.

Symbolischen Link zu einer Datei erstellen

Einen symbolischen Link kannst du mit der Methode Files.createSymbolicLink() erstellen. Achtung: Ziel- und Quelle werden hierbei in umgekerter Reihenfolge übergeben wie bei allen vorherigen Methoden: zuerst kommt der Link-Pfad, dann der Pfad der zu verlinkenden Datei. Das folgende Beispiel erzeugt eine temporäre Datei und legt dann im Home-Verzeichnis einen symbolischen Link zu der erzeugten Datei an.

Path tempFile = Files.createTempFile("happycoders-", ".tmp");
Path linkDir = Paths.get(System.getProperty("user.home"));
Path link = linkDir.resolve(tempFile.getFileName());
Files.createSymbolicLink(link, tempFile);

Dieses Beispiel funktioniert unter Linux ohne Einschränkungen. Unter Windows benötigt man Admin-Rechte, um symbolische Links zu erstellen. Fehlen diese, wird folgende Exception geworfen: FileSystemException: [link]: A required privilege is not held by the client.

Zusammenfassung und Ausblick

Dieser vierte Artikel aus der Serie über Dateien in Java hat die wichtigsten Verzeichnis- und Dateioperationen vorgestellt.

Im nächsten Teil zeige ich dir, wie du mit DataOutputStream und DataInputStream strukturierte Daten schreiben und lesen kannst.

Im Anschluss kommen wir zu den folgenden fortgeschrittenen Themen:

  • Die in Java 1.4 eingeführten NIO-Channels und Buffer, um insbesondere das Arbeiten mit großen Dateien zu beschleunigen
  • File Locking, um parallel – also aus mehreren Threads oder Prozessen – konfliktfrei auf dieselben Dateien zuzugreifen
  • Memory-mapped I/O für rasend schnellen Dateizugriff ohne Streams

Wie immer freue ich mich darüber, wenn du den Artikel teilst, oder über dein Feedback über die Kommentarfunktion. Möchtest du informiert werden, wenn der nächste Teil veröffentlicht wird? Dann trage dich gerne über das folgende Formular in meinen Verteiler ein.

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.