Skip to content

How to Copy, Move, and Delete Files and Directories (Java Files Tutorial)

Two light card-stock trays against oxblood red, a white index card in the left one, an identical card in the right one whose upper half is still forming from particles of light; a beam of light connects the two

Previous articles in this series have covered reading files with Java, writing files, and constructing directory and file paths with the File and Path classes. This fourth part describes the most important directory and file operations. It answers the following questions:

  • How to list all files in a directory?
  • How to search for files matching specific criteria within a directory tree?
  • How to create a directory?
  • How to find the current directory?
  • How to find the user’s home directory?
  • How to find the temporary directory, and how to create a temporary file?
  • How do I move files with Java?
  • How do I rename a file?
  • How do I copy a file?
  • How do I overwrite an existing target file?
  • How do I copy a directory with all its contents?
  • How to delete a file?
  • How to delete a directory with all its contents?
  • How to create a symbolic link with Java?

The article answers all questions using the NIO.2 File API, introduced in Java 7 with JSR 203.

Directory operations

For the following directory operations, you need a Path object representing the directory. You can construct this object using the static method Path.of().

A comprehensive tutorial on constructing directory paths with Path, Paths, and File can be found in the third part of this series.

How to list directory contents with Files.list()

The easiest way to list the complete contents of a directory is the Files.list() method. It returns a Stream of Path objects, which we simply write to System.out in the following example:

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

The stream keeps the directory open until you close it. That is why it belongs in a try-with-resources block: only close() releases the file handle the stream holds in the operating system. The same applies to the streams of Files.walk() and Files.find() in the following sections.

How to search a directory recursively with Files.list()

Let’s move on to a more complex case. In the following example, we want to output all regular files located in the home directory or a subdirectory of any depth thereof and whose name starts with “settings”.

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

You can use the methods Files.isRegularFile() and Files.isDirectory() to check if a file is a regular file or directory. Another file type is the symbolic link – you can recognize it with Files.isSymbolicLink(). It is also possible that all three methods return false, in which case the file is of type “other”, e.g., a named pipe, a device file, or a socket.

In the example above, we have to catch IOException inside the lambda and wrap it with an UncheckedIOException because the forEach consumer of the stream must not throw a checked exception.

If the program is not allowed to read a subdirectory, Files.list() throws an AccessDeniedException for that directory. The example catches it, prints “Access denied”, and continues with the next directory.

How to search a directory recursively with Files.walk()

You can write the previous example much shorter and more elegant – using 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);
      }
    });
  }
}

However, this variant has the disadvantage that an AccessDeniedException cannot be caught individually as before. The stream wraps it in an UncheckedIOException and throws that from forEach() – which ends the entire search. If this is acceptable for your application, this way is more elegant than the previous one.

How to search a directory recursively with Files.walkFileTree()

Another variant is Files.walkFileTree(). This method implements the visitor pattern. It sends each file within the directory structure to a FileVisitor, which you pass to the method. In the following example, we use the SimpleFileVisitor class, which implements all methods of FileVisitor. We only override the visitFile() method:

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 has four methods, and walkFileTree() calls them in a fixed order: for each directory, first preVisitDirectory(), then visitFile() for each entry – for a subdirectory, the same sequence again instead – and finally postVisitDirectory(). If walkFileTree() cannot read an entry, it calls visitFileFailed().

The following diagram shows the calls for a directory project containing the file README.md and the subdirectory src, which holds Main.java:

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

In the diagram, calls for directories have a blue border, calls for files a gray one. The order of the entries within a directory – here README.md before src – is up to the file system. The bracket on the right marks the calls that are skipped when preVisitDirectory() returns SKIP_SUBTREE for src; the list after the diagram explains the return values.

Call order of Files.walkFileTree() for the directory project with README.md and the subdirectory src containing Main.java: 1. preVisitDirectory(project), 2. visitFile(project/README.md), 3. preVisitDirectory(project/src), 4. visitFile(project/src/Main.java), 5. postVisitDirectory(project/src), 6. postVisitDirectory(project); if step 3 returns SKIP_SUBTREE, steps 4 and 5 are skipped
walkFileTree() calls preVisitDirectory() for each directory first, then its entries, and postVisitDirectory() last

The return value of visitFile() in the example above, FileVisitResult.CONTINUE, indicates that walkFileTree() should continue to traverse the directory tree. Other return values would be:

  • TERMINATE – terminates the walkFileTree() method.
  • SKIP_SIBLINGS – skips all other entries of the current directory.
  • SKIP_SUBTREE – skips the contents of a directory and the call to the postVisitDirectory() method. This return value is only meaningful in FileVisitor.preVisitDirectory(); returned from visitFile(), it has the same effect as CONTINUE.

The walkFileTree() variant, too, terminates processing in the case of an AccessDeniedException. However, there is a way to prevent this. To do so, you also need to override the method FileVisitor.visitFileFailed():

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

How to search a directory with Files.find()

An alternative approach is the Files.find() method. It expects a “matcher” as the third parameter: this is a function that has a Path and BasicFileAttributes as input parameters and returns a boolean indicating whether the corresponding file should be included in the result or not.

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

Here, too, an AccessDeniedException ends the entire search prematurely – as with Files.walk(), as an UncheckedIOException thrown from forEach(). Files.find() offers no way to avoid this.

The four variants compared

The following table compares the four ways of searching a directory tree: whether you can catch an AccessDeniedException for a single directory and continue searching, and whether you have to close a stream.

VariantCatch AccessDeniedException
individually
Must close
a stream
Files.list() + own recursion✓✓
Files.walk()✗✓
Files.walkFileTree()✓
(visitFileFailed())
✗
(no stream)
Files.find()✗✓

I recommend Files.walk() – or Files.find() if you filter by file attributes – as long as all directories are readable. If you have to expect locked directories, use Files.walkFileTree() and override visitFileFailed().

How to create a directory with Files.createDirectory() and Files.createDirectories()

You create a single directory with Files.createDirectory(). The following example creates the directory happycoders in the home directory:

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

If the directory already exists, createDirectory() throws a FileAlreadyExistsException. If the parent directory is missing, it throws a NoSuchFileException.

Files.createDirectories(), on the other hand, also creates all missing parent directories and does not throw an exception if the directory already exists. The following example creates happycoders/2026/10 in the home directory – including happycoders and 2026 if they do not exist yet:

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

I recommend createDirectories() – unless an existing directory is an error. Then use createDirectory(), because with this method, the check and the creation are a single atomic operation: if two processes try to create the same working directory at the same time, exactly one of them succeeds, and the other gets the FileAlreadyExistsException.

Accessing specific directories

Accessing the current directory

The current directory can be found via the system property “user.dir”:

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

Accessing the user’s home directory

You can find the home directory of the current user via the system property “user.home”:

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

Accessing the temporary directory

And you can find the temporary directory via the system property “java.io.tmpdir”:

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

If you want to create a temporary file in the temporary directory, you do not need to access the temporary directory first. There is a shortcut for this. You will find it at the beginning of the following chapter, “File operations”.

File operations

How to create a temporary file

After the last chapter concluded with accessing the temporary directory, this one starts with a shortcut to creating temporary files:

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

The two parameters are a prefix and a suffix. Between them, createTempFile() inserts a random number that comes from a SecureRandom. When I run the method repeatedly on my Windows system and print the variable tempFile to the console, I get the following output:

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

On Linux it looks like this:

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

It is essential to know that createTempFile() does not only create the respective Path object but actually creates an empty file. On Linux and macOS, only the account that created the file may read and write it (permissions rw-------).

How to move a file in Java

To move a file, use the method Files.move(). The following example creates a temporary file and moves it to the home directory of the logged-on user:

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

The second parameter of the move() method must represent the target file, not the target directory! If you invoked Files.move(tempFile, targetDir) here, you would get a FileAlreadyExistsException. Therefore, in the example, we use the resolve() method to concatenate the targetDir with the name of the file to be moved.

How to move a directory including all subdirectories

You can move a directory just like a file. In the following example, we create two temporary directories and one file in the first directory. We then move the first directory into the second:

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

Two special cases:

  • Within one file system, move() only renames the directory; its entries stay where they are.
  • If the target is on a different file system, move() would have to copy each entry individually. It does not do that – for a non-empty directory, it throws an IOException instead, a DirectoryNotEmptyException on Linux and macOS.

So to bring a directory tree to another file system, you need two steps: first copy it, then delete it.

How to rename a file with Java

After all, renaming a file (or a directory) is a special case of moving, with the destination directory being the same as the source directory and only the file name changing. In the following example, we rename a temporary file to “happycoders.tmp”:

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

Invoking tempFile.resolveSibling("happycoders.tmp") is a shortcut for tempFile.getParent().resolve("happycoders.tmp"): the directory is extracted from the source file and concatenated with the new file name.

How to copy a file in Java

Copying a file is similar to renaming it. The following example copies a temporary file to the home directory:

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

This method has a significant advantage over hand-written implementations with FileInputStream and FileOutputStream, as they were necessary before Java 7 and the NIO.2 File API: Files.copy() delegates the call to operating system-specific – and thus optimized – implementations.

On macOS, since Java 20, Files.copy() even creates a clone if you specify the COPY_ATTRIBUTES option from the next section: using the clonefile() system call, the original and the copy share their data blocks on APFS until one of the two files is modified. The copy then initially takes up no additional disk space (JDK-8293122).

How to overwrite an existing target file: StandardCopyOption

If the target file already exists, Files.copy() and Files.move() throw a FileAlreadyExistsException. With additional parameters of type CopyOption, you change this and other behavior.

The following example creates two temporary files and copies the first one over the second – with StandardCopyOption.REPLACE_EXISTING, because without this option, copy() would throw a FileAlreadyExistsException here:

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

The StandardCopyOption enum has three constants:

  • REPLACE_EXISTING – overwrites an existing target file. However, a non-empty target directory cannot be replaced this way.
  • COPY_ATTRIBUTES – only for copy(): copies the file attributes as well, at least the last-modified time. Which other attributes are copied depends on the operating system and the file system. Without this option, copy() does not guarantee that any attributes are copied.
  • ATOMIC_MOVE – only for move(): moves the file as a single atomic file system operation. Other processes therefore see the file either at the old or at the new location, never in an intermediate state. If that is not possible, for example because the target is on a different file system, move() throws an AtomicMoveNotSupportedException.

If you pass the COPY_ATTRIBUTES option to move() or the ATOMIC_MOVE option to copy(), the method throws an UnsupportedOperationException.

I recommend ATOMIC_MOVE wherever another process reads the target file – for example, when you move a completely written file into a directory that another program watches.

How to copy a directory with all its contents

If you call Files.copy() on a directory, it only creates an empty directory at the target – it does not copy the entries. You copy an entire directory tree with Files.walkFileTree(): in preVisitDirectory(), you create each directory at the target; in visitFile(), you copy each file.

The following method copyDirectory() copies the directory sourceDir with all its subdirectories and files to 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;
    }
  });
}

relativize() builds the target path: if sourceDir is the directory project from the diagram in the section How to search a directory recursively with Files.walkFileTree(), sourceDir.relativize() returns the relative path src/Main.java for project/src/Main.java. targetDir.resolve() appends it to the target directory.

The LinkOption.NOFOLLOW_LINKS option copies a symbolic link as a link. Without it, copy() copies the file the link points to.

If a target file already exists, copyDirectory() aborts with a FileAlreadyExistsException. With StandardCopyOption.REPLACE_EXISTING as an additional argument to Files.copy(), the method overwrites it instead.

How to delete a file in Java

You can delete a file (or a directory) with Files.delete():

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

If the file does not exist, Files.delete() throws a NoSuchFileException. If you do not want an exception in that case, use Files.deleteIfExists(). The method returns true if it deleted the file and false if the file did not exist:

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

The directory on which you invoke Files.delete() must be empty. Otherwise, the method will throw a DirectoryNotEmptyException. You can try this with the following code:

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

First, a temporary directory is created, then a temporary file in it. Then an attempt is made to delete the (non-empty) directory.

How to delete a directory with all its contents

You delete a directory with contents from the bottom up: first the files, then the empty directories. Files.walkFileTree() provides exactly this order because it calls postVisitDirectory() only after all entries of a directory have been visited – in the diagram in the section How to search a directory recursively with Files.walkFileTree(), step 5 for src and step 6 for project.

The following method deleteDirectory() deletes each file in visitFile() and each directory in postVisitDirectory() as soon as it is empty:

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

If the exc parameter of postVisitDirectory() is not null, walkFileTree() could not traverse the directory completely. The method then rethrows the exception because the exception names the cause – Files.delete() would only fail with a DirectoryNotEmptyException.

deleteDirectory() deletes symbolic links as links; the file a link points to is kept. That is because walkFileTree() does not follow symbolic links by default.

It is shorter with Files.walk(): you sort the paths in reverse order and then delete them one by one:

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

The reverse order puts each entry before its directory: the path of a directory is a prefix of the paths of its entries and therefore comes before them in ascending order – and after them in descending order.

However, sorted() collects all paths of the directory tree in memory before the first file is deleted – one Path object per entry. For large directory trees, I therefore recommend the walkFileTree() variant.

You can create a symbolic link with the method Files.createSymbolicLink(). Note that you pass target and source in the reverse order compared to all previous methods – first the link path, then the path of the file to be linked. The following example creates a temporary file and then sets a symbolic link to the created file from the home directory.

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

This example works on Linux and macOS without restrictions.

Summary and outlook

This fourth article in the series about files in Java introduced the most important directory and file operations. For entire directory trees – searching, copying, deleting – Files.walkFileTree() is the tool that also handles locked directories and large trees.

In the next part, I will show you how to write and read structured data with DataOutputStream and DataInputStream.

We then move on to the following advanced topics:

  • NIO channels and buffers introduced in Java 1.4, to speed up working with large files
  • Memory-mapped I/O for convenient and blazing-fast file access without streams
  • File locking, to access the same files in parallel – i.e., from several threads or processes – without conflict

Did you take something away from this article? With a review on my ProvenExpert profile, you help other developers assess whether these articles are worth reading – and you help me understand which content is most useful to you.

👉 Leave a review

Want Even More Knowledge?

My blog features many articles on Java, software architecture, and performance — from foundational concepts to advanced patterns.

If you want to go deeper, check out my trainings: hands-on, easy to understand, and directly applicable to your day-to-day project work. Instead of theory, I teach principles that help you write code that is better, more maintainable, and more performant in the long run.

Explore the Java Trainings

Become a Better Java Developer

My free newsletter keeps you ahead. Modern Java: new versions & features, performance, and JVM insights – once a month.

Search