Java FileInputStream vs. RandomAccessFile: JDK 8 Source Code Analysis for File I/O, Random Access, and Buffering Optimization

[AI Readability Summary] FileInputStream provides sequential byte reads, while RandomAccessFile provides position-based random read/write access. Together, they address the fundamentals of Java file I/O, including file access, permission checks, resource cleanup, and performance extensibility. Keywords: FileInputStream, RandomAccessFile, JDK 8 I/O.

This article focuses on two core file I/O capabilities in JDK 8

Parameter Description
Language Java
Runtime Environment Windows + JDK 8
Related Protocols/Interfaces InputStream, DataInput, DataOutput, Closeable
Key Objects FileDescriptor, FileChannel, RandomAccessFile
Underlying Mechanism Native methods + OS file handles
Core Dependencies java.io, java.nio.channels, sun.nio.ch.FileChannelImpl
Article Type JDK source code analysis and practice

FileInputStream is the most basic file byte input stream. It is suitable for sequentially reading binary content such as images, audio files, and compressed archives. It hides operating system differences and exposes a unified set of interfaces such as read, skip, available, and close.

RandomAccessFile does not belong to the InputStream/OutputStream inheritance hierarchy. Instead, it independently provides position-based read/write capabilities. Its core value lies in treating a file as a large byte array and using a file pointer to support seek, overwrite writes, partial reads, and structured binary I/O.

clipboard AI Visual Insight: This diagram shows where FileInputStream sits in the Java I/O inheritance model: it extends InputStream and is associated laterally with FileDescriptor and FileChannel. This shows that it is both an entry point for traditional blocking I/O and a bridge to NIO file channels.

The FileInputStream source design centers on handle management

During construction, it first validates the path and read permissions, then creates a FileDescriptor, and finally opens the file through the native open0 method. This flow reflects a three-layer separation of responsibility: the Java layer handles parameter validation and security checks, FileDescriptor manages the handle lifecycle, and the native layer performs the actual system call.

try (FileInputStream fis = new FileInputStream("D:\\nio-data.txt")) {
    System.out.println(fis.available()); // Check how many bytes are currently available to read
    fis.skip(5); // Skip the first 5 bytes
    System.out.println(fis.available()); // Check the remaining bytes again
}

This example demonstrates the sequential read behavior of FileInputStream and the common combination of skip and available.

FileInputStream reflects three classic design ideas

The first is the Template Method pattern. InputStream defines the abstract read contract, and FileInputStream only provides the concrete implementation. The second is the Adapter pattern: native methods such as read0 and close0 adapt different operating system calls into a unified Java API. The third is the Proxy pattern: FileDescriptor acts as a proxy for the system file handle and centralizes resource cleanup.

public int read() throws IOException {
    return read0(); // Call the native method to perform the actual read
}

This shows that the public API of FileInputStream is intentionally thin, while the core read/write capability actually lives in the collaboration layer between the JVM and the operating system.

RandomAccessFile provides position-aware file read/write semantics

RandomAccessFile supports four modes: r, rw, rws, and rwd. The difference is not in the API itself, but in whether file data and metadata are forcibly synchronized to the underlying storage device. For local disk files, this directly affects the trade-off between performance and reliability.

  • r: Read-only; does not create the file.
  • rw: Read/write; optimized for performance by default.
  • rws: Synchronize both content and metadata to disk.
  • rwd: Synchronize only content to disk.

seek, length, and getFilePointer form the core random-access loop

seek moves the file pointer, getFilePointer returns the current position, and length returns the file size. With this trio, RandomAccessFile can support resumable writes, index-based positioning, partial overwrites, and segmented copying.

try (RandomAccessFile raf = new RandomAccessFile("D:\\data.bin", "rw")) {
    raf.seek(8); // Move the file pointer to byte 8
    raf.writeInt(1666688888); // Write an int at the current position
    raf.seek(8); // Return to the same position
    int value = raf.readInt(); // Read data from that position
    System.out.println(value);
}

This example shows the most important capability of RandomAccessFile: precise, position-based reads and writes within the same file.

RandomAccessFile primitive I/O relies on Big-Endian byte order

By default, the JDK writes data in Big-Endian order, with the most significant byte first and the least significant byte last. For example, writeInt splits one int into four bytes and writes them in sequence; readInt reads them back in the same order and reconstructs the value using left shifts. This gives binary formats a stable, cross-platform interpretation model.

public final void writeInt(int v) throws IOException {
    write((v >>> 24) & 0xFF); // Write the highest 8 bits
    write((v >>> 16) & 0xFF); // Write the next highest 8 bits
    write((v >>> 8) & 0xFF);  // Write the next lowest 8 bits
    write((v >>> 0) & 0xFF);  // Write the lowest 8 bits
}

This source snippet is equivalent to manual byte packing and is a key entry point for understanding Java binary persistence formats.

Multithreaded segmented copying is a direct use case for RandomAccessFile

When a file is large enough, you can split the source file into ranges, let different threads seek to different starting offsets, and then write to the corresponding offsets in the destination file. This can improve throughput by leveraging disk and thread concurrency, but it also introduces additional handle and seek overhead.

try (RandomAccessFile src = new RandomAccessFile(source, "r");
     RandomAccessFile dst = new RandomAccessFile(target, "rw")) {
    src.seek(offset); // Each thread positions itself at its own starting block
    dst.seek(offset); // Position the destination file to the same offset
    byte[] buf = new byte[20 * 1024 * 1024];
    int len = src.read(buf); // Read the segment data
    if (len > 0) {
        dst.write(buf, 0, len); // Write the target segment
    }
}

This approach works well for large-file copying, resumable transfers, and fixed-block data processing, but it is not a good fit for highly contended small-file workloads.

Adding a buffer to RandomAccessFile can significantly reduce I/O calls

A native RandomAccessFile may hit the underlying storage on every read or write call, which becomes expensive for small-granularity access. Extending it into a BufferedRandomAccessFile essentially converts frequent small I/O operations into batched block I/O, thereby reducing the number of system calls.

A custom implementation typically maintains a buffer, the current position cPos, buffer boundaries lo/hi, and the disk position diskPos, then flushes changes on flush or close. This strategy is especially useful for sequential reads/writes, log archiving, and large-file sequential copying.

public void flush() throws IOException {
    if (hasDatas) {
        super.seek(lo_); // First move to the underlying file offset
        super.write(buffer, 0, (int)(cPos - lo_)); // Flush buffered data in one batch
        hasDatas = false; // Mark the cache as persisted
    }
}

The purpose of this logic is to merge multiple in-memory modifications into a single physical disk write.

The right choice depends on the access pattern, not API familiarity

If your requirement is to sequentially read regular files, prefer FileInputStream, optionally combined with BufferedInputStream when needed. If your requirement includes seek, overwrite writes, primitive-type reads/writes, or segmented processing, use RandomAccessFile. If access is frequent and very fine-grained, add a buffering layer or move directly to FileChannel/NIO.

FAQ

How should I choose between FileInputStream and FileReader?

FileInputStream is byte-oriented and is suitable for binary data such as images, audio, and compressed archives. FileReader is character-oriented and is suitable for text files. If text processing involves character encoding, prefer InputStreamReader so you can specify the charset explicitly.

Why can’t RandomAccessFile replace all input/output streams?

Its strength is random access and primitive-type I/O, but it does not follow the decorator-based stream hierarchy. Unlike BufferedInputStream, it cannot be wrapped layer by layer in the same flexible way. If your application is centered on stream processing, the traditional stream model is usually more flexible.

What is the value of a custom BufferedRandomAccessFile?

Its value lies in reducing the number of underlying I/O operations and improving performance for continuous small-block reads and writes. However, it also increases implementation complexity. You must handle flush, seek, consistency, and abnormal shutdown correctly, or you risk data loss and positioning errors.

Core Summary: This article reconstructs and analyzes FileInputStream and RandomAccessFile in JDK 8 on Windows, focusing on native I/O, FileDescriptor, FileChannel integration, random read/write behavior, Big-Endian byte order, and custom buffering optimization, along with practical examples and implementation-oriented selection guidance.