JAVA / FILES, INPUT/OUTPUT AND SERIALIZATION
Creating, copying, moving and deleting files
Create, copy, move and delete files with java.nio.file.Files, choosing the right copy options and replacing a file in place without exposing a partial write.
What you will learn
- Create files and missing parents with Files.createFile and Files.createDirectories
- Overwrite deliberately via StandardCopyOption.REPLACE_EXISTING, never by default
- Choose delete or deleteIfExists based on whether a missing file is an error
- Replace a file in place with a same-directory temp file plus an atomic move
Understanding Creating, copying, moving and deleting files
A Path is only a name; nothing on disk changes when you build one. The mutating calls on Files are commands handed to the operating system: createFile makes one directory entry, copy reads bytes from one entry and writes them into another, move usually rewrites a single directory entry, and delete removes one. Because each call is a single filesystem operation, it either succeeds or throws, and the exception type names the precondition that failed: NoSuchFileException for a missing source or a missing parent directory, FileAlreadyExistsException for an occupied target, DirectoryNotEmptyException for a directory that still has entries.
The default behaviour is deliberately unhelpful about overwriting: Files.copy and Files.move refuse an existing target instead of destroying it, and you opt in with StandardCopyOption.REPLACE_EXISTING. Two other options carry real meaning: COPY_ATTRIBUTES asks for metadata such as the last-modified time to travel with the bytes, and ATOMIC_MOVE asks for the move to be one indivisible rename. Options are requests about semantics, not flags that make impossible things possible, which is exactly why ATOMIC_MOVE can fail with AtomicMoveNotSupportedException.
Whether move is cheap depends on geography. Inside one filesystem it is a rename: the bytes stay where they are and only the directory entry changes, so it is fast and an observer sees either the old name or the new one. Across filesystems the JDK falls back to copying the bytes and then deleting the source, which is neither fast nor atomic, so the safe way to update a file in place is to write a temporary file in the same directory as the target and move it on top with REPLACE_EXISTING. This is also why Files throws instead of returning booleans the way the legacy File.delete did: a bare 'false' cannot tell you whether the parent was missing, the directory was non-empty, or you lacked permission.
placeholder
import java.io.IOException;
import java.nio.file.*;
public class FileOps {
public static void main(String[] args) throws IOException {
Path dir = Files.createTempDirectory("fileops");
Path source = dir.resolve("notes.txt");
Files.writeString(source, "first line\n");
System.out.println("created: " + Files.exists(source) + ", bytes=" + Files.size(source));
Path backup = dir.resolve("notes-backup.txt");
Files.copy(source, backup);
System.out.println("backup holds: " + Files.readString(backup).strip());
try {
Files.copy(source, backup);
} catch (FileAlreadyExistsException e) {
System.out.println("refused, target was: " + Path.of(e.getFile()).getFileName());
}
Files.copy(source, backup, StandardCopyOption.REPLACE_EXISTING);
Path archive = dir.resolve("archive.txt");
Files.move(backup, archive);
System.out.println("after move -> backup:" + Files.exists(backup)
+ " archive:" + Files.exists(archive));
System.out.println("deleteIfExists archive: " + Files.deleteIfExists(archive));
System.out.println("deleteIfExists again: " + Files.deleteIfExists(archive));
try {
Files.delete(archive);
} catch (NoSuchFileException e) {
System.out.println("delete on a missing path throws NoSuchFileException");
}
Files.delete(source);
Files.delete(dir);
System.out.println("temp dir gone: " + Files.notExists(dir));
}
}
Files.copy, move and delete are single filesystem commands that refuse to destroy data unless you pass an option saying so, and they report failure by throwing a specific exception rather than returning false.
Worked examples
Replacing a file without a partial-write window
Updates a config file so a concurrent reader sees either the complete old content or the complete new content.
import java.io.IOException;
import java.nio.file.*;
public class SafeReplace {
public static void main(String[] args) throws IOException {
Path dir = Files.createTempDirectory("safe");
Path config = dir.resolve("config.txt");
Files.writeString(config, "mode=old\n");
System.out.println("before: " + Files.readString(config).strip());
Path tmp = Files.createTempFile(dir, "config", ".tmp");
Files.writeString(tmp, "mode=new\n");
try {
Files.move(tmp, config, StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(tmp, config, StandardCopyOption.REPLACE_EXISTING);
}
System.out.println("after: " + Files.readString(config).strip());
System.out.println("temp file left behind: " + Files.exists(tmp));
Files.delete(config);
Files.delete(dir);
}
}
Example explained
Line 1Files.createTempFile(dir, ...) puts the scratch file in the target's own directory, which is what lets the later move be a rename inside one filesystem.
Line 2Files.move with REPLACE_EXISTING swaps the directory entry rather than rewriting config.txt byte by byte, so no reader can observe a half-written file.
Line 3ATOMIC_MOVE is a request, so the catch block degrades to a plain replace instead of crashing where the filesystem cannot honour it.
Line 4The temp path is gone afterwards because move removes the source; copy would have left both files.
What copy and delete do with directories
Shows that copying a directory does not copy its contents and that delete requires the directory to be empty first.
import java.io.IOException;
import java.nio.file.*;
public class CopyDirSemantics {
public static void main(String[] args) throws IOException {
Path root = Files.createTempDirectory("demo");
Path src = Files.createDirectory(root.resolve("src"));
Files.writeString(src.resolve("a.txt"), "a\n");
Path dst = root.resolve("dst");
Files.copy(src, dst);
System.out.println("dst is a directory: " + Files.isDirectory(dst));
System.out.println("dst contains a.txt: " + Files.exists(dst.resolve("a.txt")));
try {
Files.delete(src);
} catch (DirectoryNotEmptyException e) {
System.out.println("delete src refused: DirectoryNotEmptyException");
}
Files.delete(src.resolve("a.txt"));
Files.delete(src);
Files.delete(dst);
Files.delete(root);
System.out.println("everything removed: " + Files.notExists(root));
}
}
Example explained
Line 1Files.copy(src, dst) on a directory creates dst as an empty directory, so the check for a.txt inside dst prints false.
Line 2Files.delete(src) throws DirectoryNotEmptyException, a specific subclass of FileSystemException, so the reason is not buried in a generic IOException.
Line 3Removing a.txt first and then src works because delete only unlinks a directory once no entries point into it.
Line 4Files.notExists is not the negation of exists: it returns false when the status cannot be determined, although the preceding deletes make the answer definite here.
Carrying timestamps across with COPY_ATTRIBUTES
Compares a plain copy against a copy that asks for the source metadata to be preserved.
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
public class CopyAttributes {
public static void main(String[] args) throws IOException {
Path dir = Files.createTempDirectory("attrs");
Path src = dir.resolve("report.txt");
Files.writeString(src, "data\n");
FileTime stamp = FileTime.from(Instant.parse("2020-01-01T00:00:00Z"));
Files.setLastModifiedTime(src, stamp);
Path plain = Files.copy(src, dir.resolve("plain.txt"));
Path kept = Files.copy(src, dir.resolve("kept.txt"),
StandardCopyOption.COPY_ATTRIBUTES);
System.out.println("plain keeps the timestamp: "
+ Files.getLastModifiedTime(plain).equals(stamp));
System.out.println("kept keeps the timestamp: "
+ Files.getLastModifiedTime(kept).equals(stamp));
Files.delete(src);
Files.delete(plain);
Files.delete(kept);
Files.delete(dir);
}
}
Example explained
Line 1Files.setLastModifiedTime pins the source timestamp to a fixed instant so the comparison does not depend on when you run the program.
Line 2The plain copy gets the current time instead, because copy creates a brand new file entry and only transfers the bytes.
Line 3COPY_ATTRIBUTES asks the provider to carry the source attributes over, with the last-modified time as the minimum it must try.
Line 4Files.copy returns the target Path, which is why plain and kept can be assigned straight from the calls.
Important notes
REPLACE_EXISTING does not rescue you from DirectoryNotEmptyException: it can replace a regular file or an empty directory, never a directory that still has entries.
On Windows an open file cannot be deleted or moved, so close every stream, reader or channel first; on Linux and macOS the same code passes, which makes it an easy portability bug to miss.
Common mistakes
Assuming copy and move overwrite: without REPLACE_EXISTING the second run dies with FileAlreadyExistsException, so the code appears to work exactly once.
Calling Files.delete on a path that may never have been created in a cleanup or finally block: the NoSuchFileException replaces the original failure you were trying to report, whereas deleteIfExists just returns false.
Building the replacement file in the system temp directory and then moving it onto the target: that usually crosses filesystems, so ATOMIC_MOVE throws AtomicMoveNotSupportedException and a plain move silently becomes copy-then-delete, which can be observed half done.
Try it yourself
Change, predict, then run
In a temp directory, create data.txt, copy it to data-backup.txt, then run the identical copy again and print the simple name of the exception class you catch. Finish by moving the backup to data.old and calling Files.deleteIfExists on it twice, printing both return values.
Open the Java workspaceCheck your understanding
You must update a config file so that any process reading it sees either the complete old version or the complete new version, never a mixture. Which approach guarantees that?
- Write the new content to a temp file in the config's own directory, then Files.move it onto the config with REPLACE_EXISTING and ATOMIC_MOVE
- Files.delete the config first, then write the new content to the same path
- Write straight to the config path with StandardOpenOption.TRUNCATE_EXISTING so the old content goes away in one call
- Prepare the new file under the system temp directory and Files.copy it onto the config with REPLACE_EXISTING
Show answer
A move within one directory replaces the directory entry in a single step, so a reader resolves the name to either the old file or the new one. Option 4 is tempting because it also prepares the content elsewhere first, but copy truncates the target and writes bytes into it, leaving a window where a reader sees a partial file; the system temp directory is usually a different filesystem too, so a move from there would not be atomic either. Options 2 and 3 both destroy the old content before the new content exists.