I/O and NIO
Java has two distinct I/O frameworks: the original java.io (since Java 1.0), built around byte-oriented streams (InputStream, OutputStream) and character-oriented readers and writers (Reader, Writer); and java.nio (since Java 1.4) and java.nio.file (since Java 7), built around channels, buffers, and the Path/Files APIs. Both frameworks remain in active use; the conventional contemporary choice is java.nio.file for filesystem operations and java.io for stream-based I/O. Asynchronous I/O is supported through java.nio.channels.AsynchronousFileChannel and AsynchronousSocketChannel; the conventional contemporary choice for non-blocking I/O is the higher-level reactive frameworks (Reactor, RxJava) or the modern HTTP client.
This page covers the principal I/O facilities, the java.io and java.nio.file choices, the modern HTTP client, and the conventions for using each.
The java.io model
The classical I/O model is built around four abstractions:
InputStream— byte input.OutputStream— byte output.Reader— character input (with a charset for byte-to-char decoding).Writer— character output (with a charset for char-to-byte encoding).
The four are abstract; concrete implementations include FileInputStream, FileOutputStream, BufferedInputStream, ByteArrayInputStream, FileReader, BufferedReader, etc.
import java.io.*;
try (var in = new FileInputStream(path);
var buffered = new BufferedInputStream(in)) {
byte[] buf = new byte[4096];
int read;
while ((read = buffered.read(buf)) > 0) {
process(buf, 0, read);
}
}
try (var reader = new BufferedReader(new FileReader(path))) {
String line;
while ((line = reader.readLine()) != null) {
process(line);
}
}
The model uses decorator composition: each stream wraps another stream that adds behaviour (buffering, character decoding, formatting). The composition is verbose but flexible; the conventional contemporary form uses Files.newBufferedReader and similar shortcuts (treated below).
Byte streams: InputStream and OutputStream
InputStream and OutputStream operate on raw bytes:
public abstract class InputStream {
public abstract int read() throws IOException;
public int read(byte[] b) throws IOException;
public int read(byte[] b, int off, int len) throws IOException;
public byte[] readAllBytes() throws IOException; // since Java 9
public byte[] readNBytes(int n) throws IOException; // since Java 11
public long transferTo(OutputStream out) throws IOException; // since Java 9
public void close() throws IOException;
}
The principal concrete classes:
| Class | Purpose |
|---|---|
FileInputStream, FileOutputStream | File-based |
BufferedInputStream, BufferedOutputStream | Buffer the underlying stream |
ByteArrayInputStream, ByteArrayOutputStream | In-memory |
DataInputStream, DataOutputStream | Read/write primitive types |
ObjectInputStream, ObjectOutputStream | Serialisation |
PrintStream | Formatted output (System.out is one) |
The conventional discipline is to wrap raw streams in a BufferedInputStream / BufferedOutputStream for efficiency; per-byte operations on unbuffered streams are slow because each read() may incur a system call.
Character streams: Reader and Writer
Reader and Writer operate on char values, with an explicit charset for byte conversion:
public abstract class Reader {
public abstract int read(char[] cbuf, int off, int len) throws IOException;
public int read() throws IOException;
public int read(char[] cbuf) throws IOException;
public void close() throws IOException;
}
The principal concrete classes:
| Class | Purpose |
|---|---|
FileReader, FileWriter | File-based; uses default charset (deprecated for non-UTF-8) |
InputStreamReader, OutputStreamWriter | Convert between byte and char with explicit charset |
BufferedReader, BufferedWriter | Buffer; admit readLine() |
StringReader, StringWriter | In-memory string |
CharArrayReader, CharArrayWriter | In-memory char array |
PrintWriter | Formatted output |
The conventional pattern for reading text:
try (var reader = new BufferedReader(new InputStreamReader(
new FileInputStream(path), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
process(line);
}
}
The triple-decorator form is verbose. Modern Java provides shortcuts in java.nio.file.Files:
try (var reader = Files.newBufferedReader(Path.of(path), StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
process(line);
}
}
The form is the conventional contemporary choice.
Files and Path: the modern filesystem API
java.nio.file (since Java 7) provides a modern filesystem API:
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
Path path = Path.of("/var/log/app.log");
boolean exists = Files.exists(path);
long size = Files.size(path);
FileTime mod = Files.getLastModifiedTime(path);
// Read entire file:
String contents = Files.readString(path, StandardCharsets.UTF_8);
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
byte[] bytes = Files.readAllBytes(path);
// Write entire file:
Files.writeString(path, "hello, world\n");
Files.write(path, lines);
Files.write(path, bytes);
// Streaming:
try (var lineStream = Files.lines(path)) {
lineStream.filter(line -> line.contains("ERROR"))
.forEach(System.out::println);
}
// Filesystem operations:
Files.createDirectory(path);
Files.createDirectories(path);
Files.copy(source, destination);
Files.move(source, destination);
Files.delete(path);
boolean isDir = Files.isDirectory(path);
boolean isFile = Files.isRegularFile(path);
// Directory iteration:
try (var entries = Files.list(directory)) {
entries.forEach(System.out::println);
}
try (var walk = Files.walk(directory, Integer.MAX_VALUE)) {
walk.filter(Files::isRegularFile)
.forEach(System.out::println);
}
The principal types:
| Type | Purpose |
|---|---|
Path | A filesystem path (cross-platform) |
Paths | Static factory methods (mostly superseded by Path.of) |
Files | Static utility methods for filesystem operations |
FileSystem | A filesystem (typically the default; admits zip filesystems) |
FileSystems | Factory for filesystems |
The Path type abstracts over Unix and Windows paths; cross-platform code uses Path rather than String for paths.
Files.lines is the conventional contemporary form for line-by-line reading; the returned Stream<String> is AutoCloseable and must be closed (typically with try-with-resources).
Asynchronous file I/O
java.nio.channels.AsynchronousFileChannel admits non-blocking file I/O:
import java.nio.channels.AsynchronousFileChannel;
import java.nio.ByteBuffer;
import java.nio.file.StandardOpenOption;
try (var channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.allocate(1024);
Future<Integer> future = channel.read(buf, 0);
int bytesRead = future.get();
// ...
}
The mechanism is rare in user code; the conventional contemporary alternative for blocking file I/O on virtual threads (Java 21+) is the synchronous form, which scales because virtual threads cost so little.
Channels, buffers, and selectors (java.nio)
The original java.nio (Java 1.4) provides a lower-level interface:
| Type | Purpose |
|---|---|
Channel | A connection to an entity (file, socket) |
Buffer | A fixed-size container for primitive values |
Selector | A multiplexer for multiple channels |
try (var channel = FileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.allocate(4096);
while (channel.read(buf) > 0) {
buf.flip();
process(buf);
buf.clear();
}
}
The principal channel types:
FileChannel— file access.SocketChannel,ServerSocketChannel— TCP networking.DatagramChannel— UDP.Pipe.SourceChannel,Pipe.SinkChannel— in-process pipes.
The Selector admits a single thread monitoring multiple channels for readiness — the foundation of non-blocking I/O. Modern code typically uses higher-level reactive libraries (Reactor, RxJava) or virtual threads (Java 21+) instead of raw selectors.
Memory-mapped files
FileChannel.map(...) returns a MappedByteBuffer that maps a file into memory:
try (var channel = FileChannel.open(path, StandardOpenOption.READ)) {
MappedByteBuffer buf = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
// read from buf as if it were a byte array
while (buf.hasRemaining()) {
byte b = buf.get();
process(b);
}
}
The OS handles paging; the JVM treats the buffer as ordinary memory. The mechanism is the conventional choice for large-file access where random-access reads are needed.
The HTTP client (java.net.http)
Java 11 introduced a modern HTTP client:
import java.net.http.*;
import java.net.URI;
import java.time.Duration;
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/data"))
.timeout(Duration.ofSeconds(30))
.header("Accept", "application/json")
.GET()
.build();
// Synchronous:
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
int status = response.statusCode();
String body = response.body();
// Asynchronous:
CompletableFuture<HttpResponse<String>> future = client.sendAsync(
request,
HttpResponse.BodyHandlers.ofString()
);
future.thenApply(HttpResponse::body)
.thenAccept(System.out::println);
The client supports HTTP/1.1, HTTP/2, and WebSockets; it admits both synchronous and asynchronous use. The conventional contemporary choice for HTTP in Java; the older URLConnection-based API is obsolete.
For the request body:
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"alice\"}"))
.build();
Body publishers admit ofString, ofByteArray, ofFile(Path), ofInputStream(Supplier), and noBody().
Common patterns
Read entire file
String contents = Files.readString(path, StandardCharsets.UTF_8);
For very large files, prefer line-by-line:
try (var lines = Files.lines(path, StandardCharsets.UTF_8)) {
lines.forEach(this::process);
}
Write entire file
Files.writeString(path, contents, StandardCharsets.UTF_8);
Append to file
Files.writeString(path, contents, StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
Copy a stream
try (var in = Files.newInputStream(source);
var out = Files.newOutputStream(destination)) {
in.transferTo(out);
}
transferTo (Java 9+) is the conventional one-line file copy.
Read fixed-size records
try (var stream = Files.newInputStream(path);
var buffered = new BufferedInputStream(stream)) {
byte[] record = new byte[RECORD_SIZE];
while (buffered.readNBytes(record, 0, RECORD_SIZE) == RECORD_SIZE) {
process(record);
}
}
readNBytes (Java 11+) reads exactly n bytes (or fewer at EOF).
Atomic file replacement
Path tempPath = Files.createTempFile(directory, "atomic-", ".tmp");
try {
Files.writeString(tempPath, contents);
Files.move(tempPath, target, StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE);
} catch (Exception e) {
Files.deleteIfExists(tempPath);
throw e;
}
The pattern admits writing-then-renaming, which is atomic on most filesystems and avoids leaving the file in an intermediate state if the writer crashes.
Filesystem watcher
import java.nio.file.*;
WatchService watcher = FileSystems.getDefault().newWatchService();
directory.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE);
while (true) {
WatchKey key = watcher.take();
for (WatchEvent<?> event : key.pollEvents()) {
Path changed = directory.resolve((Path) event.context());
process(event.kind(), changed);
}
key.reset();
}
The mechanism admits monitoring directory changes; the principal use is hot-reloading configuration, source files, or assets.
Encoding and Charset
Java’s java.nio.charset.Charset represents a character encoding:
import java.nio.charset.StandardCharsets;
byte[] utf8 = "hello, 世界".getBytes(StandardCharsets.UTF_8);
String back = new String(utf8, StandardCharsets.UTF_8);
The StandardCharsets class provides the conventional charsets:
StandardCharsets.UTF_8— the conventional contemporary choice.StandardCharsets.UTF_16,UTF_16BE,UTF_16LE— UTF-16 variants.StandardCharsets.US_ASCII— 7-bit ASCII.StandardCharsets.ISO_8859_1— Latin-1.
Java code that handles text should specify the charset explicitly; the platform default (Charset.defaultCharset()) varies by OS and locale and is rarely the right choice.
The conventional discipline:
- Use UTF-8 explicitly for new code.
- Convert at the boundaries with external systems that use different encodings.
- Test encoding handling for non-ASCII data — many bugs are encoding-related.
A note on the alternatives
The Java I/O ecosystem includes:
- The standard library —
java.io,java.nio,java.nio.file,java.net.http. - Apache Commons IO — utility methods (
FileUtils,IOUtils). - Google Guava —
Files.toByteArray,Files.toString, etc. - Reactor / RxJava — reactive streams for I/O.
- OkHttp — an alternative HTTP client (Square).
For most application code, the standard library suffices. The third-party libraries fill in cases the standard does not cover gracefully (e.g., Apache Commons predates many modern standard-library additions).
The combination of Files for filesystem operations, the modern HttpClient for HTTP, BufferedReader / BufferedWriter for stream-based I/O, and try-with-resources for guaranteed cleanup is the conventional contemporary I/O toolkit.