Java NIO (New Input/Output) = An alternative to the original java.io API, designed to improve performance, scalability, and flexibility.
It introduces a non-blocking I/O model, buffers, channels, and selectors, which are more suitable for high-performance applications such as file systems, network servers, and frameworks.
Stream-oriented
Always blocking
Sequential access
Slower for large files or network I/O
Minimal memory use per read/write
File, InputStream, OutputStream, Reader, Writer
Buffer-oriented
Can be non-blocking (asynchronous)
Can do sequential and random access
more scalable for large files or async operations
Requires buffers (ByteBuffer, CharBuffer)
Buffer, Channel, Path, Files, Selector
Path replaces java.io.File as a more powerful, flexible, and immutable representation of paths.
Platform independent
Path p = Path.of("/home/user/data.txt");
p.getFileName(); // data.txt
p.getParent(); // /home/user
p.toUri(); // file:///home/user/file.txt
Path original = Path.of("/home/user/data.txt");
// Convert to URI
URI uri = original.toUri();
// Reconstruct Path
Path reconstructed = Path.of(uri);
URI zipUri = URI.create("jar:file:/home/user/archive.zip");
try (FileSystem fs = FileSystems.newFileSystem(zipUri, Map.of())) {
Path insideZip = fs.getPath("/data.txt");
System.out.println(Files.readString(insideZip));
}
Note: Path.toUri() gives you a file: URI — not a network URL (a URI with protocol semantics)
URI uri = URI.create("file:///home/user/data.txt");
URL url = uri.toURL(); // URI → URL
URI back = url.toURI(); // URL → URI
Path p = Path.of("/home/user/data.txt");
// Check existence
boolean exists = Files.exists(p);
// Creates a new file
Files.createFile(p);
// Create parent dirs if missing
Files.createDirectories(p.getParent());
// Copy or move
Files.copy(p, Path.of("/backup/data.txt"));
Files.move(p, Path.of("/archive/data.txt"));
// Delete
Files.deleteIfExists(p);
Path p = Path.of("example.txt");
// Write text (overwrites by default)
Files.writeString(p, "Hello, NIO!");
// Write text -- Appends
Files.writeString(p, "Hello\n", StandardOpenOption.APPEND);
// Write text -- Creates & Appends
Files.writeString(p, "New line\n", StandardOpenOption.CREATE, StandardOpenOption.APPEND);
// Read the whole file content
String content = Files.readString(p);
// Write lines (overwrites by default)
Files.write(p, List.of("Line 1", "Line 2"));
// Read lines
List<String> lines = Files.readAllLines(p);
These methods automatically handle buffering and character encoding — very convenient compared to older FileReader/BufferedReader APIs.
StandardOpenOption:
StandardOpenOption.CREATE: Create file if missing (OK if it exists)
StandardOpenOption.CREATE_NEW: Create file only if missing (error if exists)
StandardOpenOption.TRUNCATE_EXISTING: Clear old content first (default)
StandardOpenOption.APPEND: Add new content at end
Path p = Path.of("example.txt");
// size in bytes
Files.size(p);
Files.isDirectory(p));
Files.getLastModifiedTime(p);
Files.getOwner(p);
Path dir = Path.of("/home/user");
try (Stream<Path> stream = Files.walk(dir)) {
stream
.filter(Files::isRegularFile)
.forEach(System.out::println);
}
Path target = Path.of("/home/user/data.txt");
Path link = Path.of("/home/user/data-link.txt");
// Creates a link
Files.createSymbolicLink(link, target);
// Check if it's a link
Files.isSymbolicLink(link))
// Actual target path
Path target = Files.readSymbolicLink(link);
// Deletes the link, NOT the target
Files.delete(link);
java.io.* works with streams — unidirectional flows of bytes or characters. Whereas, NIO is buffer-oriented and bidirectional.
Instead of pushing data byte by byte, we read or write data through a buffer that sits between the program and the OS.
┌───────────────┐
│ Channel │ ←→ external resource (file/socket)
└───────▲───────┘
│
read/write
│
┌───────▼───────┐
│ Buffer │ ←→ Java program
└───────────────┘
Data flow:
Read: Channel → Buffer → Java code
Write: Java code → Buffer → Channel
A Buffer is a fixed-size, memory-backed container that holds data to be read from or written to a Channel.
Every Buffer has four key attributes that define its state:
capacity: Total number of elements it can hold
position: Next index to read or write
limit: End of accessible data
mark: Bookmark for resetting position
Typical buffer operations follow this pattern:
// Allocate — create a buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// Write data into buffer (e.g., from user input or a channel)
buffer.put((byte) 65); // writes 'A'
// Flip — prepare for reading
buffer.flip();
// Read data
byte b = buffer.get();
// Clear — reset for new write
buffer.clear();
ByteBuffer: 8-bit bytes → for file & network I/O
CharBuffer: 16-bit chars → for text data
IntBuffer, LongBuffer, etc.: Primitive types → for binary data
MappedByteBuffer: Memory-mapped file buffer → for huge file access
Type Created by Stored in Performance
Non-direct: ByteBuffer.allocate() → Stored in JVM heap; easier to manage
Direct: ByteBuffer.allocateDirect() → OS-managed native memory; faster I/O — bypasses copying between JVM and OS
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
A Channel is like a bidirectional stream that supports reading and writing using Buffers.
It represents an open connection to an I/O entity — a file, socket, or even a device.
Channel Implementations:
FileChannel → for Files
SocketChannel → for TCP client sockets
ServerSocketChannel → for TCP server sockets
DatagramChannel → for UDP datagrams
Channels.newChannel() → wraps standard I/O streams
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.io.IOException;
public class ChannelExample {
public static void main(String[] args) throws IOException {
Path path = Paths.get("example.txt");
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(64);
while (channel.read(buffer) != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
}
}
}
}
Path path = Paths.get("output.txt");
try (FileChannel channel = FileChannel.open(path,
StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
ByteBuffer buffer = ByteBuffer.wrap("Hello NIO!".getBytes());
channel.write(buffer);
}
A MappedByteBuffer allows to treat a file as if it were an in-memory array — extremely fast for random access.
try (FileChannel fc = FileChannel.open(Paths.get("data.bin"),
StandardOpenOption.READ, StandardOpenOption.WRITE)) {
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, fc.size());
mbb.put(0, (byte) 42); // modify directly in memory
}
long size = channel.size(); // file length
channel.position(0); // set read/write pointer
channel.truncate(1024); // reduce file size
Channels are seekable, meaning we can move around the file easily (unlike streams).
Channels support batch operations with multiple buffers:
Scattering Read: Read data from a channel into multiple buffers (in order).
Gathering Write: Write data from multiple buffers to a channel (in order).
NIO Channels can be configured as non-blocking
We can then use a Selector to monitor multiple channels simultaneously.