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.