1. 編碼(encoding)的概念
先從最重要的問題開始:什麼是編碼?
想像你抵達一場國際會議。每個人都說自己的語言,但大家都想彼此理解。這就需要一位翻譯,他知道英文的 "hello",在俄語中是 "privet",在西班牙語中是 "hola"。在電腦世界裡,編碼就像這位翻譯。
編碼是把字元表示成位元組的方式
電腦很單純:它只懂 0 和 1,也就是位元與位元組。但人類想要看到的是字母、數字、表情符號,甚至——天哪!——中文漢字。要讓電腦能「寫下」一個字元,就必須先約定每個字元對應哪一組位元組。
編碼(encoding)是一套規則,說明如何把字元(字母、數字、標點、表情符號等)轉換成位元組以便儲存與傳輸;反過來,如何把位元組還原成字元以便顯示。
範例:不同編碼中的字母 'A'
- 在 UTF-8 中,字母 'A'(西里爾字母,U+0410)會被編為兩個位元組:0xD0 0x90。
- 在 Windows-1251 中,同一個字母會是 1 個位元組:0xC0。
- 而拉丁字母 'A' 在幾乎所有常見編碼中都是 0x41。
如果用錯編碼讀取檔案,字元會變成「亂碼」(不明符號或問號)。
2. 為什麼需要編碼
為什麼不能直接把字母原樣存起來?
因為電腦只懂數字(0 和 1)。哪個數字對應哪個字母——這正是編碼的本質。
範例:「Privet」在磁碟上
當你把單字 "Privet" 寫入檔案時,對電腦來說它只是位元組序列。如何解讀這些位元組,取決於編碼。
- 如果檔案以 UTF-8 儲存,像「P」(西里爾字母 Pe,U+041F)會是兩個位元組,「r」(西里爾字母 er,U+0440)也是兩個,依此類推。
- 如果是 Windows-1251,每個字母都是一個位元組,但位元組的值不同。
在哪些地方需要編碼?
- 在把文字寫入檔案時:才能之後正確讀回位元組。
- 在從檔案讀取文字時:才能把位元組還原成字母。
- 在透過網路傳送文字時(例如 HTTP、e‑mail)。
- 在使用資料庫時:也必須知道文字以哪種編碼儲存。
如果沒有指定編碼……
就像打開一段不熟悉語言的文字,硬要讀。若你知道那是什麼語言,理解的機會會大些;若不知道——好的情況是什麼也看不懂,壞的情況就是一團亂碼。
3. 沒有正確編碼會遇到的問題
亂碼與資料遺失
新手(不只新手)最常見的抱怨是:「為什麼我應該看到 "Privet",卻看到 "Привет",或乾脆全是問號?」
這發生在檔案用某種編碼寫入,卻用另一種編碼讀取時。比如檔案是在舊版 Windows 上以 Windows-1251 建立,而你在 Linux(預設常為 UTF-8)上開啟;反過來也一樣。
範例
- 用 Windows-1251 寫入檔案:對於「P」(西里爾字母 Pe,U+041F)而言,位元組是 0xCF。
- 在 UTF-8 中開啟:程式預期西里爾字母是兩個位元組,卻只收到一個,一切就壞了。
資料遺失
若寫入時選用的編碼不支援某個字元(例如嘗試把表情符號存成 ASCII),它會消失或被替換為問號。任何「塞不進」目標編碼的內容都會流失。
交換檔案時的問題
用一種編碼寫入的檔案,若在預設不同編碼的另一台電腦上開啟,可能會顯示不正確。這常見於 Windows 與 Linux 之間的檔案交換,或開啟舊檔案時。
4. Java 中的編碼:內部與外部表示
在 JVM 內部:一律是 Unicode(UTF-16)
在 Java 中,程式內的字串(String)始終以 Unicode(更精確地說是 UTF-16)儲存。這表示你可以放心把任何語言的字串指派給變數,Java 都能「消化」。
String hello = "你好,世界! 😀";
在 JVM 記憶體中,這段文字儲存為一組 16 位元數字(char),每個字元在 Unicode 表中都有自己的代碼點。
有趣的小知識
在 Java 中,型別 char 是 16 位元(2 位元組)。但某些字元(例如罕見漢字或表情符號)需要兩個 char——這就是「代理對」(surrogate pairs)。
輸入/輸出時:編碼很重要!
當你讀取或寫出字串到外部世界(檔案、網路)時,Java 需要把內部表示(UTF-16)轉成位元組序列。這時候就需要指定編碼。
- 如果你沒有明確指定編碼,Java 會使用系統預設(在俄文 Windows 上可能是 Windows-1251,在 Linux 上常見的是 UTF-8)。
- 這很危險:在另一台電腦上的結果可能不同。
範例:未指定編碼的讀寫
// 不佳做法!未明確指定編碼。
FileReader reader = new FileReader("data.txt");
FileWriter writer = new FileWriter("data.txt");
在這種情況下,Java 會使用系統預設編碼。若檔案是在其他系統寫入——你將得到「亂碼」。
好做法:永遠明確指定編碼
// 這樣很好!明確指定了編碼。
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("data.txt"), StandardCharsets.UTF_8));
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("data.txt"), StandardCharsets.UTF_8));
5. 簡短示意:處理編碼時發生了什麼
示意圖:從檔案到程式、再回到檔案的路徑
[磁碟上的檔案(位元組,編碼 X)]
|
V
[Java 讀取位元組,並用編碼 X 轉成 String(UTF-16)]
|
V
[你在程式中處理字串]
|
V
[Java 以編碼 Y 把 String 寫成位元組]
|
V
[磁碟上的檔案(位元組,編碼 Y)]
如果 X 與 Y 相同——一切正常。若不同——可能出問題。
6. 編碼小史(給好奇的人)
ASCII
ASCII 是最早的編碼之一:每個字元 1 個位元組,只涵蓋英文字母、數字與基本標點。其他字母系統一律不支援。
Windows-1251、ISO-8859-1 與其他「老字號」
這些是一位元組編碼,對應不同的字母集合:西里爾、拉丁、希臘等。各自為政,結果就是一片混亂。
Unicode 與 UTF 家族
- Unicode:涵蓋全世界字元的全球化碼表。
- UTF-8、UTF-16、UTF-32:把 Unicode 字元表示為位元組的不同方式。
- UTF-8 已成為 Web、檔案與跨系統交換的標準。
7. 實作:編碼如何影響檔案處理
我們來看一個用不同編碼讀寫字串的小範例。
範例:用不同編碼寫入並讀取
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
public class EncodingDemo {
public static void main(String[] args) throws IOException {
String text = "你好,世界! 😀";
// 用 UTF-8 寫入檔案
try (Writer writer = new OutputStreamWriter(
new FileOutputStream("utf8.txt"), StandardCharsets.UTF_8)) {
writer.write(text);
}
// 現在嘗試用錯誤的編碼讀取它
try (Reader reader = new InputStreamReader(
new FileInputStream("utf8.txt"), Charset.forName("Windows-1251"))) {
int c;
while ((c = reader.read()) != -1) {
System.out.print((char) c);
}
}
// 螢幕上會是一堆亂碼!
}
}
結論:如果編碼不一致——文字就會被扭曲。
8. 編碼與與其他系統整合
在真實專案中,檔案常在不同程式之間交換,這些程式可能由不同語言撰寫、跑在不同作業系統上。每一方都可能期望不同的編碼。若不事先協調——就會得到「亂碼」與難以追查的錯誤。典型情況:資料庫以 UTF-8 儲存文字,而你的程式把來源檔案當作 Windows-1251 讀入再寫進資料庫——字元一定被扭曲。
9. 處理編碼時的常見錯誤
錯誤 №1:讀寫檔案時未指定編碼。
結果就是「在我電腦上沒問題」,但同事那邊全是「亂碼」。
錯誤 №2:使用過時的建構子(如 FileReader、FileWriter)。
它們一律使用系統預設編碼——是新手常踩的坑。
錯誤 №3:來源檔案的編碼不正確。
若檔案以某種編碼寫入,卻用另一種編碼讀取,部分字元會被扭曲或改成問號。
錯誤 №4:不同編碼轉換時的字元遺失。
若目標編碼不支援所有字元(例如用 ASCII 取代 UTF-8),部分文字就會直接消失。
GO TO FULL VERSION