Ch 15.2 Filter Streams (Decorator Streams)
Filter streams (also called decorator or auxiliary streams) cannot read or write data on their own. Instead, they wrap another stream to add new capabilities such as buffering, type conversion, or formatted output. This design is a textbook example of the Decorator Pattern.
1. Connecting Filter Streams
To use a filter stream, first create a node stream (connecting to the data source), then pass it to the filter stream's constructor.
import java.io.*;
// Step 1: node stream (connects directly to the file)
FileInputStream fis = new FileInputStream("test.txt");
// Step 2: wrap with a filter stream (adds buffering)
BufferedInputStream bis = new BufferedInputStream(fis);
// Step 3: use the outermost stream's methods
bis.read();
// Closing the outermost stream also closes all inner streams
bis.close();
Streams can be chained through multiple decorator layers:
// Three layers: node + encoding converter + buffering
BufferedReader br = new BufferedReader( // layer 3: line-buffered reading
new InputStreamReader( // layer 2: bytes -> chars (encoding)
new FileInputStream("file.txt"), // layer 1: raw bytes
java.nio.charset.StandardCharsets.UTF_8
)
);
2. Buffered Streams — The Most Essential Filter
Hard disk access is orders of magnitude slower than memory access. Unbuffered streams hit the disk on every single read() or write() call. Buffered streams accumulate data in a memory buffer and flush it in large chunks — often making I/O 10x to 100x faster.
BufferedInputStream / BufferedOutputStream
import java.io.*;
public class BufferedByteStreamExample {
public static void main(String[] args) throws IOException {
// Writing with buffer (default buffer: 8192 bytes)
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("data.bin"))) {
for (int i = 0; i < 1000; i++) {
bos.write(i % 256); // accumulated in memory, not written to disk each time
}
// Automatically flushed and closed at the end of try-with-resources
}
// Reading with buffer
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("data.bin"))) {
int b;
int count = 0;
while ((b = bis.read()) != -1) {
count++;
}
System.out.println("Bytes read: " + count); // 1000
}
// Specify a custom buffer size (32 KB)
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("large.bin"), 32768)) {
byte[] chunk = new byte[1024];
for (int i = 0; i < 100; i++) {
bos.write(chunk);
}
}
}
}
BufferedReader / BufferedWriter
BufferedReader's readLine() method is especially useful — it reads entire lines at once.
import java.io.*;
public class BufferedCharStreamExample {
public static void main(String[] args) throws IOException {
// Write lines efficiently
try (BufferedWriter bw = new BufferedWriter(new FileWriter("lines.txt"))) {
bw.write("First line");
bw.newLine(); // OS-appropriate line separator (\n on Unix, \r\n on Windows)
bw.write("Second line");
bw.newLine();
bw.write("Third line");
}
// Read lines efficiently
try (BufferedReader br = new BufferedReader(new FileReader("lines.txt"))) {
String line;
int lineNum = 1;
while ((line = br.readLine()) != null) { // null = end of file
System.out.printf("Line %d: %s%n", lineNum++, line);
}
}
// BufferedReader.lines() returns a Stream<String> for pipeline processing
try (BufferedReader br = new BufferedReader(new FileReader("lines.txt"))) {
br.lines()
.filter(line -> !line.isBlank())
.map(String::toUpperCase)
.forEach(System.out::println);
}
}
}
3. InputStreamReader / OutputStreamWriter — Byte-to-Character Bridge
When reading bytes from a file or network, but the data represents text, use these streams to convert between byte and character representations with a specified encoding.
import java.io.*;
import java.nio.charset.StandardCharsets;
public class EncodingBridgeExample {
public static void main(String[] args) throws IOException {
// System.in is a byte stream (InputStream).
// Wrap it to get convenient character-based line reading:
BufferedReader console = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8)
);
System.out.print("Enter your name: ");
// String name = console.readLine();
// System.out.println("Hello, " + name + "!");
// Write a text file with explicit encoding
try (OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("message.txt"),
StandardCharsets.UTF_8)) {
osw.write("Unicode test: \u00e9\u00e0\u00fc\n"); // accented characters
}
// Read it back
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream("message.txt"),
StandardCharsets.UTF_8))) {
System.out.println(br.readLine());
}
}
}
Always specify encoding explicitly.FileReader and FileWriter use the JVM's platform default encoding, which differs between Windows, Linux, and macOS. This is a common source of text corruption bugs.
// Dangerous: uses platform default encoding
new FileReader("file.txt")
// Safe: explicit UTF-8
new InputStreamReader(new FileInputStream("file.txt"), StandardCharsets.UTF_8)
4. DataInputStream / DataOutputStream — Typed Binary Data
These streams allow writing and reading Java primitive types directly in binary format.
import java.io.*;
public class DataStreamExample {
public static void main(String[] args) throws IOException {
String fileName = "typed_data.dat";
// Write typed data
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream(fileName)))) {
dos.writeInt(42); // 4 bytes
dos.writeDouble(3.14159); // 8 bytes
dos.writeBoolean(true); // 1 byte
dos.writeUTF("Hello!"); // 2-byte length prefix + UTF-8 bytes
System.out.println("Written: " + dos.size() + " bytes total");
}
// Read back in the EXACT SAME ORDER they were written
try (DataInputStream dis = new DataInputStream(
new BufferedInputStream(new FileInputStream(fileName)))) {
System.out.println("int: " + dis.readInt()); // 42
System.out.println("double: " + dis.readDouble()); // 3.14159
System.out.println("boolean: " + dis.readBoolean()); // true
System.out.println("string: " + dis.readUTF()); // Hello!
}
}
}
Data written with DataOutputStream must be read back in exactly the same order and with the same types. Reading out of order produces garbage data or throws exceptions.
5. PrintStream and PrintWriter — Formatted Text Output
System.out is a PrintStream. It (and PrintWriter) provide convenient print, println, and printf methods and never throw IOException (errors are tracked with checkError()).
import java.io.*;
public class PrintStreamWriterExample {
public static void main(String[] args) throws IOException {
// PrintStream to file with auto-flush and explicit encoding
try (PrintStream ps = new PrintStream(
new BufferedOutputStream(new FileOutputStream("output.txt")),
true, // autoFlush: flushes after each println()
"UTF-8")) {
ps.println("=== Report ===");
ps.printf("%-10s %5s %8s%n", "Name", "Age", "Score");
ps.printf("%-10s %5d %8.2f%n", "Alice", 30, 95.5);
ps.printf("%-10s %5d %8.2f%n", "Bob", 25, 88.0);
}
// PrintWriter: character-based, more suitable for text files
try (PrintWriter pw = new PrintWriter(
new BufferedWriter(new FileWriter("report.txt")))) {
pw.println("Sales Report");
pw.printf("Date: %s%n", java.time.LocalDate.now());
pw.println("---");
for (int i = 1; i <= 5; i++) {
pw.printf("Product %d: %,d units sold%n", i, i * 100);
}
if (pw.checkError()) {
System.err.println("Write error detected!");
}
}
System.out.println("Files written successfully.");
}
}
6. flush() — Forcing Buffer Contents to Disk
Buffered streams hold data in memory until the buffer fills or the stream closes. Call flush() when you must ensure data is persisted immediately — for example, in logging or network protocols.
import java.io.*;
public class FlushExample {
public static void main(String[] args) throws IOException {
try (BufferedWriter bw = new BufferedWriter(new FileWriter("live_log.txt"))) {
for (int step = 1; step <= 50; step++) {
bw.write(String.format("[Step %02d] Processing...%n", step));
// Flush every 10 steps so the log file is readable in real time
if (step % 10 == 0) {
bw.flush();
System.out.println("Flushed at step " + step);
}
}
} // also flushed and closed automatically by try-with-resources
System.out.println("Done.");
}
}
7. Practical Example: Console Input Processing Chain
import java.io.*;
import java.util.ArrayList;
import java.util.List;
public class ConsoleInputProcessor {
public static void main(String[] args) throws IOException {
// System.in is InputStream (byte-based).
// Chain: InputStream -> InputStreamReader (encoding) -> BufferedReader (line reading)
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in, java.nio.charset.StandardCharsets.UTF_8)
);
System.out.println("Enter lines of text (type 'quit' to stop):");
List<String> lines = new ArrayList<>();
String line;
while ((line = br.readLine()) != null && !line.equalsIgnoreCase("quit")) {
if (!line.isBlank()) {
lines.add(line.trim());
}
}
// Write collected lines to a file with statistics
try (PrintWriter pw = new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream("result.txt"),
java.nio.charset.StandardCharsets.UTF_8
)
))) {
pw.printf("Lines collected: %d%n%n", lines.size());
for (int i = 0; i < lines.size(); i++) {
pw.printf("%3d: %s%n", i + 1, lines.get(i));
}
}
System.out.printf("Saved %d lines to result.txt%n", lines.size());
}
}
8. Filter Stream Comparison Table
| Filter Stream | Wraps | Added Feature |
|---|---|---|
BufferedInputStream | InputStream | In-memory buffer, batch reading |
BufferedOutputStream | OutputStream | In-memory buffer, batch writing |
BufferedReader | Reader | Buffer + readLine() |
BufferedWriter | Writer | Buffer + newLine() |
InputStreamReader | InputStream | Bytes → chars (encoding) |
OutputStreamWriter | OutputStream | Chars → bytes (encoding) |
DataInputStream | InputStream | Read typed primitives |
DataOutputStream | OutputStream | Write typed primitives |
PrintStream | OutputStream | print/println/printf |
PrintWriter | Writer | print/println/printf |
ObjectInputStream | InputStream | Deserialize objects |
ObjectOutputStream | OutputStream | Serialize objects |
Pro tip: The most common and useful filter stream chain for reading text files is:
BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream("file.txt"),
StandardCharsets.UTF_8
)
);
However, for most modern use cases, Files.readString(), Files.readAllLines(), or Files.lines() from java.nio.file.Files are simpler and should be preferred.