Files, I/O and JSON
java.nio.file Path and Files, buffered reading and writing, charsets, and serialising records to JSON safely.
Path and Files
Path dir = Path.of("data");
Path file = dir.resolve("orders.csv").normalize();
Files.exists(file);
Files.createDirectories(dir);
Files.size(file);
Files.isRegularFile(file);
// never resolve untrusted input without checking the result stays inside dir
Path candidate = dir.resolve(userName).normalize();
if (!candidate.startsWith(dir.toAbsolutePath().normalize())) {
throw new IllegalArgumentException("path traversal attempt");
}| Task | Call | Note |
|---|---|---|
| Join paths | dir.resolve("a") | Never concatenate strings with a separator |
| Read all text | Files.readString(path) | Fine up to a few megabytes |
| Write text | Files.writeString(path, text) | Creates or truncates the file |
| Read lines | Files.readAllLines(path) | Whole file in memory |
| Stream lines | Files.lines(path) | Lazy — must be closed |
| Copy or move | Files.copy, Files.move | Pass a copy option to replace |
| List a directory | Files.list(dir) | Returns a stream — must be closed |
| Walk a tree | Files.walk(dir) | Must be closed; can be deep |
| Metadata | Files.readAttributes(path, ...) | Fetches many attributes in one call |
Pathis an abstraction over separators, soresolveandnormalizeproduce the right result on every platform.- A path is not a file: nothing is checked until an operation runs, so
Files.existsis inherently racy and should be a hint, not a guarantee. - Handle
IOExceptionat the layer that can decide what to do — retry, use a default, or fail the request. - Prefer the
Fileshelpers overFile; the older class reports failures as a boolean instead of an exception, which loses the reason.
Reading and writing safely
try (var reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
process(line);
}
}
try (var writer = Files.newBufferedWriter(out, StandardCharsets.UTF_8)) {
for (Order order : orders) {
writer.write(order.toCsv());
writer.newLine();
}
}
// raw output streams: buffer them, or every small write is a syscall
try (var out = new BufferedOutputStream(Files.newOutputStream(target))) {
out.write(payload);
}- Always pass a charset. The no-argument constructors use the platform default, so the same file decodes differently on a different machine.
- try-with-resources closes in reverse order and closes even when the body throws, which is what keeps file handles from leaking.
Files.lines,Files.listandFiles.walkreturn streams that hold an open directory handle — an unclosed one shows up later as a mysterious failure to delete or rename.readStringis convenient and loads everything; for a log file or an export, read line by line instead.- Writing to a temporary file and moving it into place makes an update atomic, so a crash cannot leave a half-written file.
💡
Buffering is the single highest-value change in most I/O code. A byte-at-a-time write to an unbuffered stream performs one system call per byte; wrapping it in a buffer turns thousands of calls into a handful.
JSON with records
public record Customer(String id, String email, List<String> tags) {}
ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
String json = mapper.writeValueAsString(customer); // records serialise directly
Customer back = mapper.readValue(json, Customer.class);- Serialise a dedicated DTO record rather than a database entity: the wire format then changes on your terms, not when someone renames a column.
- Jackson binds records through their canonical constructor, so a missing required field fails with a clear error instead of leaving a null behind.
- Decide the unknown-field policy explicitly and set it once on the shared mapper; the default has changed between versions and silently differs between libraries.
- Never deserialise untrusted JSON into arbitrary types through polymorphic typing — that feature has a long history of being used as a gadget for remote code execution.
- For a very large array, use the streaming API or read it in pages; mapping a hundred million elements into a
Listis a memory problem, not a JSON problem. - Keep one configured mapper and share it. Creating a new one per call is both slow and a source of inconsistent behaviour.
FAQ
Which JSON library should I pick?
Jackson for breadth and streaming, Gson for a small dependency, and the newer JSON-B or kotlinx.serialization in stacks that already standardise on them. What matters far more is having exactly one configured mapper in the application.
Why did my file write fail on Windows but work on Linux?
Windows locks files that are still open, and the platform separator and default charset differ. Resolve paths with
Files helpers, always specify UTF-8, and close every reader and stream with try-with-resources.Related
Dates, text and numbers Modules, packages and build configuration
Last refreshed 2026-09-18.