IO #
Input and Output are operations present in almost every application — reading configuration files, writing logs, processing CSVs, or saving computation results. Java provides two families of I/O APIs: java.io, which is stream-based and has existed since Java 1, and java.nio (New I/O), introduced in Java 4 and heavily updated in Java 7 with NIO.2. For almost all modern file needs, NIO.2 through java.nio.file.Files and java.nio.file.Path is the right choice — more expressive, safer, and equipped with utility methods that complete common jobs in a single line. The java.io API is still relevant for streaming and decorator chaining, but for file and path manipulation, NIO.2 is far better. This article covers both in depth, with an emphasis on correct patterns and the anti-patterns that often cause bugs and resource leaks.
The Two I/O API Families #
Before writing code, understand when to use each:
java.io
├── InputStream / OutputStream ← byte streams (binary)
├── Reader / Writer ← character streams (text)
├── FileInputStream/FileOutputStream← low-level file access
├── BufferedReader/BufferedWriter ← buffering for performance
└── File ← path representation (legacy)
java.nio.file (NIO.2 — Java 7+)
├── Path ← path representation (modern)
├── Paths / Path.of() ← factories for creating Paths
├── Files ← file operation utilities (one-liners!)
└── FileSystem / FileSystems ← filesystem abstraction
flowchart TD
TASK{Need}
TASK -->|Read/write simple text\nor binary files| NIO2["NIO.2 — Files + Path\nPrimary choice"]
TASK -->|Stream processing\ndecorator pattern| JAVAIO["java.io\nInputStream/Reader"]
TASK -->|Non-blocking I/O\nthousands of connections| NIO["java.nio\nChannel + Selector"]
TASK -->|Path operations\ndirectory manipulation| NIO2
NIO2 -->|if streaming is needed| JAVAIOPath — Representing File Locations #
Path is Java’s modern way of representing a filesystem location — replacing the old java.io.File.
import java.nio.file.Path;
import java.nio.file.Paths;
// Creating a Path (Java 11+: Path.of() is more idiomatic than Paths.get())
Path p1 = Path.of("/home/user/documents/report.txt"); // absolute
Path p2 = Path.of("data", "input", "file.csv"); // relative, cross-platform
Path p3 = Paths.get("/tmp/cache"); // old way, still valid
// Path navigation
Path dir = Path.of("/home/user/project");
Path file = dir.resolve("src/Main.java"); // combine paths
// "/home/user/project/src/Main.java"
Path parent = file.getParent(); // "/home/user/project/src"
Path fileName = file.getFileName(); // "Main.java"
Path root = file.getRoot(); // "/" (Unix) or "C:\" (Windows)
// Path information
int depth = file.getNameCount(); // number of segments
Path segment = file.getName(2); // 2nd segment (0-based)
// Normalization and absolutization
Path messy = Path.of("/home/user/../user/./documents");
Path clean = messy.normalize(); // "/home/user/documents"
Path absolute = Path.of("relative/path").toAbsolutePath(); // from the current working dir
// Relativize — compute the relative path between two paths
Path base = Path.of("/home/user");
Path target = Path.of("/home/user/documents/report.txt");
Path relative = base.relativize(target); // "documents/report.txt"
// Comparison
boolean same = p1.equals(p2); // compare path values
boolean startsWith = file.startsWith(dir); // true
// Conversion to URI and File (for interop with old APIs)
java.net.URI uri = file.toUri(); // "file:///home/user/..."
java.io.File fileObj = file.toFile(); // interop with java.io
// Cross-platform — Path.of() automatically uses the correct separator
Path crossPlatform = Path.of("data", "output", "result.csv");
// Windows: "data\output\result.csv"
// Unix: "data/output/result.csv"
Files — One-Line File Operations #
Files is a utility class with static methods for all common file operations. This is the API you should use for most file needs.
Reading and Writing Text Files #
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class TextFileOperations {
public void demo() throws Exception {
Path filePath = Path.of("data/report.txt");
// ===== READING =====
// Read the entire content as a String — for small files
String content = Files.readString(filePath); // Java 11+
String contentCharset = Files.readString(filePath, StandardCharsets.UTF_8);
// Read all lines as a List<String>
List<String> lines = Files.readAllLines(filePath); // UTF-8 default
List<String> latinLines = Files.readAllLines(filePath,
StandardCharsets.ISO_8859_1);
// Read as a Stream<String> — for large files (lazy, doesn't load everything into memory)
try (var stream = Files.lines(filePath)) {
stream
.filter(l -> !l.isBlank())
.map(String::trim)
.forEach(System.out::println);
} // stream closed automatically
// Read as a byte array — for small binary files
byte[] bytes = Files.readAllBytes(filePath);
// ===== WRITING =====
// Write a String to a file (create new or overwrite existing)
Files.writeString(filePath, "New content"); // Java 11+
Files.writeString(filePath, "UTF-8 content", StandardCharsets.UTF_8);
// Write with additional options
Files.writeString(filePath, "Additional line\n",
StandardOpenOption.APPEND); // append to the end of the file
Files.writeString(filePath, "Only if not exists",
StandardOpenOption.CREATE_NEW); // fails if the file already exists
// Write a List<String> as lines
List<String> contentLines = List.of("line 1", "line 2", "line 3");
Files.write(filePath, contentLines);
Files.write(filePath, contentLines, StandardCharsets.UTF_8,
StandardOpenOption.APPEND);
// Write a byte array
byte[] data = "binary data".getBytes(StandardCharsets.UTF_8);
Files.write(filePath, data);
}
}
File and Directory Operations #
public class FileManagement {
public void demo() throws Exception {
Path src = Path.of("source.txt");
Path dst = Path.of("destination.txt");
Path dir = Path.of("new/directory");
// ===== EXISTENCE AND ATTRIBUTES =====
boolean exists = Files.exists(src);
boolean notExists = Files.notExists(src);
boolean isFile = Files.isRegularFile(src);
boolean isDir = Files.isDirectory(dir);
boolean readable = Files.isReadable(src);
boolean writable = Files.isWritable(src);
boolean executable = Files.isExecutable(src);
long size = Files.size(src); // in bytes
java.time.Instant modified = Files.getLastModifiedTime(src).toInstant();
// ===== COPY AND MOVE =====
// Copy a file
Files.copy(src, dst); // fails if dst exists
Files.copy(src, dst,
java.nio.file.StandardCopyOption.REPLACE_EXISTING, // overwrite if it exists
java.nio.file.StandardCopyOption.COPY_ATTRIBUTES); // copy file attributes
// Move / rename a file
Files.move(src, dst);
Files.move(src, dst,
java.nio.file.StandardCopyOption.REPLACE_EXISTING,
java.nio.file.StandardCopyOption.ATOMIC_MOVE); // atomic on supporting filesystems
// ===== DELETE =====
Files.delete(src); // throws an exception if it doesn't exist
Files.deleteIfExists(src); // doesn't throw if it doesn't exist
// ===== DIRECTORIES =====
Files.createDirectory(dir); // create one directory level
Files.createDirectories(dir); // create all levels (mkdir -p)
// Temporary files
Path tmpFile = Files.createTempFile("prefix-", ".tmp");
Path tmpDir = Files.createTempDirectory("tmpdir-");
// REMEMBER: delete temporary files when done
tmpFile.toFile().deleteOnExit(); // delete when the JVM exits
}
}
Reading Files with BufferedReader #
For large text files that need line-by-line processing, BufferedReader is more efficient than Files.readAllLines() because it doesn’t load the entire file into memory at once.
import java.io.*;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
public class BufferedReaderDemo {
// ✗ ANTI-PATTERN: FileReader without a charset — depends on the platform default
public void readWithoutCharset(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
// ✓ CORRECT: always specify an explicit charset
public void readWithCharset(Path path) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
// process the line
}
}
// try-with-resources automatically closes the reader even if an exception occurs
}
// ✓ MORE MODERN: Files.lines() with the Stream API
public long countNonEmptyLines(Path path) throws IOException {
try (var lines = Files.lines(path, StandardCharsets.UTF_8)) {
return lines.filter(l -> !l.isBlank()).count();
}
}
// Process a large CSV file without loading everything into memory
public void processLargeCSV(Path csvPath) throws IOException {
try (var lines = Files.lines(csvPath, StandardCharsets.UTF_8)) {
lines
.skip(1) // skip the header
.map(line -> line.split(",")) // split into columns
.filter(columns -> columns.length >= 3)
.forEach(columns -> {
String name = columns[0].trim();
String price = columns[1].trim();
System.out.println("Product: " + name + " | Price: " + price);
});
}
}
}
Writing Files with BufferedWriter #
import java.io.*;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
public class BufferedWriterDemo {
// ✓ CORRECT: BufferedWriter with an explicit charset
public void writeWithBuffer(Path path, List<String> data) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
for (String line : data) {
writer.write(line);
writer.newLine(); // \n or \r\n depending on the OS
}
}
}
// Append to an existing file
public void appendLog(Path logPath, String message) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(
logPath,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE, // create if it doesn't exist
StandardOpenOption.APPEND)) // append to the end
{
writer.write(java.time.LocalDateTime.now() + " | " + message);
writer.newLine();
}
}
// PrintWriter — more convenient for formatted output
public void writeReport(Path path) throws IOException {
try (PrintWriter pw = new PrintWriter(
Files.newBufferedWriter(path, StandardCharsets.UTF_8))) {
pw.println("=== Sales Report ===");
pw.printf("Date: %tF%n", java.time.LocalDate.now());
pw.printf("Total: Rp %.2f%n", 15000000.0);
pw.flush(); // make sure all data is written
}
}
}
Reading and Writing Binary Files #
For binary files (images, PDFs, archives, serialized data), use byte streams without character encoding.
import java.io.*;
import java.nio.file.*;
public class BinaryFileDemo {
// Read a binary file (for small files — loads everything into memory)
public byte[] readBinaryFile(Path path) throws IOException {
return Files.readAllBytes(path); // all bytes at once
}
// Read a large binary file with a buffer
public void processLargeFile(Path src, Path dst) throws IOException {
try (InputStream in = new BufferedInputStream(Files.newInputStream(src));
OutputStream out = new BufferedOutputStream(Files.newOutputStream(dst))) {
byte[] buffer = new byte[8192]; // 8 KB buffer
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
// Both are closed automatically — even if an exception occurs mid-process
}
// Transfer a file — the most efficient way using an OS-level copy
public void copyFile(Path src, Path dst) throws IOException {
try (InputStream in = Files.newInputStream(src);
OutputStream out = Files.newOutputStream(dst)) {
in.transferTo(out); // Java 9+ — delegate to the OS for maximum efficiency
}
}
// DataInputStream / DataOutputStream — read/write primitive types to a binary stream
public void writeBinaryData(Path path) throws IOException {
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(Files.newOutputStream(path)))) {
dos.writeInt(42);
dos.writeDouble(3.14);
dos.writeBoolean(true);
dos.writeUTF("hello"); // write a String as UTF-8 with a length prefix
}
}
public void readBinaryData(Path path) throws IOException {
try (DataInputStream dis = new DataInputStream(
new BufferedInputStream(Files.newInputStream(path)))) {
int number = dis.readInt(); // must be read in the same order as written!
double decimal = dis.readDouble();
boolean bool = dis.readBoolean();
String text = dis.readUTF();
System.out.printf("int=%d, double=%.2f, bool=%b, str=%s%n",
number, decimal, bool, text);
}
}
}
Directory Traversal #
Listing Directory Contents #
import java.nio.file.*;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
public class DirectoryTraversal {
// List directory contents (one level only)
public List<Path> listContents(Path dir) throws IOException {
try (var stream = Files.list(dir)) {
return stream
.sorted() // sort by name
.toList();
}
}
// Filter only specific files
public List<Path> findJavaFiles(Path dir) throws IOException {
try (var stream = Files.list(dir)) {
return stream
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.sorted()
.toList();
}
}
// Recursive traversal of the entire directory tree
public List<Path> allFiles(Path root) throws IOException {
try (var stream = Files.walk(root)) {
return stream
.filter(Files::isRegularFile)
.sorted()
.toList();
}
}
// walk with a maximum depth
public List<Path> filesAtDepth2(Path root) throws IOException {
try (var stream = Files.walk(root, 2)) { // max 2 levels down
return stream
.filter(Files::isRegularFile)
.toList();
}
}
// Calculate the total size of a directory
public long totalDirSize(Path dir) throws IOException {
try (var stream = Files.walk(dir)) {
return stream
.filter(Files::isRegularFile)
.mapToLong(p -> {
try { return Files.size(p); }
catch (IOException e) { return 0L; }
})
.sum();
}
}
// find() — traversal with stronger filters
public List<Path> findLargeFiles(Path root, long minSize) throws IOException {
try (var stream = Files.find(root, Integer.MAX_VALUE,
(path, attrs) ->
attrs.isRegularFile() &&
attrs.size() > minSize)) {
return stream.toList();
}
}
// Delete a directory and its contents (recursively)
public void deleteDirectory(Path dir) throws IOException {
// Files.delete() fails if the directory isn't empty
// Must delete the contents first using walk in reverse order
try (var stream = Files.walk(dir)) {
stream.sorted(java.util.Comparator.reverseOrder())
.forEach(path -> {
try { Files.delete(path); }
catch (IOException e) {
throw new RuntimeException("Failed to delete: " + path, e);
}
});
}
}
// Copy a directory and its contents
public void copyDirectory(Path src, Path dst) throws IOException {
try (var stream = Files.walk(src)) {
stream.forEach(sourcePath -> {
Path targetPath = dst.resolve(src.relativize(sourcePath));
try {
if (Files.isDirectory(sourcePath)) {
Files.createDirectories(targetPath);
} else {
Files.copy(sourcePath, targetPath,
StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) {
throw new RuntimeException("Failed to copy: " + sourcePath, e);
}
});
}
}
}
File Attributes and Metadata #
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.time.Instant;
public class FileAttributeDemo {
public void readAttributes(Path path) throws Exception {
// Basic attributes
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
System.out.println("Regular file: " + attrs.isRegularFile());
System.out.println("Directory: " + attrs.isDirectory());
System.out.println("Symbolic link: " + attrs.isSymbolicLink());
System.out.println("Size: " + attrs.size() + " bytes");
System.out.println("Created: " + attrs.creationTime().toInstant());
System.out.println("Last modified: " + attrs.lastModifiedTime().toInstant());
System.out.println("Last accessed: " + attrs.lastAccessTime().toInstant());
// Change the modification time
Files.setLastModifiedTime(path,
FileTime.from(Instant.now()));
// POSIX attributes (Unix/Linux/Mac)
if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
PosixFileAttributes posix = Files.readAttributes(path, PosixFileAttributes.class);
System.out.println("Owner: " + posix.owner().getName());
System.out.println("Group: " + posix.group().getName());
System.out.println("Permissions: " + PosixFilePermissions.toString(posix.permissions()));
// Change permissions (equivalent to chmod 644)
Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("rw-r--r--"));
}
}
}
WatchService — Watching for File Changes #
WatchService lets an application react to directory changes — new files added, modified, or deleted — without repeated polling.
import java.nio.file.*;
public class FileWatcherDemo {
public void watchDirectory(Path dir) throws Exception {
WatchService watcher = FileSystems.getDefault().newWatchService();
// Register the events to watch
dir.register(watcher,
StandardWatchEventKinds.ENTRY_CREATE, // new file created
StandardWatchEventKinds.ENTRY_MODIFY, // file modified
StandardWatchEventKinds.ENTRY_DELETE // file deleted
);
System.out.println("Watching directory: " + dir);
while (true) {
// take() — blocks until an event arrives
WatchKey key = watcher.take();
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
// OVERFLOW means some events may have been missed
if (kind == StandardWatchEventKinds.OVERFLOW) {
System.out.println("Some events may have been missed!");
continue;
}
// The name of the changed file
@SuppressWarnings("unchecked")
WatchEvent<Path> pathEvent = (WatchEvent<Path>) event;
Path fileName = pathEvent.context();
Path fullPath = dir.resolve(fileName);
if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
System.out.println("File created: " + fullPath);
} else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
System.out.println("File modified: " + fullPath);
} else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
System.out.println("File deleted: " + fullPath);
}
}
// Reset the key — required to receive the next events
boolean valid = key.reset();
if (!valid) {
System.out.println("The directory can no longer be watched");
break;
}
}
}
}
Object Serialization #
Java provides a built-in serialization mechanism to convert objects to a byte stream and back. Note that Java’s built-in serialization has many weaknesses and should be replaced with more modern formats (JSON, Protobuf, Avro) for inter-system communication.
import java.io.*;
import java.nio.file.*;
// A serializable class must implement Serializable
public class Product implements Serializable {
// serialVersionUID is important! If it's missing and the class structure changes,
// deserializing old objects fails with an InvalidClassException
@Serial
private static final long serialVersionUID = 1L;
private String name;
private double price;
// Fields that don't need serialization — marked transient
private transient String cacheKey; // won't be serialized
public Product(String name, double price) {
this.name = name;
this.price = price;
}
// getters...
}
public class SerializationDemo {
// Serialization — object to file
public void save(Product product, Path path) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(Files.newOutputStream(path)))) {
oos.writeObject(product);
}
}
// Deserialization — file to object
public Product load(Path path) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(Files.newInputStream(path)))) {
return (Product) ois.readObject();
}
}
}
Java’s built-in serialization (ObjectInputStream/ObjectOutputStream) has serious security risks — deserializing untrusted data can lead to Remote Code Execution. For data exchanged between systems or stored long-term, use JSON (Jackson/Gson), Protobuf, or a more modern format. Java’s built-in serialization is only safe when the data comes from a fully trusted source.
Safe Resource Handling Patterns #
Try-with-Resources #
Always use try-with-resources to ensure resources are closed even if an exception occurs:
// ✗ ANTI-PATTERN: manual closing — resource leak if an exception occurs before close()
BufferedReader reader = null;
try {
reader = Files.newBufferedReader(path);
String line = reader.readLine();
// if an exception occurs here, reader.close() is never called!
} finally {
if (reader != null) {
reader.close(); // boilerplate and easy to forget
}
}
// ✓ CORRECT: try-with-resources — close() is called automatically, even on exceptions
try (BufferedReader reader = Files.newBufferedReader(path)) {
String line = reader.readLine();
// exception here? reader.close() is still called!
}
// Multiple resources — closed in reverse order (ois before bis)
try (InputStream bis = new BufferedInputStream(Files.newInputStream(src));
ObjectInputStream ois = new ObjectInputStream(bis)) {
Object obj = ois.readObject();
}
// Custom resources — implement AutoCloseable
public class DBConnection implements AutoCloseable {
public DBConnection(String url) { /* open the connection */ }
@Override
public void close() {
/* close the connection — called automatically by try-with-resources */
System.out.println("DB connection closed");
}
}
try (DBConnection db = new DBConnection("jdbc:postgresql://...")) {
// use db
} // db.close() is called automatically
When to Use Which API #
USE Files (NIO.2) WHEN:
✓ Reading/writing text files — Files.readString(), Files.writeString()
✓ Simple binary read/write — Files.readAllBytes(), Files.write()
✓ File/directory manipulation — copy, move, delete, createDirectories
✓ Directory traversal — Files.list(), Files.walk(), Files.find()
✓ Checking file attributes and metadata
✓ Almost all modern file needs
USE java.io Streams WHEN:
✓ Very large files that don't fit in memory — line-by-line streaming
✓ Decorator chaining (Buffer + Gzip + Cipher + stream)
✓ Java object serialization
✓ Interoperability with old APIs that use InputStream/Reader
USE WatchService WHEN:
✓ The application needs to react to file changes in real time
✓ Hot-reloading configuration without restarting the application
✓ Pipelines that process files as they arrive in a directory
Summary #
FilesandPath(NIO.2) are the modern APIs to use for almost all file operations —Files.readString(),Files.writeString(),Files.copy(),Files.walk()complete common jobs in one expressive line.- Always use try-with-resources for all I/O resources —
BufferedReader,InputStream,Stream<Path>are allAutoCloseableand must be closed to prevent file handle leaks.- Specify the charset explicitly (
StandardCharsets.UTF_8) every time you work with text. Relying on the platform default causes bugs that are hard to reproduce in different environments.Files.lines()for large text files — it returns a lazyStream<String>that doesn’t load the entire file into memory likeFiles.readAllLines().BufferedInputStream/BufferedOutputStreamalways wrap rawInputStream/OutputStreamfor performance — unbuffered I/O is very slow because everyread()/write()becomes a system call.Path.of()(Java 11+) replacesPaths.get()andnew File()— use it for all new path representations. It’s cross-platform and automatically uses the correct separator.in.transferTo(out)(Java 9+) is the most efficient way to copy streams — delegate to the OS level to avoid buffer copying in Java.- Avoid Java’s built-in serialization for data exchanged between systems or stored long-term — use JSON, Protobuf, or Avro, which are safer, more portable, and easier to evolve.