FileInputStream
FileInputStream is a concrete InputStream subclass that reads raw bytes from a file on the file system. It is one of the most fundamental I/O classes in Java, providing a direct connection between the Java byte stream model and the OS file descriptor for reading. FileInputStream opens a file, holds an OS file descriptor for it, and reads bytes by delegating to the OS read() system call. Its read() methods are direct I/O — unbuffered — making BufferedInputStream an essential wrapper for any usage pattern that reads more than a few bytes. FileInputStream can be constructed from a String path, a File object, or (less commonly) an existing FileDescriptor. It provides the standard InputStream API plus getChannel() for NIO integration and getFD() for low-level file descriptor access. This entry covers FileInputStream construction and its open-file resource contract, the performance implications of unbuffered reading, reading specific file types (binary, structured, large), the getChannel() bridge to FileChannel and memory-mapped I/O, the complete interaction between FileInputStream and NIO.2, common mistakes including the partial-read bug, and when to use NIO.2's Files methods instead.
Construction, the File Descriptor Contract, and Buffering
// ── Three constructors ────────────────────────────────────────────────
// From String path:
FileInputStream fis1 = new FileInputStream("/home/user/data.bin");
// From File object:
File f = new File("/home/user/data.bin");
FileInputStream fis2 = new FileInputStream(f);
// From FileDescriptor (rare — usually for stdin):
FileInputStream fromStdin = new FileInputStream(FileDescriptor.in);
// ── FileNotFoundException — thrown at construction ─────────────────────
try {
FileInputStream missing = new FileInputStream("/nonexistent/path/file.bin");
} catch (FileNotFoundException e) {
System.err.println("File not found: " + e.getMessage());
// Message includes path — useful for diagnosis
}
// FileNotFoundException for directory:
try {
FileInputStream dir = new FileInputStream("/home/user"); // Is a directory
} catch (FileNotFoundException e) {
System.err.println("Cannot read directory: " + e.getMessage());
}
// ── Always use try-with-resources — file descriptor leak without it ────
// WRONG: no close() call — file descriptor leaked if exception occurs
FileInputStream leaky = new FileInputStream("data.bin");
int b = leaky.read();
leaky.close(); // not called if read() throws
// CORRECT: try-with-resources guarantees close() even on exception:
try (FileInputStream fis = new FileInputStream("data.bin")) {
int b2 = fis.read();
} // fis.close() always called here
// ── Unbuffered reading performance — always wrap in BufferedInputStream
// Unbuffered: one system call per byte
long start = System.nanoTime();
try (FileInputStream fis = new FileInputStream("1mb.bin")) {
while (fis.read() != -1) {} // 1,000,000 individual read() calls
}
System.out.printf("Unbuffered: %.0f ms%n", (System.nanoTime()-start)/1e6); // ~500ms
// Buffered: one system call per 8KB
start = System.nanoTime();
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("1mb.bin"))) {
while (bis.read() != -1) {} // reads fill 8KB buffer; ~128 system calls total
}
System.out.printf("Buffered: %.0f ms%n", (System.nanoTime()-start)/1e6); // ~5ms
// Array-based read with buffer:
start = System.nanoTime();
try (FileInputStream fis = new FileInputStream("1mb.bin")) {
byte[] buf = new byte[65536];
while (fis.read(buf) != -1) {} // ~16 system calls for 1MB
}
System.out.printf("Chunked: %.0f ms%n", (System.nanoTime()-start)/1e6); // ~2ms
// ── NIO.2 alternatives for simple cases ──────────────────────────────
// Small file: read entire content at once (cleaner than FileInputStream)
byte[] content = Files.readAllBytes(Path.of("config.json")); // ~1 line, no resource mgmt
// Medium file: stream with buffering (explicit control)
try (BufferedInputStream bis = new BufferedInputStream(
Files.newInputStream(Path.of("data.bin")))) {
// Files.newInputStream() returns an InputStream backed by FileChannel
}
// Large file: avoid loading all into memory
try (FileInputStream fis = new FileInputStream("large.bin");
BufferedInputStream bis = new BufferedInputStream(fis, 65536)) {
byte[] chunk = new byte[65536];
int n;
while ((n = bis.read(chunk)) != -1) {
processChunk(chunk, n);
}
}Binary File Reading, Partial Read Bug, and Structured Data
// ── Partial read bug — the most common FileInputStream mistake ────────
byte[] header = new byte[16];
// WRONG: assumes read() fills the entire buffer
try (FileInputStream fis = new FileInputStream("protocol.bin")) {
fis.read(header); // reads up to 16 bytes — may read fewer!
int version = (header[0] << 8) | (header[1] & 0xFF); // may be wrong if partial read
}
// CORRECT option 1: loop until full
try (FileInputStream fis = new FileInputStream("protocol.bin")) {
int totalRead = 0;
while (totalRead < header.length) {
int n = fis.read(header, totalRead, header.length - totalRead);
if (n == -1) throw new EOFException("File too short: expected 16 bytes header");
totalRead += n;
}
int version = ((header[0] & 0xFF) << 8) | (header[1] & 0xFF);
}
// CORRECT option 2: DataInputStream.readNBytes() (Java 11+)
try (DataInputStream dis = new DataInputStream(new FileInputStream("protocol.bin"))) {
int n = dis.readNBytes(header, 0, header.length);
if (n < header.length) throw new EOFException("File too short");
}
// CORRECT option 3: DataInputStream.readFully() (available since Java 1.0)
try (DataInputStream dis = new DataInputStream(
new BufferedInputStream(new FileInputStream("protocol.bin")))) {
dis.readFully(header); // throws EOFException if stream ends before header.length bytes
int version = dis.readShort() & 0xFFFF; // unsigned short — big-endian
}
// ── Structured binary format reading ──────────────────────────────────
// PNG file header parsing example:
try (DataInputStream dis = new DataInputStream(
new BufferedInputStream(new FileInputStream("image.png")))) {
// PNG magic bytes: 137 80 78 71 13 10 26 10
byte[] magic = new byte[8];
dis.readFully(magic);
if (magic[0] != (byte)0x89 || magic[1] != 'P' || magic[2] != 'N' || magic[3] != 'G') {
throw new IOException("Not a PNG file");
}
// First chunk: IHDR
int chunkLength = dis.readInt(); // 4 bytes big-endian — always 13 for IHDR
byte[] chunkType = new byte[4];
dis.readFully(chunkType);
String type = new String(chunkType, StandardCharsets.US_ASCII); // "IHDR"
// IHDR data:
int width = dis.readInt();
int height = dis.readInt();
byte bitDepth = dis.readByte();
byte colorType = dis.readByte();
byte compression = dis.readByte();
byte filter = dis.readByte();
byte interlace = dis.readByte();
int crc = dis.readInt(); // CRC32 of type+data
System.out.printf("PNG: %dx%d, bitDepth=%d, colorType=%d%n",
width, height, bitDepth, colorType);
}
// ── Memory-mapped I/O via getChannel() — for very large files ──────────
try (FileInputStream fis = new FileInputStream("large-database.bin");
FileChannel channel = fis.getChannel()) {
long fileSize = channel.size();
System.out.printf("File size: %.2f GB%n", fileSize / 1e9);
// Map entire file into virtual memory (efficient up to ~2GB per map):
MappedByteBuffer mapped = channel.map(
FileChannel.MapMode.READ_ONLY, 0, Math.min(fileSize, Integer.MAX_VALUE));
// Read directly from virtual memory — no copy through heap:
mapped.order(ByteOrder.LITTLE_ENDIAN); // many binary formats are little-endian
int magic = mapped.getInt(); // reads 4 bytes at current position
long timestamp = mapped.getLong();
System.out.printf("Magic: 0x%08X Timestamp: %d%n", magic, timestamp);
// Random access: seek to specific offset
mapped.position(1024); // seek to byte 1024
byte recordType = mapped.get(); // read one byte
// For files > 2GB: map in chunks
long chunkSize = 512L * 1024 * 1024; // 512MB chunks
for (long offset = 0; offset < fileSize; offset += chunkSize) {
long len = Math.min(chunkSize, fileSize - offset);
MappedByteBuffer chunk = channel.map(FileChannel.MapMode.READ_ONLY, offset, len);
processChunkBuffer(chunk);
}
}
// ── getFD() — accessing the raw file descriptor ───────────────────────
try (FileInputStream fis = new FileInputStream("data.bin")) {
FileDescriptor fd = fis.getFD();
fd.sync(); // flush OS kernel buffer to disk hardware (fsync)
// Rarely needed for reads; useful when FD must be passed to native code
System.out.println("FD valid: " + fd.valid());
}Reading Text Files, Skip, Available, and Complete Usage Patterns
// ── Text reading — never use FileInputStream directly for text ────────
// WRONG: platform-dependent charset, byte-level reading
try (FileInputStream fis = new FileInputStream("document.txt")) {
byte[] bytes = fis.readAllBytes();
String text = new String(bytes); // uses platform default charset — WRONG
}
// CORRECT: explicit charset with InputStreamReader bridge
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("document.txt"), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
processLine(line);
}
}
// BEST for text files (Java 11+):
String text = Files.readString(Path.of("document.txt"), StandardCharsets.UTF_8);
// Or for line-by-line:
try (Stream<String> lines = Files.lines(Path.of("document.txt"), StandardCharsets.UTF_8)) {
lines.forEach(this::processLine);
}
// ── available() — meaningful for FileInputStream ──────────────────────
try (FileInputStream fis = new FileInputStream("data.bin")) {
int remainingBytes = fis.available(); // = total file size (at start)
System.out.printf("File size: %,d bytes%n", remainingBytes);
// Pre-size buffer based on available() — safe for FileInputStream:
byte[] buffer = new byte[remainingBytes];
// But readAllBytes() is simpler and more correct:
byte[] allData = fis.readAllBytes();
}
// ── skip() — reliable skipping ────────────────────────────────────────
try (FileInputStream fis = new FileInputStream("data.bin")) {
// Skip 1024-byte header:
long skipped = 0;
while (skipped < 1024) {
long n = fis.skip(1024 - skipped);
if (n <= 0) throw new EOFException("File too short to skip 1024 bytes");
skipped += n;
}
// Now read the data after the header:
byte[] data = fis.readAllBytes();
}
// ── Complete binary file reading patterns ─────────────────────────────
// Pattern 1: small binary file — read all bytes at once:
byte[] smallFile = Files.readAllBytes(Path.of("config.bin"));
ByteBuffer bb = ByteBuffer.wrap(smallFile).order(ByteOrder.BIG_ENDIAN);
int magic = bb.getInt();
int version = bb.getShort() & 0xFFFF;
// Process the whole file from memory — simplest and most correct
// Pattern 2: large binary file with records — streaming:
try (DataInputStream dis = new DataInputStream(
new BufferedInputStream(new FileInputStream("records.bin"), 65536))) {
// Read file header:
byte[] fileHeader = new byte[32];
dis.readFully(fileHeader); // guaranteed: throws EOFException if < 32 bytes
int recordCount = ByteBuffer.wrap(fileHeader).getInt(4);
System.out.println("Records: " + recordCount);
// Read each fixed-size record:
byte[] record = new byte[64];
for (int i = 0; i < recordCount; i++) {
try {
dis.readFully(record); // throws EOFException on truncated file
} catch (EOFException e) {
throw new IOException("File truncated at record " + i, e);
}
processRecord(i, record);
}
}
// Pattern 3: CSV-like text file disguised as "binary" reading — DON'T:
// Some developers use FileInputStream to read text files byte-by-byte and manually
// assemble lines. This is always wrong. Use BufferedReader.readLine() instead.
// Pattern 4: copy one file to another efficiently:
try (FileInputStream src = new FileInputStream("source.bin");
FileOutputStream dest = new FileOutputStream("dest.bin")) {
src.transferTo(dest); // Java 9+: efficient, handles partial reads internally
}
// NIO.2 is cleaner:
Files.copy(Path.of("source.bin"), Path.of("dest.bin"),
StandardCopyOption.REPLACE_EXISTING);