RandomAccessFile
Most file APIs force you to start at the beginning and work forward — but sometimes you need to jump straight to byte 10 000, update four bytes, and leave the rest untouched. RandomAccessFile gives you exactly that power: a single object that can both read and write any position in a file using an internal file pointer you control directly.
What Is RandomAccessFile?
RandomAccessFile lives in java.io. Unlike the stream classes, it does not extend InputStream or OutputStream — it implements both DataInput and DataOutput directly, bundling read and write support into one class. Think of it as treating a file like a big byte array you can index into at will.
Key characteristics at a glance:
| Feature | RandomAccessFile |
|---|---|
| Extends | Object |
| Implements | DataInput, DataOutput, Closeable |
| Direction | Read, write, or both |
| Position control | seek(long pos) |
| Default pointer | 0 (start of file) |
| Thread safety | Not thread-safe |
Note: For pure sequential reads or writes, prefer FileInputStream / FileOutputStream wrapped in a BufferedInputStream.
RandomAccessFileshines when you genuinely need non-sequential access.
Opening a RandomAccessFile
The constructor takes a file path (or a File object) and a mode string:
import java.io.RandomAccessFile;
public class OpenExample {
public static void main(String[] args) throws Exception {
// "r" — read-only
RandomAccessFile reader = new RandomAccessFile("data.bin", "r");
// "rw" — read and write; creates the file if it doesn't exist
RandomAccessFile rw = new RandomAccessFile("data.bin", "rw");
reader.close();
rw.close();
}
}
Mode options:
| Mode | Meaning |
|---|---|
"r" | Read-only. Throws FileNotFoundException if the file doesn’t exist. |
"rw" | Read/write. Creates the file if it doesn’t exist. |
"rws" | Read/write + sync file content and metadata to storage on every write. |
"rwd" | Read/write + sync file content (but not metadata) on every write. |
"rws" and "rwd" are useful for durability-critical code (e.g., write-ahead logs) but are much slower because every write flushes to disk.
The File Pointer
Every RandomAccessFile maintains a file pointer — an offset (in bytes from the start of the file) indicating where the next read or write will happen. After each read or write operation the pointer advances automatically by the number of bytes transferred.
import java.io.RandomAccessFile;
public class PointerDemo {
public static void main(String[] args) throws Exception {
try (RandomAccessFile raf = new RandomAccessFile("demo.dat", "rw")) {
System.out.println("Initial pointer: " + raf.getFilePointer()); // 0
raf.writeInt(42); // writes 4 bytes
System.out.println("After writeInt: " + raf.getFilePointer()); // 4
raf.seek(0); // jump back to start
System.out.println("After seek(0): " + raf.getFilePointer()); // 0
int value = raf.readInt();
System.out.println("Read value: " + value); // 42
}
}
}
Output:
Initial pointer: 0
After writeInt: 4
After seek(0): 0
Read value: 42
Two essential pointer methods:
long getFilePointer()— returns the current byte offset.void seek(long pos)— moves the pointer topos. You can seek past the end of file; writing there extends the file.
Writing Data
Because RandomAccessFile implements DataOutput, it has typed write methods that match the primitive data types exactly:
import java.io.RandomAccessFile;
public class WriteExample {
public static void main(String[] args) throws Exception {
try (RandomAccessFile raf = new RandomAccessFile("record.dat", "rw")) {
raf.writeInt(1001); // employee ID — 4 bytes
raf.writeDouble(75_000.50); // salary — 8 bytes
raf.writeBoolean(true); // active flag — 1 byte
raf.writeUTF("Alice"); // name — 2 + chars bytes
}
System.out.println("Written successfully.");
}
}
Output:
Written successfully.
Tip:
writeUTFprepends a 2-byte length, making it easy to read back withreadUTF. If you need fixed-width records (important for random access!), write names as fixed-length byte arrays instead.
Reading Data
The symmetric DataInput methods read back exactly what was written:
import java.io.RandomAccessFile;
public class ReadExample {
public static void main(String[] args) throws Exception {
try (RandomAccessFile raf = new RandomAccessFile("record.dat", "r")) {
int id = raf.readInt();
double salary = raf.readDouble();
boolean active = raf.readBoolean();
String name = raf.readUTF();
System.out.println("ID: " + id);
System.out.println("Salary: " + salary);
System.out.println("Active: " + active);
System.out.println("Name: " + name);
}
}
}
Output:
ID: 1001
Salary: 75000.5
Active: true
Name: Alice
Updating a Record In-Place
This is where RandomAccessFile truly earns its name. Given a fixed-record file, you can jump to any record and overwrite it without touching the surrounding data:
import java.io.RandomAccessFile;
public class UpdateRecord {
// Each record: int (4) + double (8) = 12 bytes (ignoring name for simplicity)
private static final int RECORD_SIZE = 12;
public static void main(String[] args) throws Exception {
// Write three simple records
try (RandomAccessFile raf = new RandomAccessFile("employees.dat", "rw")) {
writeRecord(raf, 1, 50_000.0);
writeRecord(raf, 2, 60_000.0);
writeRecord(raf, 3, 70_000.0);
}
// Update record 2's salary to 65_000.0
try (RandomAccessFile raf = new RandomAccessFile("employees.dat", "rw")) {
long offset = (long) (2 - 1) * RECORD_SIZE; // zero-based index
raf.seek(offset + 4); // skip the int ID (4 bytes)
raf.writeDouble(65_000.0);
}
// Read back all three records
try (RandomAccessFile raf = new RandomAccessFile("employees.dat", "r")) {
for (int i = 0; i < 3; i++) {
int id = raf.readInt();
double salary = raf.readDouble();
System.out.println("ID " + id + " -> $" + salary);
}
}
}
private static void writeRecord(RandomAccessFile raf, int id, double salary)
throws Exception {
raf.writeInt(id);
raf.writeDouble(salary);
}
}
Output:
ID 1 -> $50000.0
ID 2 -> $65000.0
ID 3 -> $70000.0
Notice record 2 was updated surgically — records 1 and 3 were never rewritten.
Useful Methods at a Glance
| Method | Description |
|---|---|
seek(long pos) | Move file pointer to pos |
getFilePointer() | Return current pointer position |
length() | Return file length in bytes |
setLength(long len) | Truncate or extend the file |
read() | Read a single byte (-1 at EOF) |
read(byte[] b) | Fill byte array from current position |
readFully(byte[] b) | Read exactly b.length bytes or throw |
skipBytes(int n) | Advance pointer by n bytes |
readLine() | Read a line as a String (ASCII only) |
Warning:
readLine()inRandomAccessFilereads bytes and converts them as ISO-8859-1. For proper Unicode line reading, use BufferedReader instead.
Under the Hood
At the OS level, RandomAccessFile maps to a single file descriptor opened with both read and write flags. Calls to seek() translate directly to the lseek system call (or SetFilePointer on Windows), making position changes O(1) — there is no data copying or file rewinding happening.
The typed readInt(), readDouble() etc. read the appropriate number of bytes and reassemble them in big-endian (network byte order) format, regardless of the host CPU. This guarantees that a file written on an x86 machine reads correctly on ARM or any other architecture — important for cross-platform binary formats.
Buffering: RandomAccessFile does not have its own application-level buffer (unlike BufferedInputStream). Every read/write goes through a thin JNI layer to the OS. The OS page cache does buffer at the kernel level, so sequential access is still reasonably fast. If you are doing many small reads at adjacent positions, batching them into readFully(byte[]) reduces JNI overhead significantly.
Extending the file: Seeking past the end of file and writing there causes the file to grow. On most systems the gap between the old end and the new write is filled with zero bytes (a sparse file on Linux/NTFS, an actual allocation on older filesystems).
NIO alternative: java.nio.channels.FileChannel (accessible via FileChannel.open() or fileInputStream.getChannel()) offers the same random-access capability with better performance for large transfers, memory-mapped I/O (map()), and explicit locking. For new code that needs heavy random I/O, consider FileChannel — see NIO.2: Path & Files for a starting point.
Common Pitfalls
- Variable-length records make seeking by index impossible. Either use fixed-length fields or maintain a separate index structure that stores each record’s byte offset.
- Forgetting to seek before reading back — after a write the pointer sits at the end of what you just wrote. Always
seek(0)or seek to the target offset before reading. - Not closing the file —
RandomAccessFileholds a native file descriptor. Always use try-with-resources. - Concurrent access —
RandomAccessFilehas no built-in synchronization. In multi-threaded code, coordinate access externally or useFileChannelwithFileLock.
Related Topics
- File Handling — overview of all file I/O options in Java
- FileInputStream — sequential byte reading from a file
- FileOutputStream — sequential byte writing to a file
- NIO.2: Path & Files — the modern NIO.2 API, including
FileChannelfor high-performance random access - Serialization — writing entire objects to a file rather than individual primitives
- DataInputStream — the
DataInputinterface in sequential stream form