Fundamentals 12 min read

Understanding Java IO: BIO, NIO, and AIO Explained

The article explains Java's I/O ecosystem, covering synchronous blocking BIO, synchronous non‑blocking NIO, and asynchronous non‑blocking AIO, their classifications, practical code examples, and when to choose byte streams versus character streams.

Shepherd Advanced Notes
Shepherd Advanced Notes
Shepherd Advanced Notes
Understanding Java IO: BIO, NIO, and AIO Explained

IO Models

Java I/O operations are categorized into three models:

Blocking I/O (BIO) : synchronous and blocking; a read call blocks the thread until the kernel copies data to user space.

Non‑blocking I/O (NIO) : synchronous but does not block; the application polls multiple channels and processes those that are ready.

Asynchronous I/O (AIO) : the kernel notifies the application when an operation completes, eliminating the need for explicit polling.

Stream Classification

Streams are divided by data direction and data unit:

Direction

Input streams bring data from devices into the process ( InputStream for bytes, Reader for characters).

Output streams send data from the process to devices ( OutputStream for bytes, Writer for characters).

Data unit

Byte streams transfer 8‑bit units ( InputStream, OutputStream).

Character streams transfer Unicode characters (2 bytes per character) via Reader and Writer.

Top‑Level Abstract Classes

InputStream

– byte input OutputStream – byte output Reader – character input Writer – character output

Conversion Streams

Byte‑to‑character: InputStreamReader Character‑to‑byte:

OutputStreamWriter

Three Implementation Models Illustrated

BIO : continuously monitor a single kettle; after it boils, move to the next kettle.

NIO : periodically check all kettles and handle those that have boiled.

AIO : each kettle notifies a thread when it boils, eliminating the need for monitoring.

Blocking I/O (BIO)

BIO works for a small number of connections but becomes a bottleneck when handling tens or hundreds of thousands of concurrent connections.

File‑copy example using plain streams (BIO)

package com.shepherd.example.bio;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import java.io.*;
import java.nio.channels.FileChannel;
import java.nio.ByteBuffer;

public class BioDemo {
    public static void main(String[] args) throws IOException {
        String source = "/path/to/source.mp4";
        String destination = "/path/to/dest.mp4";
        long start = System.currentTimeMillis();
        copyWithFileInputStream(source, destination);
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

    public static void copyWithFileInputStream(String source, String destination) throws IOException {
        InputStream inputStream = new FileInputStream(source);
        OutputStream outputStream = new FileOutputStream(destination);
        byte[] buf = new byte[8192];
        int len;
        while ((len = inputStream.read(buf)) > 0) {
            outputStream.write(buf, 0, len);
        }
        inputStream.close();
        outputStream.close();
    }

    public static void copyWithBufferInputStream(String source, String destination) throws IOException {
        InputStream in = new FileInputStream(source);
        OutputStream out = new FileOutputStream(destination);
        BufferedInputStream bis = new BufferedInputStream(in);
        BufferedOutputStream bos = new BufferedOutputStream(out);
        byte[] buf = new byte[8192];
        int len;
        while ((len = bis.read(buf)) > 0) {
            bos.write(buf, 0, len);
        }
        bis.close();
        bos.close();
    }

    public static void copyWithFileChannel(String source, String destination) throws IOException {
        FileChannel inChannel = new FileInputStream(source).getChannel();
        FileChannel outChannel = new FileOutputStream(destination).getChannel();
        ByteBuffer buf = ByteBuffer.allocate(8192);
        while (inChannel.read(buf) != -1) {
            buf.flip();
            outChannel.write(buf);
            buf.clear();
        }
        inChannel.close();
        outChannel.close();
    }

    public static void copyWithUtils(String source, String destination) throws IOException {
        FileUtils.copyFile(new File(source), new File(destination));
    }
}

Key observations from the code:

Using raw FileInputStream / FileOutputStream reads and writes 8 KB buffers in a tight loop – a classic BIO pattern.

Wrapping the streams with BufferedInputStream and BufferedOutputStream adds an 8 KB in‑memory buffer, reducing the number of system calls when the read size is small.

When the read size approaches or exceeds the buffer size, the performance difference between buffered and unbuffered streams diminishes.

The FileChannel version uses NIO buffers ( ByteBuffer) and the channel’s read / write methods, demonstrating a non‑blocking style under the hood.

Apache Commons FileUtils.copyFile implementation

The method internally creates FileInputStream and FileOutputStream, obtains their channels, and then uses FileChannel.transferFrom to move data. The core logic (simplified) is:

private static void doCopyFile(File srcFile, File destFile, boolean preserveFileDate) throws IOException {
    if (destFile.exists() && destFile.isDirectory()) {
        throw new IOException("Destination '" + destFile + "' exists but is a directory");
    }
    FileInputStream fis = null;
    FileOutputStream fos = null;
    FileChannel input = null;
    FileChannel output = null;
    try {
        fis = new FileInputStream(srcFile);
        fos = new FileOutputStream(destFile);
        input = fis.getChannel();
        output = fos.getChannel();
        long size = input.size();
        long pos = 0;
        long count;
        while (pos < size) {
            count = (size - pos) > FILE_COPY_BUFFER_SIZE ? FILE_COPY_BUFFER_SIZE : (size - pos);
            pos += output.transferFrom(input, pos, count);
        }
    } finally {
        IOUtils.closeQuietly(output);
        IOUtils.closeQuietly(fos);
        IOUtils.closeQuietly(input);
        IOUtils.closeQuietly(fis);
    }
    if (srcFile.length() != destFile.length()) {
        throw new IOException("Failed to copy full contents from '" + srcFile + "' to '" + destFile + "'");
    }
    if (preserveFileDate) {
        destFile.setLastModified(srcFile.lastModified());
    }
}

Because the implementation relies on FileChannel.transferFrom, the actual data movement uses NIO, even though the surrounding API appears as a high‑level BIO utility.

Summary of Core Concepts

Byte streams operate on raw byte arrays; character streams add a Unicode conversion layer, which can be costly and may cause encoding issues.

Use character streams for text data and byte streams for binary data such as images or audio.

Buffered streams improve performance for small read/write operations by reducing system‑call overhead.

When copying large files, NIO channels ( FileChannel) and methods like transferFrom provide a more scalable solution.

High‑level utilities (e.g., Apache Commons FileUtils.copyFile) may internally choose BIO or NIO based on the method implementation.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaNIOIOBIOAIOFileChannelBufferedStream
Shepherd Advanced Notes
Written by

Shepherd Advanced Notes

Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.