How to Move or Rename a File in Java
Java 13 min read
A move within one filesystem is atomic and nearly free; across filesystems it is a copy and a delete, and ATOMIC_MOVE throws rather than degrading silently. Renaming is the same call.
Moving and renaming are the same operation in Java, and the distinction that matters is not between them but between a move within a filesystem and one across filesystems. The first rewrites a directory entry and is atomic. The second copies every byte and deletes the original, and can fail halfway.
Written against Java 17.
The call
Path source = Path.of("/var/data/report.csv");
Path target = Path.of("/var/archive/report-2026-08.csv");
Files.move(source, target); // fails if the target exists
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
Renaming is the same thing with the target in the same directory:
Files.move(source, source.resolveSibling("report-final.csv"));
resolveSibling replaces the last element of the path, which is exactly what a rename is and avoids
reconstructing the parent by hand.
As with copy, the target is the full destination path. Passing a directory creates a file with that directory’s name rather than moving into it:
Files.move(source, Path.of("/var/archive")); // wrong
Files.move(source, Path.of("/var/archive").resolve(source.getFileName())); // right
Atomic, and when it is not
Files.move(source, target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE);
Within one filesystem, a move is a metadata operation, the directory entry is rewritten and no data is touched. It is instant regardless of file size, and it is atomic: any observer sees the file either at the old path or at the new one, never at neither and never partially written.
Across filesystems there is no such operation. The JVM falls back to copying the bytes and deleting the source, which takes time proportional to the size and has a window in which the file exists in both places, or in a partial state at the target.
ATOMIC_MOVE makes that explicit: it throws AtomicMoveNotSupportedException rather than quietly
degrading. That is the behaviour to want, because code relying on atomicity should fail loudly when
it cannot have it.
The consequence for the publish-atomically pattern is that the temporary file must be created in the target’s own directory:
Path temp = target.resolveSibling(target.getFileName() + ".tmp");
Files.writeString(temp, content, StandardCharsets.UTF_8);
Files.move(temp, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
A temp file in /tmp is usually on a different filesystem, in a container it almost always is, so
the move degrades and the guarantee is lost.
Which filesystem is a path on?
FileStore sourceStore = Files.getFileStore(source);
FileStore targetStore = Files.getFileStore(target.getParent());
boolean sameFilesystem = sourceStore.equals(targetStore);
Worth checking when the destination is configurable. A deployment that mounts /var/archive as a
network volume turns an instant atomic move into a slow copy, and nothing in the code changes.
Moving directories
Files.move(sourceDir, targetDir);
Unlike copy, this does move the whole tree, within a filesystem, because it is one directory entry.
Across filesystems it may throw IOException rather than recursing, and the specification permits
either. So a directory move that works in development and fails in production is usually a mount
boundary, and the portable approach is to copy the tree and then delete it:
copyDirectory(source, target); // the recursive copy
deleteRecursively(source); // the reverse-ordered delete
That is not atomic and cannot be made so. There is a window in which the tree exists twice, and a failure partway leaves both a partial copy and the intact original. Where it matters, arrange for both paths to be on one filesystem; where it does not, delete the source only after the copy has been verified.
REPLACE_EXISTING and directories
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
Replacing an existing file works. Replacing an existing non-empty directory throws
DirectoryNotEmptyException, the option replaces an empty directory only.
That asymmetry catches people writing a “swap in the new version” routine over a directory. The sequence that works is a three-way move: move the current one aside, move the new one into place, delete the old one. Each step is atomic; the sequence is not, so there is a moment with no directory at the target path.
What a move does not preserve
A rename within a filesystem preserves everything, because nothing is rewritten, inode, timestamps, permissions, hard links.
A cross-filesystem move preserves much less. It is a copy, so the target gets a new inode, hard links to the source are broken, and attributes come across only as far as the platform manages. Extended attributes and ACLs are commonly lost.
If the destination’s attributes matter, do the copy explicitly with COPY_ATTRIBUTES and delete
afterwards, rather than leaving it to the fallback.
Concurrency and the overwrite window
Files.move with REPLACE_EXISTING is atomic within a filesystem, so two processes racing to move
different files onto the same target produce one winner and no corruption. That is a genuinely useful
property: a rename is the standard way to implement a lock-free publish.
Without REPLACE_EXISTING the move fails if the target exists, and that failure is also atomic. It
is the basis of a simple mutual-exclusion primitive:
try {
Files.move(claim, lock); // exactly one process succeeds
// this process holds the lock
} catch (FileAlreadyExistsException e) {
// another process got there first
}
Do not build a real distributed lock on this. It has no expiry and no owner, but for a single-host “only one worker should process this file” it is correct and needs no dependencies.
The legacy renameTo, and why not
File file = new File("/var/data/report.csv");
boolean ok = file.renameTo(new File("/var/archive/report.csv"));
File.renameTo returns false for every kind of failure (missing source, existing target, no
permission, cross-filesystem, target directory absent) with no way to tell them apart. Its Javadoc
says outright that its behaviour is platform-dependent, which is unusually candid.
Two specific behaviours differ by platform and neither is documented as a guarantee. On Unix it
overwrites an existing target; on Windows it fails. And it does not move across filesystems at all on
most platforms, where Files.move falls back to copy-and-delete.
There is no reason to use it in new code. path.toFile() and file.toPath() convert at the boundary
of any API that still demands the old type.
Handling the failure modes
try {
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
} catch (NoSuchFileException e) {
// the source is gone, or the target's parent does not exist
} catch (FileAlreadyExistsException e) {
// target exists and REPLACE_EXISTING was not passed
} catch (DirectoryNotEmptyException e) {
// replacing a populated directory
} catch (AtomicMoveNotSupportedException e) {
// different filesystems — retry without the option, or fail
} catch (AccessDeniedException e) {
// permissions, or an open handle on Windows
}
NoSuchFileException covering both a missing source and a missing target parent is worth knowing,
because the two need different fixes and the message names only one path. Files.createDirectories(target.getParent())
before the move removes the second case entirely, and it is idempotent.
Watching for the move
A file appearing at its final path via a move is what makes a directory watcher reliable:
WatchService watcher = FileSystems.getDefault().newWatchService();
dir.register(watcher, StandardWatchEventKinds.ENTRY_CREATE);
A writer that creates the file in place fires ENTRY_CREATE when the file is empty, and the watcher
reads a partial file. A writer that writes elsewhere and moves it in fires the event when the file is
already complete.
That is the same reasoning as the publish pattern, from the reader’s side, and it is why “write to
.tmp, then rename” is the convention in every pipeline that hands files between processes.
Related: copying, deleting and creating a file. More in the Java guides.
Frequently asked questions
How do I rename a file in Java?
Files.move(source, source.resolveSibling("newname")). Renaming
and moving are the same call; resolveSibling replaces the final path element.
Can I pass a directory as the move target?
No, the target is the full destination path. Passing a directory creates a file with that name instead of moving into it.
Is Files.move atomic?
Within one filesystem, yes, it rewrites a directory entry. Across filesystems it is a copy plus a delete and is not atomic.
What does ATOMIC_MOVE do?
It requires atomicity and throws AtomicMoveNotSupportedException when
it cannot be provided, rather than silently falling back to copy-and-delete.
Why does my atomic move throw?
Source and target are on different filesystems. Create the temporary file in the target’s own directory so both are on the same one.
Does moving a directory move its contents?
Within a filesystem, yes. It is one directory entry. Across filesystems it may throw, so copy the tree and delete it instead.
Why can’t I replace a non-empty directory?
REPLACE_EXISTING replaces a file or an empty
directory only. Swapping a populated directory needs a three-step move-aside sequence.
Does a move preserve timestamps and permissions?
Within a filesystem, everything. Nothing is rewritten. Across filesystems it is a copy, so extended attributes and hard links are commonly lost.
How do I check whether two paths are on the same filesystem?
Compare
Files.getFileStore(a) with Files.getFileStore(b). Worth doing when the destination is
configurable.
Why is write-then-rename the standard publishing pattern?
A reader sees either the old file or the complete new one, never a partial write, and a directory watcher fires only once the file is whole.