Ch 15.1 Java I/O Overview
In Java, Input/Output (I/O) refers to transferring data between the computer's internal memory and external devices such as the keyboard, monitor, files, and network. Java's java.io package provides a rich set of classes that handle this data transfer in a consistent, unified manner.
1. Node Streams (Source/Destination Streams)
At the core of Java I/O are node streams, which connect directly to a data source (input) or destination (output) such as a file or network socket.
Depending on the type of data they handle, streams are divided into two families:
Byte-based Streams
Transfer data one byte at a time. Can handle all types of data— images, videos, text, binary data.
- Top-level abstract classes:
InputStreamandOutputStream - Common implementations:
FileInputStream/FileOutputStream— file accessByteArrayInputStream/ByteArrayOutputStream— memory buffers
Character-based Streams
Java's char type is 2 bytes. Using byte streams for text can corrupt multi-byte characters (e.g., Korean, Chinese). Character streams are designed exclusively for text data.
- Top-level abstract classes:
ReaderandWriter - Common implementations:
FileReader/FileWriter— text file accessStringReader/StringWriter— string-based I/O
2. The Stream Hierarchy
InputStream (abstract)
├── FileInputStream
├── ByteArrayInputStream
└── FilterInputStream
├── BufferedInputStream
└── DataInputStream
OutputStream (abstract)
├── FileOutputStream
├── ByteArrayOutputStream
└── FilterOutputStream
├── BufferedOutputStream
├── DataOutputStream
└── PrintStream
Reader (abstract)
├── InputStreamReader
│ └── FileReader
├── StringReader
└── BufferedReader
Writer (abstract)
├── OutputStreamWriter
│ └── FileWriter
├── StringWriter
├── BufferedWriter
└── PrintWriter
3. Basic File I/O Example
The most common use of I/O is reading from and writing to files. Resources must always be closed after use with close() to free system handles. Java 7+ introduced try-with-resources to automate this.
import java.io.*;
public class FileIOExample {
public static void main(String[] args) {
// Write a string to a file
try (FileWriter fw = new FileWriter("test.txt")) {
fw.write("Hello, World!\nThis is Java I/O.");
} catch (IOException e) {
e.printStackTrace();
}
// Read the file back character by character
try (FileReader fr = new FileReader("test.txt")) {
int data;
// read() returns -1 when end of file is reached
while ((data = fr.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Streams are unidirectional— input and output each require a separate stream object. You cannot read and write from the same FileInputStream or FileWriter.
4. The Decorator Pattern in Java I/O
Java I/O uses the Decorator Pattern: you wrap a basic (node) stream with one or more filter (wrapper) streams to add functionality.
// Node stream: connects directly to the file
FileInputStream fis = new FileInputStream("data.txt");
// Decorator 1: add buffering for performance
BufferedInputStream bis = new BufferedInputStream(fis);
// Use the outermost (most decorated) stream
int b = bis.read();
Common decorator chains:
// Text file reading with encoding + buffering
BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream("file.txt"),
java.nio.charset.StandardCharsets.UTF_8
)
);
// Text file writing with buffering + printf support
PrintWriter pw = new PrintWriter(
new BufferedWriter(
new FileWriter("output.txt")
)
);
5. try-with-resources — The Essential Pattern
import java.io.*;
public class TryWithResourcesExample {
public static void main(String[] args) {
// Old style: manual close, error-prone
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("file.txt"));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try { br.close(); } catch (IOException e) { e.printStackTrace(); }
}
}
// Modern style: try-with-resources (Java 7+)
// Any AutoCloseable is automatically closed when the block exits
try (BufferedReader br2 = new BufferedReader(new FileReader("file.txt"))) {
String line;
while ((line = br2.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
// Multiple resources in one try-with-resources
try (
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))
) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line.toUpperCase());
writer.newLine();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
6. Byte vs Character Stream Comparison
import java.io.*;
import java.nio.charset.StandardCharsets;
public class ByteVsCharExample {
public static void main(String[] args) throws IOException {
String text = "Hello, Java I/O!";
// Byte stream: write raw bytes
try (FileOutputStream fos = new FileOutputStream("bytes.bin")) {
fos.write(text.getBytes(StandardCharsets.UTF_8));
}
// Byte stream: read raw bytes
try (FileInputStream fis = new FileInputStream("bytes.bin")) {
byte[] buffer = new byte[1024];
int bytesRead = fis.read(buffer);
String result = new String(buffer, 0, bytesRead, StandardCharsets.UTF_8);
System.out.println(result); // Hello, Java I/O!
}
// Character stream: write text directly
try (FileWriter fw = new FileWriter("chars.txt", StandardCharsets.UTF_8)) {
fw.write(text);
}
// Character stream: read text directly
try (FileReader fr = new FileReader("chars.txt", StandardCharsets.UTF_8)) {
char[] buffer = new char[1024];
int charsRead = fr.read(buffer);
System.out.println(new String(buffer, 0, charsRead)); // Hello, Java I/O!
}
}
}
| Aspect | Byte Streams | Character Streams |
|---|---|---|
| Unit | 1 byte at a time | 1-2 chars (Unicode-aware) |
| Use case | Images, audio, binary data | Plain text files |
| Base classes | InputStream, OutputStream | Reader, Writer |
| Encoding | Manual | Automatic (charset specified) |
7. Buffered I/O for Performance
Unbuffered streams access the disk on every read/write call. Buffered streams collect data in memory first, reducing disk access significantly.
import java.io.*;
public class BufferedPerformanceDemo {
public static void main(String[] args) throws IOException {
// Prepare a 1 MB test file
byte[] data = new byte[1024 * 1024];
try (FileOutputStream fos = new FileOutputStream("test_large.bin")) {
fos.write(data);
}
// Unbuffered read: slow (many disk accesses)
long start1 = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream("test_large.bin")) {
while (fis.read() != -1) {} // one byte at a time
}
System.out.println("Unbuffered: " + (System.currentTimeMillis() - start1) + "ms");
// Buffered read: fast (reads large chunks at once)
long start2 = System.currentTimeMillis();
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("test_large.bin"))) {
while (bis.read() != -1) {}
}
System.out.println("Buffered: " + (System.currentTimeMillis() - start2) + "ms");
}
}
8. Java NIO.2 — The Modern Alternative
Java 7 introduced java.nio.file (NIO.2) as a more powerful and concise API for file operations. For new code, prefer NIO.2 over traditional I/O.
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class NioBasicsExample {
public static void main(String[] args) throws Exception {
Path path = Path.of("hello.txt");
// Write a file (one line)
Files.writeString(path, "Hello from NIO.2!\nSecond line.");
// Read entire file as String (small files)
String content = Files.readString(path);
System.out.println(content);
// Read all lines as a List
List<String> lines = Files.readAllLines(path);
lines.forEach(System.out::println);
// File metadata
System.out.println("Size: " + Files.size(path) + " bytes");
System.out.println("Exists: " + Files.exists(path));
// Delete
Files.deleteIfExists(path);
}
}
Pro tip: For modern Java applications:
- Use
java.nio.file.FilesandPathfor most file operations (simpler, more powerful) - Use
BufferedReader/Writerwhen fine-grained control or legacy compatibility is needed - Always specify encoding explicitly (
StandardCharsets.UTF_8) to avoid platform-dependent behavior - Always use
try-with-resources— never skipclose()