File Handling in Java with Path and Files
Path names a location and Files performs the operations. Together they replaced the older File class for almost every purpose.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
Path and Files
java.nio.file.Path represents a location in a file system. It is only a name: creating one touches nothing on disk. java.nio.file.Files is the utility class that actually reads, writes, copies and deletes.
Path path = Path.of("data", "notes", "export.txt");
Path absolute = path.toAbsolutePath();
Path home = Path.of(System.getProperty("user.home"));
System.out.println(path.getFileName()); // export.txt
System.out.println(path.getParent()); // data/notes
System.out.println(path.getNameCount()); // 3
System.out.println(path.resolve("v2.txt")); // data/notes/export.txt/v2.txt
System.out.println(path.resolveSibling("backup.txt"));
System.out.println(path.normalize());Path.of builds a platform correct path from its parts, so no separator is ever hard coded. That alone removes a large class of portability bugs.Reading a whole file
Path path = Path.of("notes.txt");
String content = Files.readString(path); // Java 11, UTF-8
List<String> lines = Files.readAllLines(path);
byte[] bytes = Files.readAllBytes(path);These load everything into memory, which is fine for configuration and small documents and wrong for large files.
Reading line by line
try (BufferedReader reader = Files.newBufferedReader(path)) {
String line;
while ((line = reader.readLine()) != null) {
process(line);
}
}
try (Stream<String> lines = Files.lines(path)) {
long blanks = lines.filter(String::isBlank).count();
}Files.lines is lazy and holds an open file handle, so it must be closed. Using it inside try with resources is not optional.
Writing
Files.writeString(path, "First line" + System.lineSeparator());
Files.write(path, List.of("a", "b", "c"));
Files.write(path, bytes);
Files.writeString(path, "appended
", StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
try (BufferedWriter writer = Files.newBufferedWriter(path)) {
for (Note note : notes) {
writer.write(note.title());
writer.newLine();
}
}| Open option | Effect |
|---|---|
CREATE | Create the file if it does not exist |
CREATE_NEW | Create, and fail if it already exists |
APPEND | Write at the end |
TRUNCATE_EXISTING | Empty the file first, the default for writing |
DELETE_ON_CLOSE | Remove the file when the stream closes |
Checking and inspecting
System.out.println(Files.exists(path));
System.out.println(Files.notExists(path)); // not the same as !exists
System.out.println(Files.isDirectory(path));
System.out.println(Files.isReadable(path));
System.out.println(Files.size(path));
System.out.println(Files.getLastModifiedTime(path));
System.out.println(Files.probeContentType(path));exists and notExists can both be false when the status cannot be determined, for example because of permissions.
Creating, copying, moving, deleting
Files.createDirectories(Path.of("data", "exports")); // creates missing parents
Files.createFile(Path.of("data", "exports", "run.log"));
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
Files.delete(path); // throws if missing
Files.deleteIfExists(path); // returns false if missingWalking a directory tree
try (Stream<Path> entries = Files.list(directory)) { // one level only
entries.filter(Files::isRegularFile).forEach(System.out::println);
}
try (Stream<Path> tree = Files.walk(directory)) { // recursive
long javaFiles = tree
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.count();
}
try (Stream<Path> found = Files.find(directory, 5,
(p, attrs) -> attrs.isRegularFile() && attrs.size() > 1_000_000)) {
found.forEach(System.out::println);
}All three return streams that hold operating system resources and must be closed.
Character encoding
String content = Files.readString(path); // UTF-8
String legacy = Files.readString(path, StandardCharsets.ISO_8859_1);
Files.writeString(path, text, StandardCharsets.UTF_8);The java.nio.file methods default to UTF-8. Older java.io classes used the platform default charset, which is why the same code could produce different results on different machines. Since Java 18 UTF-8 is the platform default for those too, but stating the charset explicitly is still the safer habit.
A worked example
public static Map<String, Integer> countWords(Path path) throws IOException {
Map<String, Integer> counts = new TreeMap<>();
try (Stream<String> lines = Files.lines(path)) {
lines.flatMap(line -> Arrays.stream(line.toLowerCase().split("[^a-z]+")))
.filter(word -> !word.isBlank())
.forEach(word -> counts.merge(word, 1, Integer::sum));
}
return counts;
}Writing safely
public static void writeAtomically(Path target, String content) throws IOException {
Path temporary = target.resolveSibling(target.getFileName() + ".tmp");
Files.writeString(temporary, content);
Files.move(temporary, target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE);
}Writing to a temporary file and then moving it means a reader never sees a half written file. This pattern is worth knowing for configuration and export files.
The old File class
File old = new File("notes.txt");
Path modern = old.toPath(); // convert when working with legacy APIs
File back = modern.toFile();java.io.File reports failures by returning false rather than explaining what went wrong. Files throws a specific exception with a message. Use Path and Files in new code and convert only at the boundary of an older library.
Common mistakes
- Not closing a
Files.lines,walkorliststream. - Reading a large file entirely into memory.
- Hard coding path separators instead of using
Path.oforresolve. - Ignoring the charset and getting different results on different systems.
- Assuming
Files.writeappends. It truncates unlessAPPENDis given. - Using
File.delete()and ignoring the boolean result.
Best practices
- Use
PathandFilesrather thanFile. - Wrap every stream returning method in try with resources.
- State the charset, or rely on the UTF-8 default of the
Filesmethods. - Stream large files rather than loading them.
- Use
createDirectoriesbefore writing into a new folder. - Write to a temporary file and move it when the result must never be partial.
Practice
- Count the lines in a file without loading it into memory.
- Why must a
Files.linesstream be closed when aListfromreadAllLinesneed not be? - Copy every file with a given extension from one folder into another.
- Explain the difference between
Files.writewith and withoutStandardOpenOption.APPEND. - Implement an atomic write and describe the failure it prevents.
Conclusion
Path names a location and Files does the work. Prefer them to File, close every stream that touches the file system, be explicit about encoding, and stream anything large.