Introduction
File Input/Output (I/O) streams in Java provide a low-level, byte-oriented mechanism for reading from and writing to files. Unlike higher-level readers and writers (e.g., BufferedReader, PrintWriter), streams operate directly on raw bytes, making them suitable for handling binary data such as images, audio, video, or serialized objects. Java’s stream-based I/O is built on a layered architecture of abstract classes and concrete implementations in the java.io package. Understanding file streams is essential for efficient, flexible, and robust file handling—especially when performance, binary data, or fine-grained control is required.
1. Core Stream Classes
Java provides two main hierarchies for file I/O:
A. InputStream (for reading)
- Abstract base class for all byte-input streams.
- Key subclass:
FileInputStream
B. OutputStream (for writing)
- Abstract base class for all byte-output streams.
- Key subclass:
FileOutputStream
Note: These are byte streams, not character streams. For text, use
Reader/Writerinstead.
2. FileInputStream
Used to read raw bytes from a file.
Constructors
// From file path (String) FileInputStream fis = new FileInputStream("data.bin"); // From File object File file = new File("data.bin"); FileInputStream fis = new FileInputStream(file); Key Methods
| Method | Description |
|---|---|
int read() | Reads a single byte (0–255) or -1 if EOF |
int read(byte[] b) | Reads up to b.length bytes into array |
int read(byte[] b, int off, int len) | Reads up to len bytes into b starting at off |
void close() | Closes the stream and releases resources |
Example: Reading a Binary File
try (FileInputStream fis = new FileInputStream("image.jpg")) { byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { // Process buffer (e.g., write to another stream) System.out.println("Read " + bytesRead + " bytes"); } } catch (IOException e) { e.printStackTrace(); } Best Practice: Always use try-with-resources to ensure streams are closed.
3. FileOutputStream
Used to write raw bytes to a file.
Constructors
// Overwrite mode FileOutputStream fos = new FileOutputStream("output.bin"); // Append mode FileOutputStream fos = new FileOutputStream("output.bin", true); // From File object FileOutputStream fos = new FileOutputStream(new File("output.bin")); Key Methods
| Method | Description |
|---|---|
void write(int b) | Writes the lower 8 bits of b |
void write(byte[] b) | Writes entire byte array |
void write(byte[] b, int off, int len) | Writes len bytes from b starting at off |
void flush() | Forces buffered bytes to be written (not always needed) |
void close() | Closes the stream |
Example: Writing Binary Data
try (FileOutputStream fos = new FileOutputStream("data.bin")) { String text = "Hello, Binary World!"; byte[] data = text.getBytes(); // Convert to bytes fos.write(data); fos.write('\n'); // Write newline as byte } catch (IOException e) { e.printStackTrace(); } 4. Combining Streams: Copying Files
A common use case is copying one file to another.
Basic Byte-by-Byte Copy (Inefficient)
try (FileInputStream fis = new FileInputStream("source.txt"); FileOutputStream fos = new FileOutputStream("dest.txt")) { int byteRead; while ((byteRead = fis.read()) != -1) { fos.write(byteRead); } } catch (IOException e) { e.printStackTrace(); } Efficient Buffered Copy
try (FileInputStream fis = new FileInputStream("source.jpg"); FileOutputStream fos = new FileOutputStream("copy.jpg")) { byte[] buffer = new byte[8192]; // 8KB buffer int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { fos.write(buffer, 0, bytesRead); } } catch (IOException e) { e.printStackTrace(); } Performance Tip: Use a buffer (typically 4KB–64KB) for large files.
5. Buffered Streams for Performance
Wrap file streams with BufferedInputStream and BufferedOutputStream to reduce system calls.
Example
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("input.bin")); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("output.bin"))) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = bis.read(buffer)) != -1) { bos.write(buffer, 0, bytesRead); } } catch (IOException e) { e.printStackTrace(); } Why Buffer?: Each
read()/write()on a raw stream triggers a system call. Buffering minimizes these calls.
6. Reading/Writing Primitive Data: Data Streams
Use DataInputStream and DataOutputStream to read/write Java primitive types in a portable way.
Example: Writing and Reading Primitives
// Writing try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.dat"))) { dos.writeInt(100); dos.writeDouble(3.14159); dos.writeBoolean(true); } // Reading try (DataInputStream dis = new DataInputStream(new FileInputStream("data.dat"))) { int i = dis.readInt(); // 100 double d = dis.readDouble(); // 3.14159 boolean b = dis.readBoolean(); // true } Note: Data streams use big-endian byte order by default.
7. Object Serialization: Object Streams
Persist Java objects to files using ObjectOutputStream and ObjectInputStream.
Requirements
- Class must implement
Serializable - Non-serializable fields marked
transient
Example
// Serializable class class Person implements Serializable { private static final long serialVersionUID = 1L; String name; int age; Person(String name, int age) { this.name = name; this.age = age; } } // Writing object try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.dat"))) { oos.writeObject(new Person("Alice", 30)); } // Reading object try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.dat"))) { Person p = (Person) ois.readObject(); System.out.println(p.name + ", " + p.age); } Warning: Never serialize sensitive data (e.g., passwords). Use
transientfor such fields.
8. Key Differences: Streams vs. Readers/Writers
| Feature | Streams (InputStream/OutputStream) | Readers/Writers (Reader/Writer) |
|---|---|---|
| Data Type | Raw bytes | Characters (text) |
| Encoding | None (binary) | Uses character encoding (e.g., UTF-8) |
| Use Case | Binary files (images, audio, objects) | Text files (CSV, logs, config) |
| Example | FileInputStream, FileOutputStream | FileReader, FileWriter |
Rule of Thumb:
- Use streams for binary data.
- Use readers/writers for text.
9. Best Practices
- Always use try-with-resources to auto-close streams.
- Buffer large I/O operations (4KB–64KB buffer size).
- Prefer NIO.2 (
Filesclass) for simple file operations (e.g.,Files.copy()). - Handle
IOExceptionappropriately—don’t ignore it. - Specify encoding explicitly when converting between bytes and text:
String text = new String(bytes, StandardCharsets.UTF_8); byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
- Avoid
available()for determining file size—it’s unreliable for many stream types.
10. Common Pitfalls
- Not closing streams: Causes resource leaks (file handles remain open).
- Using streams for text without encoding: Leads to mojibake (garbled text).
- Assuming
read()fills the entire buffer: It may return fewer bytes. - Ignoring return value of
read(): Always check for-1(EOF). - Mixing text and binary streams: Never wrap a
FileReaderaround aFileInputStream.
Conclusion
File I/O streams form the foundation of Java’s input/output system, offering direct, efficient access to binary data. While higher-level utilities like Files and Scanner simplify common tasks, streams provide the flexibility needed for performance-critical or binary-intensive applications. By understanding the core classes (FileInputStream, FileOutputStream), leveraging buffering, and combining streams for complex operations (e.g., object serialization), developers can handle any file I/O requirement. Always prioritize resource management with try-with-resources, choose the right stream type for your data (binary vs. text), and follow best practices to build robust, efficient file-handling code. Whether processing images, serializing objects, or copying large files, mastering streams is essential for professional Java development.