Polyglot
Languages Java io
Java § io

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:

ClassPurpose
FileInputStream, FileOutputStreamFile-based
BufferedInputStream, BufferedOutputStreamBuffer the underlying stream
ByteArrayInputStream, ByteArrayOutputStreamIn-memory
DataInputStream, DataOutputStreamRead/write primitive types
ObjectInputStream, ObjectOutputStreamSerialisation
PrintStreamFormatted 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:

ClassPurpose
FileReader, FileWriterFile-based; uses default charset (deprecated for non-UTF-8)
InputStreamReader, OutputStreamWriterConvert between byte and char with explicit charset
BufferedReader, BufferedWriterBuffer; admit readLine()
StringReader, StringWriterIn-memory string
CharArrayReader, CharArrayWriterIn-memory char array
PrintWriterFormatted 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:

TypePurpose
PathA filesystem path (cross-platform)
PathsStatic factory methods (mostly superseded by Path.of)
FilesStatic utility methods for filesystem operations
FileSystemA filesystem (typically the default; admits zip filesystems)
FileSystemsFactory 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:

TypePurpose
ChannelA connection to an entity (file, socket)
BufferA fixed-size container for primitive values
SelectorA 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 libraryjava.io, java.nio, java.nio.file, java.net.http.
  • Apache Commons IO — utility methods (FileUtils, IOUtils).
  • Google GuavaFiles.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.