在 Java 程式設計中,「流」是一個抽象概念,它封裝了輸入/輸出設備的底層操作細節。無論是檔案、網路連線還是記憶體,資料的傳輸都以「流」的形式進行。流具有方向性:以程式(或 JVM)為參考,資料從程式流向設備稱為輸出流,反之則為輸入流。這就像水管一樣,方向決定了資料的流向。
InputStream/OutputStream 及其子類(以 Stream 結尾)處理位元組流(byte,8 bits),適用於二進位檔案(圖片、執行檔、壓縮包等);Reader/Writer 及其子類處理字元流(char,16 bits),適用於文字檔案(文字、XML、JSON 等)。FileInputStream、ByteArrayInputStream。BufferedInputStream、InputStreamReader、ObjectInputStream。

InputStream 或 Reader,提供 read() 方法;輸出流繼承自 OutputStream 或 Writer,提供 write() 方法。OutputStreamWriter/InputStreamReader)。根據使用場景快速選擇適當的流類別:
| 分類依據 | 場景 | 推薦類別 |
|---|---|---|
| 資料來源/去向 | 檔案 | FileInputStream, FileOutputStream, FileReader, FileWriter |
| 位元組陣列 | ByteArrayInputStream, ByteArrayOutputStream | |
| 字元陣列 | CharArrayReader, CharArrayWriter | |
| 字串 | StringReader, StringWriter | |
| 是否格式化輸出 | 需要 | PrintStream, PrintWriter |
| 不需要 | 一般流類別 | |
| 是否需要緩衝 | 需要 | BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter |
| 不需要 | 一般流類別 | |
| 資料格式 | 二進位/不確定 | InputStream, OutputStream 及其子類 |
| 純文字 | Reader, Writer 及其子類 | |
| 特殊需求 | 位元組↔字元轉換 | InputStreamReader, OutputStreamWriter |
| 物件序列化 | ObjectInputStream, ObjectOutputStream | |
| 行號追蹤 | LineNumberReader, LineNumberInputStream | |
| 推回輸入 | PushbackReader, PushbackInputStream |
readLine() 等特有方法)import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class TestSystemIn {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
String line;
while ((line = br.readLine()) != null) {
if ("exit".equalsIgnoreCase(line)) break;
System.out.println(line.toUpperCase());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
重點:System.in 是位元組流,透過 InputStreamReader 轉為字元流,再用 BufferedReader 獲得 readLine() 便利方法。使用 try-with-resources 自動關閉資源。
import java.io.*;
public class TestBuffer {
public static void main(String[] args) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter("d:/java.txt"));
BufferedReader br = new BufferedReader(new FileReader("d:/java.txt"))) {
for (int i = 1; i < 100; i++) {
bw.write(String.valueOf(Math.random()));
bw.newLine(); // 跨平台換行
}
bw.flush(); // 強制刷新緩衝區
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
重點:BufferedWriter 提供 newLine() 和 flush();BufferedReader 提供 readLine()。緩衝顯著提升大量資料讀寫效能。
import java.io.*;
public class TestFileInputStream {
public static void main(String[] args) {
try (FileInputStream in = new FileInputStream("e:/1.txt")) {
int b;
long count = 0;
while ((b = in.read()) != -1) {
System.out.print((char) b); // 直接轉 char 會導致中文亂碼
count++;
}
System.out.println("\n共讀取 " + count + " 個位元組");
} catch (FileNotFoundException e) {
System.err.println("找不到檔案");
} catch (IOException e) {
System.err.println("讀取錯誤");
}
}
}
教訓:位元組流讀取中文會產生亂碼,因為一個中文字元佔 2-3 個位元組,需使用字元流(FileReader)或指定編碼的轉換流。
import java.io.*;
public class TestFileOutputStream {
public static void main(String[] args) {
try (FileInputStream in = new FileInputStream("d:/java.txt");
FileOutputStream out = new FileOutputStream("d:/my_java.txt")) {
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
System.out.println("檔案已複製");
} catch (IOException e) {
System.err.println("複製失敗: " + e.getMessage());
}
}
}
改進建議:實際專案中應使用緩衝流(BufferedInputStream/BufferedOutputStream)或 NIO Files.copy() 以獲得更好效能。
import java.io.*;
class T implements Serializable {
int i = 10;
int j = 9;
double d = 2.3;
transient int k = 15; // 序列化時忽略此欄位
}
public class TestObjectIO {
public static void main(String[] args) throws Exception {
T obj = new T();
obj.k = 8; // 修改後的值不會被序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/1.txt"))) {
oos.writeObject(obj);
}
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/1.txt"))) {
T restored = (T) ois.readObject();
System.out.println(restored.i + " " + restored.j + " " + restored.k); // k 輸出 0 (預設值)
}
}
}
重點:實作 Serializable 介面啟用序列化;transient 標記的欄位不參與序列化,反序列化時為預設值(int 為 0)。
import java.io.*;
public class TestTransForm {
public static void main(String[] args) {
try (OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("d:/java.txt"), "UTF-8")) {
osw.write("你好123");
System.out.println("編碼方式:" + osw.getEncoding()); // 輸出 UTF-8
// 追加模式,指定 ISO-8859-1
try (OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream("d:/java.txt", true), "ISO-8859-1")) {
osw2.write("這是追加的內容");
System.out.println("編碼方式:" + osw2.getEncoding());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
關鍵:OutputStreamWriter 是位元組流到字元流的橋樑,務必顯式指定編碼,避免平台預設編碼導致亂碼。中文 Windows 預設 GBK,Linux/Mac 預設 UTF-8。
import java.io.*;
public class TestPrintStream {
public static void main(String[] args) {
try (PrintStream ps = new PrintStream(new FileOutputStream("d:/java.txt"))) {
System.setOut(ps); // 將標準輸出重定向到檔案
int lineCount = 0;
for (char c = 0; c < 65535; c++) {
System.out.print(c + " ");
if (++lineCount > 100) {
System.out.println();
lineCount = 0;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
特性:PrintStream/PrintWriter 不拋出受檢異常,提供 println()、printf() 等格式化方法,適合日誌輸出。
import java.io.*;
public class TestDataStream {
public static void main(String[] args) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
DataInputStream dis = new DataInputStream(bais)) {
dos.writeDouble(Math.random());
dos.writeBoolean(true);
System.out.println("可用位元組: " + bais.available());
System.out.println("讀取 double: " + dis.readDouble());
System.out.println("讀取 boolean: " + dis.readBoolean());
// 注意:讀取順序必須與寫入順序一致
} catch (IOException e) {
e.printStackTrace();
}
}
}
用途:DataInputStream/DataOutputStream 以機器無關方式讀寫 Java 基本型別,適合網路傳輸或二進位檔案儲存結構化資料。
Q: 為什麼 Reader/Writer 不繼承 InputStream/OutputStream?
A: 這是單一職責原則的體現。位元組流處理原始二進位資料,字元流處理編碼後的文字。強行繼承會導致設計混亂:字元流內部必須持有位元組流並處理編碼轉換,透過組合(InputStreamReader 包裝 InputStream)比繼承更靈活、解耦。