Zum Inhalt springen

Dateien und Verzeichnisse kopieren, verschieben, löschen (Java Datei Tutorial)

Zwei Ablagekästen aus hellem Karton vor Oxblutrot, links eine weiße Karteikarte, rechts eine gleich große, deren obere Hälfte sich noch aus Lichtpartikeln formt; ein Lichtstrahl verbindet beide

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 Dateioperationen. 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 erstellt man ein Verzeichnis?
  • 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 überschreibt man eine vorhandene Zieldatei?
  • Wie kopiert man ein Verzeichnis samt Inhalt?
  • Wie löscht man eine Datei?
  • Wie löscht man ein Verzeichnis samt Inhalt?
  • 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().

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 homeDir = Path.of(System.getProperty("user.home"));
try (Stream<Path> children = Files.list(homeDir)) {
  children.forEach(System.out::println);
}

Der Stream hält das Verzeichnis geöffnet, bis du ihn schließt. Deshalb gehört er in einen try-with-resources-Block: Erst close() gibt das Datei-Handle frei, das der Stream beim Betriebssystem belegt. Das gilt genauso für die Streams von Files.walk() und Files.find() in den folgenden Abschnitten.

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.

import java.io.*;
import java.nio.file.*;
import java.util.stream.Stream;

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

  private static void findFileRecursively(
        Path currentDir, String fileNamePrefix) throws IOException {
    try (Stream<Path> children = Files.list(currentDir)) {
      children.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“, z. B. eine Named Pipe, eine Gerätedatei oder ein Socket.

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.

Darf das Programm ein Unterverzeichnis nicht lesen, wirft Files.list() für dieses Verzeichnis eine AccessDeniedException. Das Beispiel fängt sie ab, gibt „Access denied“ aus und sucht im nächsten Verzeichnis weiter.

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 {
  try (Stream<Path> paths = Files.walk(currentDir)) {
    paths.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. Der Stream verpackt sie in eine UncheckedIOException und wirft diese aus forEach() – damit endet die gesamte Suche. 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;
    }
  });
}

FileVisitor hat vier Methoden, und walkFileTree() ruft sie in einer festen Reihenfolge auf: Für jedes Verzeichnis kommt zuerst preVisitDirectory(), dann für jeden Eintrag visitFile() – bei einem Unterverzeichnis stattdessen wieder dieselbe Folge – und zum Schluss postVisitDirectory(). Kann walkFileTree() einen Eintrag nicht lesen, ruft es visitFileFailed() auf.

Das folgende Diagramm zeigt die Aufrufe für ein Verzeichnis projekt mit der Datei README.md und dem Unterverzeichnis src, in dem Main.java liegt:

projekt/
├── README.md
└── src/
    └── Main.java

Im Diagramm sind Aufrufe für Verzeichnisse blau umrandet, Aufrufe für Dateien grau. In welcher Reihenfolge die Einträge eines Verzeichnisses kommen – hier README.md vor src –, legt das Dateisystem fest. Die Klammer rechts zeigt, welche Aufrufe entfallen, wenn preVisitDirectory() für src den Rückgabewert SKIP_SUBTREE liefert; die Rückgabewerte erklärt die Liste nach dem Diagramm.

Aufrufreihenfolge von Files.walkFileTree() für das Verzeichnis projekt mit README.md und dem Unterverzeichnis src mit Main.java: 1. preVisitDirectory(projekt), 2. visitFile(projekt/README.md), 3. preVisitDirectory(projekt/src), 4. visitFile(projekt/src/Main.java), 5. postVisitDirectory(projekt/src), 6. postVisitDirectory(projekt); liefert Schritt 3 den Rückgabewert SKIP_SUBTREE, entfallen die Schritte 4 und 5
walkFileTree() ruft für jedes Verzeichnis zuerst preVisitDirectory(), dann die Einträge und zuletzt postVisitDirectory() auf

Der Rückgabewert von visitFile() im Beispiel oben, 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 Einträge des aktuellen Verzeichnisses.
  • SKIP_SUBTREE – überspringt den Inhalt eines Verzeichnisses und den Aufruf der postVisitDirectory()-Methode. Sinnvoll ist dieser Rückgabewert nur in FileVisitor.preVisitDirectory(); aus visitFile() zurückgegeben, wirkt er wie CONTINUE.

Auch die walkFileTree()-Variante bricht im Fall einer AccessDeniedException die Bearbeitung komplett ab. 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 jeweilige Datei im Ergebnis enthalten sein soll oder nicht.

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

Auch hier beendet eine AccessDeniedException die gesamte Suche vorzeitig – wie bei Files.walk() als UncheckedIOException aus forEach(). Files.find() bietet keine Möglichkeit, das zu umgehen.

Die vier Varianten im Vergleich

Die folgende Tabelle stellt die vier Wege, einen Verzeichnisbaum zu durchsuchen, nebeneinander: ob du eine AccessDeniedException für ein einzelnes Verzeichnis abfangen und danach weitersuchen kannst und ob du einen Stream schließen musst.

VarianteAccessDeniedException
einzeln abfangbar
Stream schließen
nötig
Files.list() + eigene Rekursion✓✓
Files.walk()✗✓
Files.walkFileTree()✓
(visitFileFailed())
✗
(kein Stream)
Files.find()✗✓

Ich empfehle Files.walk() – oder Files.find(), wenn du nach Dateiattributen filterst –, solange alle Verzeichnisse lesbar sind. Musst du mit gesperrten Verzeichnissen rechnen, nimm Files.walkFileTree() und überschreibe visitFileFailed().

Verzeichnis erstellen mit Files.createDirectory() und Files.createDirectories()

Ein einzelnes Verzeichnis legst du mit Files.createDirectory() an. Das folgende Beispiel erstellt im Home-Verzeichnis das Verzeichnis happycoders:

Path dir = Path.of(System.getProperty("user.home"), "happycoders");
Files.createDirectory(dir);

Existiert das Verzeichnis schon, wirft createDirectory() eine FileAlreadyExistsException. Fehlt das übergeordnete Verzeichnis, wirft es eine NoSuchFileException.

Files.createDirectories() legt dagegen auch alle fehlenden übergeordneten Verzeichnisse an und wirft keine Exception, wenn das Verzeichnis schon existiert. Das folgende Beispiel erstellt im Home-Verzeichnis happycoders/2026/10 – samt happycoders und 2026, falls es die noch nicht gibt:

Path dir = Path.of(System.getProperty("user.home"), "happycoders", "2026", "10");
Files.createDirectories(dir);

Ich empfehle createDirectories() – außer, ein schon vorhandenes Verzeichnis ist ein Fehler. Dann nimm createDirectory(), denn bei dieser Methode sind die Prüfung und das Anlegen eine einzige atomare Operation: Wollen zwei Prozesse gleichzeitig dasselbe Arbeitsverzeichnis anlegen, gelingt das genau einem, und der andere bekommt die FileAlreadyExistsException.

Spezielle Verzeichnisse finden

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. Dafür gibt es einen Shortcut, den du zu Beginn des folgenden Kapitels „Dateioperationen“ findest.

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 Präfix sowie ein Suffix. Dazwischen fügt createTempFile() eine Zufallszahl ein, die aus einem SecureRandom stammt. 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. Unter Linux und macOS darf nur der Account, der sie angelegt hat, die Datei lesen und schreiben (Rechte rw-------).

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ürdest du hier Files.move(tempFile, targetDir) aufrufen, bekämst du eine FileAlreadyExistsException. Daher wird im Beispiel mit der resolve()-Methode das targetDir mit dem Dateinamen der zu verschiebenden Datei verkettet.

Wie verschiebt man ein Verzeichnis mitsamt allen Unterverzeichnissen?

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);

Zwei Sonderfälle:

  • Innerhalb eines Dateisystems benennt move() nur das Verzeichnis um; seine Einträge bleiben, wo sie sind.
  • Liegt das Ziel auf einem anderen Dateisystem, müsste move() jeden Eintrag einzeln kopieren. Das tut es nicht – bei einem nicht leeren Verzeichnis wirft es stattdessen eine IOException, unter Linux und macOS eine DirectoryNotEmptyException.

Einen Verzeichnisbaum auf ein anderes Dateisystem bringst du deshalb in zwei Schritten: erst kopieren, dann löschen.

Datei umbenennen 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 in „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 Eigenimplementierungen 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.

Unter macOS legt Files.copy() seit Java 20 sogar einen Klon an, wenn du die Option COPY_ATTRIBUTES aus dem nächsten Abschnitt angibst: Per Systemaufruf clonefile() teilen sich Original und Kopie auf APFS ihre Datenblöcke, bis eine der beiden Dateien geändert wird. Die Kopie belegt dann zunächst keinen zusätzlichen Speicherplatz (JDK-8293122).

Vorhandene Zieldatei überschreiben: StandardCopyOption

Existiert die Zieldatei schon, werfen Files.copy() und Files.move() eine FileAlreadyExistsException. Über zusätzliche Parameter vom Typ CopyOption änderst du dieses und anderes Verhalten.

Das folgende Beispiel legt zwei temporäre Dateien an und kopiert die erste über die zweite – mit StandardCopyOption.REPLACE_EXISTING, denn ohne diese Option würde copy() hier eine FileAlreadyExistsException werfen:

Path source = Files.createTempFile("happycoders-", ".tmp");
Path target = Files.createTempFile("happycoders-", ".tmp");
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);

Die Enum StandardCopyOption hat drei Konstanten:

  • REPLACE_EXISTING – überschreibt eine vorhandene Zieldatei. Ein nicht leeres Zielverzeichnis lässt sich so allerdings nicht ersetzen.
  • COPY_ATTRIBUTES – nur für copy(): kopiert die Dateiattribute mit, mindestens den Zeitpunkt der letzten Änderung. Welche Attribute sonst noch mitkommen, hängt von Betriebssystem und Dateisystem ab. Ohne diese Option garantiert copy() nicht, dass Attribute mitkopiert werden.
  • ATOMIC_MOVE – nur für move(): verschiebt die Datei als eine einzige atomare Operation des Dateisystems. Andere Prozesse sehen die Datei also entweder an der alten oder an der neuen Stelle, nie in einem Zwischenzustand. Geht das nicht, etwa weil das Ziel auf einem anderen Dateisystem liegt, wirft move() eine AtomicMoveNotSupportedException.

Übergibst du move() die Option COPY_ATTRIBUTES oder copy() die Option ATOMIC_MOVE, wirft die Methode eine UnsupportedOperationException.

Ich empfehle ATOMIC_MOVE überall dort, wo ein anderer Prozess die Zieldatei liest – etwa wenn du eine fertig geschriebene Datei in ein Verzeichnis verschiebst, das ein anderes Programm überwacht.

Wie kopiert man ein Verzeichnis samt Inhalt?

Rufst du Files.copy() auf einem Verzeichnis auf, legt es am Ziel nur ein leeres Verzeichnis an – die Einträge kopiert es nicht. Einen ganzen Verzeichnisbaum kopierst du mit Files.walkFileTree(): In preVisitDirectory() legst du jedes Verzeichnis am Ziel an, in visitFile() kopierst du jede Datei.

Die folgende Methode copyDirectory() kopiert das Verzeichnis sourceDir mit allen Unterverzeichnissen und Dateien nach targetDir:

private static void copyDirectory(Path sourceDir, Path targetDir)
      throws IOException {
  Files.walkFileTree(sourceDir, new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
          throws IOException {
      Files.createDirectories(targetDir.resolve(sourceDir.relativize(dir)));
      return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
          throws IOException {
      Files.copy(file, targetDir.resolve(sourceDir.relativize(file)),
            LinkOption.NOFOLLOW_LINKS);
      return FileVisitResult.CONTINUE;
    }
  });
}

Den Zielpfad bildet relativize(): Ist sourceDir das Verzeichnis projekt aus dem Diagramm im Abschnitt Verzeichnis rekursiv durchsuchen mit Files.walkFileTree(), liefert sourceDir.relativize() für projekt/src/Main.java den relativen Pfad src/Main.java. targetDir.resolve() hängt ihn an das Zielverzeichnis an.

Die Option LinkOption.NOFOLLOW_LINKS kopiert einen symbolischen Link als Link. Ohne sie kopiert copy() die Datei, auf die der Link zeigt.

Existiert eine Zieldatei schon, bricht copyDirectory() mit einer FileAlreadyExistsException ab. Mit StandardCopyOption.REPLACE_EXISTING als weiterem Argument von Files.copy() überschreibt die Methode sie stattdessen.

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);

Existiert die Datei nicht, wirft Files.delete() eine NoSuchFileException. Willst du in diesem Fall keine Exception, nimm Files.deleteIfExists(). Die Methode liefert true, wenn sie die Datei gelöscht hat, und false, wenn es die Datei nicht gab:

Path tempFile = Files.createTempFile("happycoders-", ".tmp");
boolean deleted = Files.deleteIfExists(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.

Wie löscht man ein Verzeichnis samt Inhalt?

Ein Verzeichnis mit Inhalt löschst du von unten nach oben: erst die Dateien, dann die leeren Verzeichnisse. Genau diese Reihenfolge liefert Files.walkFileTree(), denn es ruft postVisitDirectory() erst auf, wenn alle Einträge eines Verzeichnisses besucht sind – im Diagramm im Abschnitt Verzeichnis rekursiv durchsuchen mit Files.walkFileTree() Schritt 5 für src und Schritt 6 für projekt.

Die folgende Methode deleteDirectory() löscht in visitFile() jede Datei und in postVisitDirectory() jedes Verzeichnis, sobald es leer ist:

private static void deleteDirectory(Path dirToDelete) throws IOException {
  Files.walkFileTree(dirToDelete, new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
          throws IOException {
      Files.delete(file);
      return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc)
          throws IOException {
      if (exc != null) {
        throw exc;
      }
      Files.delete(dir);
      return FileVisitResult.CONTINUE;
    }
  });
}

Ist der Parameter exc von postVisitDirectory() nicht null, konnte walkFileTree() das Verzeichnis nicht vollständig durchlaufen. Die Methode wirft die Exception dann weiter, denn die Exception nennt die Ursache – Files.delete() würde nur mit einer DirectoryNotEmptyException scheitern.

Symbolische Links löscht deleteDirectory() als Link; die Datei, auf die ein Link zeigt, bleibt erhalten, denn walkFileTree() folgt symbolischen Links standardmäßig nicht.

Kürzer geht es mit Files.walk(): Du sortierst die Pfade in umgekehrter Reihenfolge und löschst sie dann einzeln:

private static void deleteDirectoryWithWalk(Path dirToDelete) throws IOException {
  try (Stream<Path> paths = Files.walk(dirToDelete)) {
    paths
        .sorted(Comparator.reverseOrder())
        .forEach(path -> {
          try {
            Files.delete(path);
          } catch (IOException e) {
            throw new UncheckedIOException(e);
          }
        });
  }
}

Die umgekehrte Sortierung bringt jeden Eintrag vor sein Verzeichnis: Der Pfad eines Verzeichnisses ist ein Präfix der Pfade seiner Einträge und steht in aufsteigender Sortierung deshalb vor ihnen – in absteigender also dahinter.

Allerdings sammelt sorted() alle Pfade des Verzeichnisbaums im Speicher, bevor die erste Datei gelöscht wird – ein Path-Objekt pro Eintrag. Für große Verzeichnisbäume empfehle ich deshalb die Variante mit walkFileTree().

Einen symbolischen Link kannst du mit der Methode Files.createSymbolicLink() erstellen. Achtung: Ziel und Quelle übergibst du hier in umgekehrter Reihenfolge als bei allen vorherigen Methoden – zuerst den Link-Pfad, dann den 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 = Path.of(System.getProperty("user.home"));
Path link = linkDir.resolve(tempFile.getFileName());
Files.createSymbolicLink(link, tempFile);

Dieses Beispiel funktioniert unter Linux und macOS ohne Einschränkungen.

Zusammenfassung und Ausblick

Dieser vierte Artikel aus der Serie über Dateien in Java hat die wichtigsten Verzeichnis- und Dateioperationen vorgestellt. Für ganze Verzeichnisbäume – durchsuchen, kopieren, löschen – ist Files.walkFileTree() das Werkzeug, das auch mit gesperrten Verzeichnissen und großen Bäumen zurechtkommt.

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
  • Memory-mapped I/O für komfortablen und rasend schnellen Dateizugriff ohne Streams
  • File Locking, um parallel – also aus mehreren Threads oder Prozessen – konfliktfrei auf dieselben Dateien zuzugreifen

Konntest du etwas aus diesem Artikel mitnehmen? Mit einer Bewertung auf meinem ProvenExpert-Profil hilfst du anderen Entwickler:innen einzuschätzen, ob sich das Lesen lohnt – und mir zu verstehen, welche Inhalte dir weiterhelfen.

👉 Bewertung abgeben

Lust auf noch mehr Wissen?

In meinem Blog findest du viele Artikel zu Java, Softwarearchitektur und Performance von grundlegenden Konzepten bis hin zu fortgeschrittenen Patterns.

Wenn du tiefer einsteigen willst, schau dir meine Trainings an: praxisnah, verständlich und direkt auf euren Projektalltag übertragbar. Statt Theorie vermittle ich Prinzipien, die euch helfen, Code langfristig besser, wartbarer und performanter zu schreiben.

Zu den Java-Schulungen

Werde ein:e bessere:r Java-Entwickler:in

Mit meinem kostenlosen Newsletter bleibst du vorn. Modernes Java: neue Versionen & Features, Performance und JVM-Insights – 1x im Monat.

Suche