Chuyển tới nội dung chính

Streams, Readers và Writers

Mục tiêu bài học

Sau bài này, bạn sẽ:

  • Hiểu InputStream/OutputStream (byte streams) vs Reader/Writer (character streams)
  • Nắm được decorator pattern trong Java I/O: BufferedInputStream, DataInputStream, etc.
  • Sử dụng buffering để tăng performance khi đọc/ghi dữ liệu
  • Xử lý character encoding đúng cách (UTF-8, UTF-16) với InputStreamReader/OutputStreamWriter
  • Biết khi nào dùng byte streams vs character streams

Bài trước: File I/O cơ bản — Đã học về File class và đọc/ghi file văn bản. Bài này sẽ đi sâu vào stream hierarchy và decorator pattern trong Java I/O.

InputStream và OutputStream - Byte-level I/O

InputStreamOutputStreamabstract base classes cho tất cả byte streams trong Java. Chúng xử lý dữ liệu ở mức byte (8-bit binary data).

InputStream Hierarchy

InputStream (abstract)
├── FileInputStream // Đọc file
├── ByteArrayInputStream // Đọc từ byte array trong memory
├── BufferedInputStream // Buffered reading (decorator)
├── DataInputStream // Đọc primitive types (decorator)
├── ObjectInputStream // Đọc objects (serialization)
└── FilterInputStream // Base cho decorators

InputStream - Các method chính

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class InputStreamExample {
public static void main(String[] args) {
try (InputStream in = new FileInputStream("data.bin")) {

// 1. read() - Đọc 1 byte (0-255), -1 nếu EOF
int singleByte = in.read();
if (singleByte != -1) {
System.out.println("First byte: " + singleByte);
}

// 2. read(byte[] buffer) - Đọc nhiều bytes vào array
byte[] buffer = new byte[1024];
int bytesRead = in.read(buffer);
System.out.println("Bytes read: " + bytesRead);

// 3. read(byte[] buffer, int offset, int length)
byte[] largeBuffer = new byte[2048];
int count = in.read(largeBuffer, 0, 512); // Đọc max 512 bytes

// 4. available() - Số bytes có thể đọc mà không bị block
int available = in.available();
System.out.println("Available bytes: " + available);

// 5. skip(long n) - Bỏ qua n bytes
long skipped = in.skip(100);

// 6. mark() và reset() - Đánh dấu vị trí và quay lại
if (in.markSupported()) {
in.mark(1000); // Đánh dấu, cho phép reset trong 1000 bytes
// ... đọc một ít
in.reset(); // Quay lại vị trí đã mark
}

} catch (IOException e) {
e.printStackTrace();
}
}
}

OutputStream - Các method chính

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class OutputStreamExample {
public static void main(String[] args) {
try (OutputStream out = new FileOutputStream("output.bin")) {

// 1. write(int b) - Ghi 1 byte (chỉ 8 bits thấp)
out.write(65); // Ghi byte 'A'

// 2. write(byte[] buffer) - Ghi toàn bộ array
byte[] data = {66, 67, 68}; // 'B', 'C', 'D'
out.write(data);

// 3. write(byte[] buffer, int offset, int length)
byte[] largeData = new byte[100];
out.write(largeData, 10, 50); // Ghi 50 bytes từ vị trí 10

// 4. flush() - Đẩy buffer ra file ngay
out.flush();

} catch (IOException e) {
e.printStackTrace();
}
}
}

FileInputStream và FileOutputStream

FileInputStream - Đọc file nhị phân

import java.io.FileInputStream;
import java.io.IOException;

public class FileInputStreamExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("image.jpg")) {

byte[] buffer = new byte[8192]; // 8KB buffer
int bytesRead;

long totalBytes = 0;
while ((bytesRead = fis.read(buffer)) != -1) {
totalBytes += bytesRead;
// Xử lý buffer...
}

System.out.println("Total bytes read: " + totalBytes);

} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}

FileOutputStream - Ghi file nhị phân

import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputStreamExample {
public static void main(String[] args) {
// Constructor với append flag
try (FileOutputStream fos = new FileOutputStream("data.bin", false)) {
// false = overwrite, true = append

byte[] data = {10, 20, 30, 40, 50};
fos.write(data);

fos.flush(); // Đảm bảo data được ghi ra disk

} catch (IOException e) {
e.printStackTrace();
}
}
}

Ví dụ: Copy file nhị phân (ảnh, video, etc.)

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileCopyBinary {
public static void copyFile(String source, String destination) throws IOException {
try (FileInputStream in = new FileInputStream(source);
FileOutputStream out = new FileOutputStream(destination)) {

byte[] buffer = new byte[8192]; // 8KB buffer
int bytesRead;

while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}

System.out.println("File copied successfully!");
}
}

public static void main(String[] args) {
try {
copyFile("photo.jpg", "photo_backup.jpg");
} catch (IOException e) {
System.err.println("Copy failed: " + e.getMessage());
}
}
}
Performance tip

Luôn dùng buffer (array) khi đọc/ghi file. Đọc từng byte một (read() không tham số) CỰC CHẬM vì mỗi lần gọi system call!

BufferedInputStream và BufferedOutputStream - Tại sao cần Buffering?

Vấn đề với FileInputStream/FileOutputStream

// CHẬM - System call mỗi byte!
try (FileInputStream fis = new FileInputStream("large.dat")) {
int b;
while ((b = fis.read()) != -1) { // Mỗi lần gọi read() = 1 system call!
process(b);
}
}

System calls rất chậm vì:

  • Context switch giữa user mode và kernel mode
  • Overhead của OS
  • Đọc 1MB file = 1,048,576 system calls → cực kỳ chậm!

Solution: Buffering

// NHANH - Đọc chunks lớn, giảm system calls
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("large.dat"))) {

int b;
while ((b = bis.read()) != -1) {
// BufferedInputStream đọc chunks (vd 8KB) vào internal buffer
// read() chỉ lấy từ buffer, không gọi system call mỗi lần!
process(b);
}
}

BufferedInputStream

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class BufferedInputStreamExample {
public static void main(String[] args) {
// Default buffer size: 8192 bytes (8KB)
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("data.txt"))) {

int b;
while ((b = bis.read()) != -1) {
System.out.print((char) b);
}

} catch (IOException e) {
e.printStackTrace();
}

// Custom buffer size
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("large.dat"), 65536)) { // 64KB buffer

// Đọc file...

} catch (IOException e) {
e.printStackTrace();
}
}
}

BufferedOutputStream

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class BufferedOutputStreamExample {
public static void main(String[] args) {
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("output.txt"))) {

for (int i = 0; i < 10000; i++) {
bos.write(("Line " + i + "\n").getBytes());
// Data được ghi vào buffer, chưa ghi ra disk
}

// flush() để đẩy buffer ra disk ngay
bos.flush();

} catch (IOException e) {
e.printStackTrace();
}
// close() tự động gọi flush()
}
}

Performance Comparison

import java.io.*;

public class BufferingPerformance {
public static void main(String[] args) throws IOException {
String file = "test.dat";

// Tạo test file 10MB
createTestFile(file, 10_000_000);

// Test 1: Không buffer
long start1 = System.currentTimeMillis();
copyWithoutBuffer(file, "copy1.dat");
long time1 = System.currentTimeMillis() - start1;

// Test 2: Có buffer
long start2 = System.currentTimeMillis();
copyWithBuffer(file, "copy2.dat");
long time2 = System.currentTimeMillis() - start2;

System.out.println("Without buffer: " + time1 + "ms");
System.out.println("With buffer: " + time2 + "ms");
System.out.println("Speedup: " + (time1 / (double) time2) + "x");
}

static void copyWithoutBuffer(String src, String dest) throws IOException {
try (FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dest)) {
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
}
}

static void copyWithBuffer(String src, String dest) throws IOException {
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(src));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(dest))) {
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
}
}

static void createTestFile(String file, int size) throws IOException {
try (FileOutputStream fos = new FileOutputStream(file)) {
for (int i = 0; i < size; i++) {
fos.write((byte) (i % 256));
}
}
}
}

Kết quả thực tế:

Without buffer: 45230ms  (45 giây!)
With buffer: 127ms (0.12 giây)
Speedup: 356x
Luôn dùng BufferedInputStream/BufferedOutputStream

Không buffer = hàng trăm lần chậm hơn! Wrap tất cả FileInputStream/FileOutputStream với Buffered version.

Reader và Writer - Character-level I/O

ReaderWriter xử lý character data (16-bit Unicode). Khác với byte streams, character streams tự động xử lý character encoding.

Reader Hierarchy

Reader (abstract)
├── FileReader // Đọc text file
├── BufferedReader // Buffered, có readLine()
├── InputStreamReader // Bridge: InputStream → Reader
├── StringReader // Đọc từ String
└── CharArrayReader // Đọc từ char array

Writer Hierarchy

Writer (abstract)
├── FileWriter // Ghi text file
├── BufferedWriter // Buffered, có newLine()
├── OutputStreamWriter // Bridge: Writer → OutputStream
├── PrintWriter // Formatted output
└── StringWriter // Ghi vào String buffer

Reader và Writer - Các method chính

import java.io.*;

public class ReaderWriterExample {
public static void readerExample() throws IOException {
try (Reader reader = new FileReader("text.txt")) {

// 1. read() - Đọc 1 character (0-65535), -1 nếu EOF
int ch = reader.read();

// 2. read(char[] buffer)
char[] buffer = new char[1024];
int charsRead = reader.read(buffer);

// 3. read(char[] buffer, int offset, int length)
reader.read(buffer, 0, 512);

// 4. skip(long n)
reader.skip(100);
}
}

public static void writerExample() throws IOException {
try (Writer writer = new FileWriter("output.txt")) {

// 1. write(int c) - Ghi 1 character
writer.write('A');

// 2. write(char[] buffer)
char[] chars = {'H', 'e', 'l', 'l', 'o'};
writer.write(chars);

// 3. write(String str)
writer.write("Hello, World!");

// 4. write(String str, int offset, int length)
writer.write("Hello, World!", 0, 5); // "Hello"

// 5. flush() và close()
writer.flush();
}
}
}

InputStreamReader và OutputStreamWriter - Bridge giữa Byte↔Character

Vấn đề: InputStream/OutputStream xử lý bytes, nhưng văn bản cần character encoding (UTF-8, ISO-8859-1, etc.)

Giải pháp: InputStreamReaderOutputStreamWriterbridge classes chuyển đổi giữa byte streams và character streams.

Encoding Flow Diagram

InputStreamReader

import java.io.*;
import java.nio.charset.StandardCharsets;

public class InputStreamReaderExample {
public static void main(String[] args) throws IOException {

// Đọc từ file với UTF-8 encoding
try (InputStreamReader reader = new InputStreamReader(
new FileInputStream("vietnamese.txt"),
StandardCharsets.UTF_8)) {

int ch;
while ((ch = reader.read()) != -1) {
System.out.print((char) ch);
}
}

// Đọc từ network socket với ISO-8859-1
// Socket socket = ...
// try (InputStreamReader reader = new InputStreamReader(
// socket.getInputStream(),
// StandardCharsets.ISO_8859_1)) {
// // ...
// }
}
}

OutputStreamWriter

import java.io.*;
import java.nio.charset.StandardCharsets;

public class OutputStreamWriterExample {
public static void main(String[] args) throws IOException {

// Ghi file với UTF-8 encoding
try (OutputStreamWriter writer = new OutputStreamWriter(
new FileOutputStream("vietnamese.txt"),
StandardCharsets.UTF_8)) {

writer.write("Xin chào Việt Nam!");
writer.write("\n");
writer.write("Chữ Việt có dấu: á à ả ã ạ");
}

// Ghi với Windows-1252 encoding
try (OutputStreamWriter writer = new OutputStreamWriter(
new FileOutputStream("legacy.txt"),
"Windows-1252")) {

writer.write("Legacy encoding");
}
}
}

Character Encoding - UTF-8, ISO-8859-1

Các encoding phổ biến

EncodingMô tảUse case
UTF-8Variable-length (1-4 bytes/char), supports all UnicodeRecommended cho mọi ứng dụng mới
UTF-162 hoặc 4 bytes/char, Java internal representationJava Strings internally
ISO-8859-1 (Latin-1)1 byte/char, supports Western European languagesLegacy systems
Windows-12521 byte/char, Windows defaultOld Windows apps
US-ASCII1 byte/char, chỉ 0-127English-only text
Luôn dùng UTF-8

UTF-8 là standard de facto cho web, APIs, và modern applications. Hỗ trợ mọi ngôn ngữ (tiếng Việt, emoji, Chinese, etc.)

Character Encoding Pitfalls - Safe Code Examples

🔥 Bẫy OCP: Reading with wrong charset = mojibake
import java.io.*;
import java.nio.charset.StandardCharsets;

public class EncodingPitfalls {
public static void main(String[] args) throws IOException {
String vietnamese = "Xin chào! Đây là tiếng Việt: áàảãạ";

// ===== PITFALL 1: FileWriter uses platform default =====
System.out.println("=== PITFALL 1: FileWriter (platform default) ===");
try (FileWriter writer = new FileWriter("wrong.txt")) {
writer.write(vietnamese);
}
// → Windows: saves as Windows-1252
// → Linux/Mac: saves as UTF-8
// CODE NOT PORTABLE!

// SAFE: Always specify UTF-8
try (OutputStreamWriter writer = new OutputStreamWriter(
new FileOutputStream("correct.txt"),
StandardCharsets.UTF_8)) {
writer.write(vietnamese);
}
// ✅ Portable across all platforms!

// ===== PITFALL 2: Reading with wrong charset =====
System.out.println("\n=== PITFALL 2: Wrong charset when reading ===");

// Read UTF-8 file with ISO-8859-1 → mojibake (garbled text)
System.out.println("Reading UTF-8 file with ISO-8859-1:");
try (InputStreamReader reader = new InputStreamReader(
new FileInputStream("correct.txt"),
StandardCharsets.ISO_8859_1)) { // ❌ WRONG!

int ch;
while ((ch = reader.read()) != -1) {
System.out.print((char) ch);
}
System.out.println();
// Output: Xin chà o! Ä Ã¢y là tiếng Viá»t: áà ãạ
// ❌ Mojibake! Unreadable!
}

// ✅ CORRECT: Read with matching charset
System.out.println("\nReading UTF-8 file with UTF-8:");
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("correct.txt"),
StandardCharsets.UTF_8))) { // ✅ CORRECT!

String line = reader.readLine();
System.out.println(line);
// Output: Xin chào! Đây là tiếng Việt: áàảãạ
// ✅ Perfect!
}

// ===== PITFALL 3: Forgetting to close = data loss =====
System.out.println("\n=== PITFALL 3: Forgetting flush/close ===");

// ❌ BAD: Manual close, easy to forget!
OutputStreamWriter writer = null;
try {
writer = new OutputStreamWriter(
new FileOutputStream("manual.txt"),
StandardCharsets.UTF_8);
writer.write("Important data");
// Forgot flush()!
// Forgot close()!
// → Data might not be written!
} catch (IOException e) {
e.printStackTrace();
}

// ✅ GOOD: try-with-resources auto-closes
try (OutputStreamWriter safeWriter = new OutputStreamWriter(
new FileOutputStream("safe.txt"),
StandardCharsets.UTF_8)) {
safeWriter.write("Important data");
// Auto-closes and flushes!
}

System.out.println("\n=== Platform default charset ===");
System.out.println("Default charset: " +
java.nio.charset.Charset.defaultCharset());
// Windows: windows-1252
// Linux/Mac: UTF-8
}
}

Kết luận:

  • FileReader / FileWriter → Platform-dependent encoding
  • InputStreamReader / OutputStreamWriter + StandardCharsets.UTF_8 → Safe
  • ✅ ALWAYS use try-with-resources
  • ✅ ALWAYS specify StandardCharsets.UTF_8 for Vietnamese text

Flush Semantics Flowchart

Flushing Semantics - BufferedWriter data loss if not flushed!

🔥 Bẫy OCP: Forgetting flush() = data loss!
import java.io.*;

public class FlushingSemantics {
public static void main(String[] args) throws IOException {

// ===== SCENARIO 1: No flush = data lost on crash =====
System.out.println("=== Test 1: No flush ===");
try (BufferedWriter writer = new BufferedWriter(
new FileWriter("noflush.txt"))) {

writer.write("Line 1\n");
writer.write("Line 2\n");
writer.write("Line 3\n");
// NO flush()!

// Simulate crash BEFORE close()
if (true) {
System.out.println("Simulating crash...");
System.exit(1); // ❌ Data in buffer LOST!
}
}
// close() never called → flush() never called → data lost!

// ===== SCENARIO 2: Explicit flush = data safe =====
System.out.println("\n=== Test 2: With flush ===");
try (BufferedWriter writer = new BufferedWriter(
new FileWriter("withflush.txt"))) {

writer.write("Line 1\n");
writer.flush(); // ✅ Force write to disk NOW!

writer.write("Line 2\n");
writer.flush(); // ✅ Safe!

writer.write("Line 3\n");
writer.flush(); // ✅ Safe!

// Even if crash here, all data is safe!
}

// ===== SCENARIO 3: Buffering delays =====
System.out.println("\n=== Test 3: Buffering delays ===");
BufferedWriter writer = new BufferedWriter(
new FileWriter("delayed.txt"));

writer.write("Buffered data");
// Data is in BUFFER (memory), NOT on disk yet!

// Check file immediately
System.out.println("File size before flush: " +
new java.io.File("delayed.txt").length());
// Output: 0 bytes! (data still in buffer)

writer.flush(); // Force write to disk

System.out.println("File size after flush: " +
new java.io.File("delayed.txt").length());
// Output: 13 bytes (data written to disk)

writer.close();
}
}

Kết luận:

  • write() → data goes to BUFFER (memory), NOT disk!
  • flush() → force write buffer to DISK
  • close() → calls flush() automatically, then closes
  • ❌ If program crashes BEFORE close() → data in buffer LOST!
  • ✅ Call flush() explicitly after important writes!
📖 Theo Java API Docs

"If the requested length is at least as large as the buffer, however, then this method will flush the buffer and write the characters directly to the underlying output stream."

— BufferedWriter.write() javadoc

Buffering chỉ hoạt động khi write nhỏ hơn buffer size (default 8KB). Nếu write >= 8KB → bypass buffer, ghi trực tiếp!

mark() và reset() support

mark()reset() cho phép "bookmark" vị trí trong stream để quay lại sau.

Không phải stream nào cũng support mark/reset!
  • BufferedInputStream, BufferedReader: Support (marks position in buffer)
  • FileInputStream, FileReader: KHÔNG support
  • Check với markSupported() trước khi dùng!
import java.io.*;

public class MarkResetExample {
public static void main(String[] args) throws IOException {

// ===== FileInputStream: NO mark/reset support =====
System.out.println("=== FileInputStream (no mark support) ===");
try (FileInputStream fis = new FileInputStream("data.txt")) {
System.out.println("markSupported(): " + fis.markSupported());
// Output: false

// mark(100); // Would throw IOException!
}

// ===== BufferedInputStream: HAS mark/reset support =====
System.out.println("\n=== BufferedInputStream (has mark support) ===");
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("data.txt"))) {

System.out.println("markSupported(): " + bis.markSupported());
// Output: true

// Read first 5 bytes
System.out.print("First read: ");
for (int i = 0; i < 5; i++) {
System.out.print((char) bis.read());
}
System.out.println();

// Mark position (with readlimit = 100 bytes)
bis.mark(100);
System.out.println("Mark set!");

// Read next 5 bytes
System.out.print("Second read: ");
for (int i = 0; i < 5; i++) {
System.out.print((char) bis.read());
}
System.out.println();

// Reset to marked position
bis.reset();
System.out.println("Reset to mark!");

// Read again from marked position
System.out.print("After reset: ");
for (int i = 0; i < 5; i++) {
System.out.print((char) bis.read());
}
System.out.println();
// Output: Same as "Second read"
}

// ===== BufferedReader: mark/reset with characters =====
System.out.println("\n=== BufferedReader (character-level) ===");
try (BufferedReader reader = new BufferedReader(
new FileReader("data.txt"))) {

System.out.println("markSupported(): " + reader.markSupported());
// Output: true

String line1 = reader.readLine();
System.out.println("Line 1: " + line1);

reader.mark(1000); // Mark with 1000 chars readlimit

String line2 = reader.readLine();
System.out.println("Line 2: " + line2);

reader.reset(); // Go back to mark

String line2Again = reader.readLine();
System.out.println("Line 2 again: " + line2Again);
// Same as line2!
}
}
}

readlimit parameter:

mark(int readlimit)
  • readlimit: Số bytes/chars tối đa có thể đọc sau mark mà vẫn reset được
  • Nếu đọc > readlimit → reset() có thể fail!
🔥 Bẫy OCP: mark invalidated by excessive reading
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("file.txt"));

bis.mark(10); // readlimit = 10 bytes

byte[] data = new byte[20];
bis.read(data); // Read 20 bytes > readlimit!

bis.reset(); // ❌ Có thể throw IOException! Mark invalidated!
// Output: IOException: Resetting to invalid mark

PrintWriter - Formatted Output

PrintWriter kết hợp tính năng của Writer với methods tiện lợi như println(), printf().

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

public class PrintWriterExample {
public static void main(String[] args) {
try (PrintWriter writer = new PrintWriter(new FileWriter("report.txt"))) {

// println() - Tự động thêm newline
writer.println("===== REPORT =====");
writer.println();

// print() - Không thêm newline
writer.print("Name: ");
writer.print("Nguyễn Văn A");
writer.println();

// printf() - Formatted output (giống C printf)
writer.printf("Age: %d%n", 25);
writer.printf("Score: %.2f%n", 8.75);
writer.printf("%-10s | %5d | %6.2f%n", "John", 30, 9.5);

// format() - Alias của printf()
writer.format("Date: %tF%n", new java.util.Date());

// Không ném checked IOException!
// Phải check error manually
if (writer.checkError()) {
System.err.println("Error occurred!");
}

} catch (IOException e) {
e.printStackTrace();
}
}
}

Output (report.txt):

===== REPORT =====

Name: Nguyễn Văn A
Age: 25
Score: 8.75
John | 30 | 9.50
Date: 2026-02-08

PrintWriter với auto-flush

// Auto-flush: tự động flush sau mỗi println()
try (PrintWriter writer = new PrintWriter(
new FileWriter("log.txt"), true)) { // true = auto-flush

writer.println("Log entry 1"); // Tự động flush
writer.println("Log entry 2"); // Tự động flush
}

PrintStream vs PrintWriter - System.out differences

PrintStream vs PrintWriter
  • PrintStream: Byte-oriented, System.out is PrintStream, auto-flush on println()
  • PrintWriter: Char-oriented, NO auto-flush by default
import java.io.*;

public class PrintStreamVsPrintWriter {
public static void main(String[] args) throws IOException {

// ===== PrintStream (System.out) =====
System.out.println("=== PrintStream (System.out) ===");
System.out.println("System.out is: " + System.out.getClass().getName());
// Output: java.io.PrintStream

PrintStream ps = new PrintStream(new FileOutputStream("printstream.txt"));
ps.println("Line 1"); // Auto-flush after println()!
ps.print("Line 2 "); // NO auto-flush
ps.close();

// ===== PrintWriter (NO auto-flush by default) =====
System.out.println("\n=== PrintWriter (no auto-flush) ===");

// Constructor 1: NO auto-flush
PrintWriter pw1 = new PrintWriter(new FileWriter("printwriter1.txt"));
pw1.println("Line 1"); // NO auto-flush!
pw1.print("Line 2"); // NO auto-flush!
// Data still in buffer!

// Check file immediately
System.out.println("File size before flush: " +
new File("printwriter1.txt").length());
// Output: 0 (data in buffer!)

pw1.flush(); // Manual flush required!
System.out.println("File size after flush: " +
new File("printwriter1.txt").length());
pw1.close();

// ===== PrintWriter WITH auto-flush =====
System.out.println("\n=== PrintWriter (with auto-flush) ===");

// Constructor 2: WITH auto-flush
PrintWriter pw2 = new PrintWriter(
new FileWriter("printwriter2.txt"),
true); // ← true = auto-flush!

pw2.println("Line 1"); // Auto-flush!
pw2.print("Line 2"); // NO auto-flush (only println auto-flushes)
pw2.close();

// ===== Exception handling differences =====
System.out.println("\n=== Exception handling ===");

// PrintStream: Swallows exceptions! Check with checkError()
PrintStream ps2 = new PrintStream("/invalid/path/file.txt");
// No exception thrown!
ps2.println("Data");
if (ps2.checkError()) {
System.out.println("PrintStream error occurred!");
}

// PrintWriter: Also swallows exceptions!
try (PrintWriter pw3 = new PrintWriter("/invalid/path/file.txt")) {
// May not throw exception here either!
pw3.println("Data");
if (pw3.checkError()) {
System.out.println("PrintWriter error occurred!");
}
} catch (IOException e) {
System.out.println("Caught: " + e.getMessage());
}
}
}

Summary table:

FeaturePrintStreamPrintWriter
OrientationByte-orientedCharacter-oriented
System.outSystem.out is PrintStream
Auto-flush✅ Yes (on println())❌ No (unless specified in constructor)
EncodingPlatform defaultPlatform default (or specify)
ExceptionSwallowed (use checkError())Swallowed (use checkError())
Use caseConsole output, binary streamsText files, char streams
🔥 Bẫy OCP: PrintWriter doesn't auto-flush by default!
// ❌ BAD: No auto-flush, data lost on crash!
PrintWriter pw = new PrintWriter(new FileWriter("log.txt"));
pw.println("Important log"); // Still in buffer!
// Crash here → data lost!

// ✅ GOOD: Enable auto-flush
PrintWriter pw = new PrintWriter(new FileWriter("log.txt"), true);
pw.println("Important log"); // Auto-flushed!
💡 Cách nhớ
  • PrintStream = System.outSystem → Stream → auto-flush
  • PrintWriter = file Writing → need manual flush (unless enabled)

Decorator Pattern trong Java I/O

Java I/O được thiết kế theo Decorator Pattern - wrap objects để thêm functionality.

Decorator Pattern Structure

┌─────────────────────────────────────────────────┐
│ new BufferedReader( │
│ new InputStreamReader( │
│ new FileInputStream("file.txt"), │
│ StandardCharsets.UTF_8 │
│ ) │
│ ) │
└─────────────────────────────────────────────────┘
↓ ↓ ↓
Decorator Bridge/Decoder Core component
(Buffering) (Byte→Char) (File reading)

Ví dụ: Wrapping streams

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.zip.GZIPInputStream;

public class DecoratorExample {
public static void main(String[] args) throws IOException {

// Example 1: FileInputStream → BufferedInputStream
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("data.bin"))) {
// FileInputStream: đọc file
// BufferedInputStream: thêm buffering
}

// Example 2: FileInputStream → InputStreamReader → BufferedReader
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("text.txt"),
StandardCharsets.UTF_8))) {

// FileInputStream: đọc file (bytes)
// InputStreamReader: convert bytes → characters (UTF-8)
// BufferedReader: thêm buffering + readLine()
}

// Example 3: FileInputStream → GZIPInputStream → ObjectInputStream
try (ObjectInputStream ois = new ObjectInputStream(
new GZIPInputStream(
new FileInputStream("data.gz")))) {

// FileInputStream: đọc file
// GZIPInputStream: decompress gzip
// ObjectInputStream: deserialize objects
Object obj = ois.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

// Example 4: FileOutputStream → BufferedOutputStream → DataOutputStream
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("primitives.dat")))) {

// FileOutputStream: ghi file
// BufferedOutputStream: buffering
// DataOutputStream: ghi primitive types
dos.writeInt(42);
dos.writeDouble(3.14159);
dos.writeUTF("Hello");
}
}
}

Tại sao dùng Decorator Pattern?

Ưu điểm:

  • Flexibility: Mix & match các decorators theo nhu cầu
  • Single Responsibility: Mỗi class có 1 nhiệm vụ duy nhất
  • Open/Closed Principle: Thêm functionality mà không sửa existing code

Ví dụ tổ hợp:

// Cần gì? Tổ hợp như Lego!

// 1. Đọc file text, UTF-8, buffered, đọc từng dòng
BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("file.txt"),
StandardCharsets.UTF_8));

// 2. Ghi file compressed, với buffering
BufferedOutputStream out = new BufferedOutputStream(
new GZIPOutputStream(
new FileOutputStream("data.gz")));

// 3. Đọc file encrypted, compressed, deserialize objects
ObjectInputStream ois = new ObjectInputStream(
new CipherInputStream(
new GZIPInputStream(
new FileInputStream("secure.dat")),
cipher));

Ví dụ: Copy file với encoding conversion

import java.io.*;
import java.nio.charset.StandardCharsets;

public class EncodingConverter {
/**
* Copy file và convert encoding
* @param source Source file path
* @param dest Destination file path
* @param srcEncoding Source encoding (vd: "Windows-1252")
* @param destEncoding Destination encoding (vd: "UTF-8")
*/
public static void convertEncoding(String source, String dest,
String srcEncoding, String destEncoding)
throws IOException {

try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream(source), srcEncoding));

BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(dest), destEncoding))) {

String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}

System.out.println("Encoding converted: " + srcEncoding + " → " + destEncoding);
}
}

public static void main(String[] args) {
try {
// Convert old Windows file to UTF-8
convertEncoding("legacy.txt", "modern.txt",
"Windows-1252", "UTF-8");

} catch (IOException e) {
System.err.println("Conversion failed: " + e.getMessage());
}
}
}

Decorator Pattern Mermaid Diagrams

OCP Traps Summary

🔥 Common OCP Traps
  1. Forgetting to flush BufferedWriter → data lost!

    BufferedWriter writer = new BufferedWriter(new FileWriter("file.txt"));
    writer.write("data");
    // ❌ Forgot flush() → data in buffer, not on disk!
  2. Reading with wrong charset → mojibake!

    // File is UTF-8, but reading with ISO-8859-1
    InputStreamReader reader = new InputStreamReader(
    new FileInputStream("utf8.txt"),
    StandardCharsets.ISO_8859_1); // ❌ Wrong!
  3. Closing outer wrapper closes inner stream

    FileInputStream fis = new FileInputStream("file.txt");
    BufferedInputStream bis = new BufferedInputStream(fis);
    bis.close(); // ← Also closes fis!
    // fis.read(); // ❌ IOException: Stream closed
  4. System.out is PrintStream, not PrintWriter

    System.out.getClass(); // java.io.PrintStream (not PrintWriter!)
  5. mark() requires markSupported() check

    FileInputStream fis = new FileInputStream("file.txt");
    fis.mark(100); // ❌ IOException! FileInputStream doesn't support mark!
    // Must use BufferedInputStream!
  6. PrintWriter doesn't auto-flush by default

    PrintWriter pw = new PrintWriter(new FileWriter("log.txt"));
    pw.println("log"); // ❌ Not flushed! Need pw.flush() or enable auto-flush

Tổng kết

InputStream/OutputStream (Byte Streams)

  • Xử lý binary data (bytes)
  • Dùng cho: images, videos, audio, binary files
  • Core classes: FileInputStream, FileOutputStream
  • Luôn wrap với BufferedInputStream/BufferedOutputStream cho performance

Reader/Writer (Character Streams)

  • Xử lý text data (characters)
  • Tự động handle character encoding
  • Dùng cho: text files, CSV, JSON, XML
  • Core classes: FileReader, FileWriter, BufferedReader, BufferedWriter

Buffering

  • CRITICAL cho performance
  • BufferedInputStream/BufferedOutputStream: cho byte streams
  • BufferedReader/BufferedWriter: cho character streams
  • Giảm system calls hàng trăm lần

Encoding Bridges

  • InputStreamReader: InputStream → Reader (decode bytes → characters)
  • OutputStreamWriter: Writer → OutputStream (encode characters → bytes)
  • Luôn chỉ định encoding UTF-8 cho tiếng Việt!

Decorator Pattern

  • Wrap streams để thêm functionality
  • Mix & match: buffering, compression, encryption, serialization
  • Flexible và extensible

Bài tập thực hành:

  1. Implement file copy utility:

    • Copy binary files (images, videos)
    • Copy text files với encoding conversion
    • So sánh performance: with/without buffering
  2. Viết chương trình đọc CSV file lớn (100MB+):

    • Dùng BufferedReader
    • Parse từng dòng
    • Đếm statistics (min, max, average)
  3. Implement log file analyzer:

    • Đọc log file (có thể rất lớn)
    • Filter theo level (INFO, ERROR, etc.)
    • Ghi filtered logs vào file mới
    • Support encoding UTF-8

Đọc thêm