Java RandomAccessFile: Seek, Read and Write at Arbitrary Positions

Sequential streams like FileInputStream and BufferedReader read a file from start to finish: you cannot jump to position 500 and read from there. RandomAccessFile breaks that restriction: it maintains a file pointer that you can move to any byte offset with seek(), then read or write data at that exact position. This makes it the right tool for binary file formats with fixed-length records, in-place updates, and file headers. This article covers the API, the open modes, typed reads/writes, fixed-length record files, and when to choose FileChannel instead.

What RandomAccessFile Is and When to Use It

Sequential I/O classes (FileInputStream, BufferedReader, Files.lines()) are designed for reading a file from beginning to end. Each read advances an implicit cursor forward. There is no way to jump backwards or skip to a specific byte without closing and reopening the file and reading past the bytes you want to skip.

RandomAccessFile works differently. It exposes a file pointer, a 64-bit offset from the start of the file, and lets you move it freely with seek(long pos). Every read or write advances the pointer by the number of bytes consumed; a seek lets you reposition it anywhere. This maps directly to how operating systems expose files as byte arrays with a random-access interface.

Use RandomAccessFile for: binary file formats with fixed-size records (a simple database, an index file, sensor log files); in-place updates (changing one field without rewriting the whole file); reading a file header at offset 0 then jumping to a data section at offset 1024; implementing append-only logs with an in-file index. For high-throughput use cases, prefer FileChannel (which has a position() method and supports memory-mapped I/O), but RandomAccessFile is simpler and adequate for most non-performance-critical needs.

Opening a RandomAccessFile: Modes

RandomAccessFile takes a file path (or File object) and a mode string as constructor arguments. The mode controls read/write access and synchronisation. If the file does not exist and the mode includes write access, the file is created automatically.

  • "r": read-only. Writing throws IOException.
  • "rw": read and write. Creates the file if it does not exist.
  • "rws": read/write with synchronous content+metadata flush to storage after every write. Equivalent to StandardOpenOption.SYNC.
  • "rwd": read/write with synchronous content flush only (metadata updates may be buffered). Slightly faster than "rws" when metadata durability is not required.
Java
import java.io.RandomAccessFile;
import java.io.IOException;

// Open for reading only
try (RandomAccessFile raf = new RandomAccessFile("data.bin", "r")) {
    System.out.println("File length: " + raf.length() + " bytes");
    System.out.println("Current pointer: " + raf.getFilePointer()); // 0
}

// Open for reading and writing (creates file if absent)
try (RandomAccessFile raf = new RandomAccessFile("output.bin", "rw")) {
    raf.writeInt(42);
    raf.writeDouble(3.14);
    raf.writeUTF("Hello");

    System.out.println("After writes, pointer at: " + raf.getFilePointer());
    System.out.println("File length: " + raf.length());

    // Seek back to the start and read back
    raf.seek(0);
    System.out.println(raf.readInt());    // 42
    System.out.println(raf.readDouble()); // 3.14
    System.out.println(raf.readUTF());    // Hello
}
RandomAccessFile implements Closeable, so always use try-with-resources or call close() explicitly. Failing to close leaves an OS file handle open: most operating systems have a per-process limit of open file handles (typically 1024–4096).

Fixed-Length Record Files

The most powerful use of RandomAccessFile is working with fixed-length records. When every record in a file is exactly the same number of bytes, you can calculate the byte offset of any record by index: offset = index × recordSize. Reading record N, updating it in-place, or deleting it (by zeroing the bytes) are all O(1) operations: no scanning the entire file needed.

This is the foundation of simple binary databases and index files. The trade-off is that variable-length fields (strings of arbitrary length) must be padded or stored indirectly: a fixed-length record file typically stores strings in fixed-size byte arrays, truncating or padding with null bytes as needed.

Java
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public class PersonRecord {
    // Fixed-length record layout:
    //   id     : 4 bytes  (int)
    //   age    : 4 bytes  (int)
    //   name   : 32 bytes (fixed-length padded UTF-8)
    // Total    : 40 bytes per record

    static final int RECORD_SIZE = 40;
    static final int NAME_BYTES   = 32;

    static void writeRecord(RandomAccessFile raf, int index,
                            int id, int age, String name) throws IOException {
        raf.seek((long) index * RECORD_SIZE);
        raf.writeInt(id);
        raf.writeInt(age);

        // Write name as fixed 32-byte field (pad with 0s)
        byte[] nameBytes = Arrays.copyOf(
            name.getBytes(StandardCharsets.UTF_8), NAME_BYTES);
        raf.write(nameBytes);
    }

    static String readName(RandomAccessFile raf, int index) throws IOException {
        raf.seek((long) index * RECORD_SIZE + 8); // skip id (4) + age (4)
        byte[] nameBytes = new byte[NAME_BYTES];
        raf.readFully(nameBytes);
        // Trim trailing null bytes
        int len = 0;
        while (len < NAME_BYTES && nameBytes[len] != 0) len++;
        return new String(nameBytes, 0, len, StandardCharsets.UTF_8);
    }

    public static void main(String[] args) throws IOException {
        try (RandomAccessFile raf = new RandomAccessFile("persons.bin", "rw")) {
            // Write 3 records
            writeRecord(raf, 0, 1, 30, "Alice");
            writeRecord(raf, 1, 2, 25, "Bob");
            writeRecord(raf, 2, 3, 35, "Carol");

            // Read record 1 (Bob) directly — O(1) access
            System.out.println("Record 1 name: " + readName(raf, 1)); // Bob

            // Update record 0 (Alice) age in-place
            raf.seek(0L * RECORD_SIZE + 4); // skip id, position at age
            raf.writeInt(31);

            // Read record 2 (Carol) name
            System.out.println("Record 2 name: " + readName(raf, 2)); // Carol

            System.out.println("Total records: " + raf.length() / RECORD_SIZE); // 3
        }
    }
}

RandomAccessFile vs FileChannel: When to Choose Each

RandomAccessFile is straightforward but has limitations for high-performance scenarios. It is not buffered: every read and write goes directly to the OS (though the OS has its own page cache). It does not support memory-mapped I/O directly. And it uses blocking I/O, which is fine for most uses but unsuitable for non-blocking NIO selectors.

FileChannel (obtained via RandomAccessFile.getChannel() or FileChannel.open()) offers the same random-access capability plus memory-mapped files (map()), scatter/gather I/O, zero-copy transferTo()/transferFrom(), and NIO integration. For anything performance-critical (log writers, database storage engines, large binary file processing) prefer FileChannel.

Java
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.*;

// FileChannel equivalent of RandomAccessFile random access
try (FileChannel fc = FileChannel.open(
        Path.of("data.bin"),
        StandardOpenOption.READ, StandardOpenOption.WRITE,
        StandardOpenOption.CREATE)) {

    // Write at position 0
    ByteBuffer buf = ByteBuffer.allocate(8);
    buf.putInt(42).putDouble(3.14).flip(); // ← flip to switch from write to read mode...
    // Actually for separate writes:
    ByteBuffer ibuf = ByteBuffer.allocate(4);
    ibuf.putInt(42);
    ibuf.flip();
    fc.write(ibuf, 0);  // write int at position 0

    // Read back at position 0
    ibuf.clear();
    fc.read(ibuf, 0);
    ibuf.flip();
    System.out.println("Value: " + ibuf.getInt()); // 42

    // Get a RandomAccessFile's channel
    try (RandomAccessFile raf = new RandomAccessFile("existing.bin", "rw");
         FileChannel channel = raf.getChannel()) {
        // now use channel for high-throughput I/O
        System.out.println("Channel size: " + channel.size());
    }
}
FeatureRandomAccessFileFileChannel
Seek to arbitrary position✅ seek()✅ position()
Typed read/write (int, double, UTF)✅ Built-in❌ Use ByteBuffer manually
Memory-mapped files❌✅ map()
Zero-copy transfer❌✅ transferTo/From
NIO non-blocking❌✅
SimplicityHighMedium