在 Java 開發中,字符編碼與解碼是處理文本數據時繞不開的核心主題。無論是網絡傳輸、文件存儲,還是跨平台數據交換,正確理解字符集、編碼方案與 Java NIO 中的 Charset、CharsetEncoder、CharsetDecoder 等類別的運作機制,都是構建健壯應用的關鍵。本文將系統梳理相關概念,並通過完整代碼示例演示標準字符集的編解碼行為,進而指導如何實作自定義字符集並透過 SPI 機制註冊到 JVM。
在 Java 中,java.nio.charset.Charset 封裝了「編碼字符集 + 編碼方案」的組合,對應 RFC 2278 定義的 charset 概念。
Unicode 旨在為全球所有語言提供統一碼表,Java 內部採用 UTF-16(16 位字符)。然而作業系統 I/O 與文件存儲以位元組為導向,因此必須在字符序列與位元組序列之間轉換。java.nio.charset 包提供了系統級、全面且靈活的轉換機制,並透過 java.nio.charset.spi 支援可插拔的自定義字符集。
以下示例程式 EncodeTest 將包含非 ASCII 字符的字符串 "¿Mañana?" 依序以 US-ASCII、ISO-8859-1、UTF-8、UTF-16BE、UTF-16LE、UTF-16 進行編碼,並輸出十六進制位元組值:
package com.ronsoft.books.nio.charset;
import java.nio.charset.Charset;
import java.nio.ByteBuffer;
public class EncodeTest {
public static void main(String[] argv) throws Exception {
String input = " \u00bfMa\u00f1ana?";
String[] charsetNames = { "US-ASCII", "ISO-8859-1", "UTF-8",
"UTF-16BE", "UTF-16LE", "UTF-16" };
for (int i = 0; i < charsetNames.length; i++) {
doEncode(Charset.forName(charsetNames[i]), input);
}
}
private static void doEncode(Charset cs, String input) {
ByteBuffer bb = cs.encode(input);
System.out.println("Charset: " + cs.name());
System.out.println(" Input: " + input);
System.out.println("Encoded: ");
for (int i = 0; bb.hasRemaining(); i++) {
int b = bb.get();
int ival = ((int) b) & 0xff;
char c = (char) ival;
if (i < 10) System.out.print(" ");
System.out.print(" " + i + ": ");
if (ival < 16) System.out.print("0");
System.out.print(Integer.toHexString(ival));
if (Character.isWhitespace(c) || Character.isISOControl(c)) {
System.out.println("");
} else {
System.out.println(" (" + c + ")");
}
}
System.out.println("");
}
}
這直觀展示了不同編碼方案對同一字符序列的位元組表現差異,也是排查亂碼問題的基礎。
Charset 為抽象類,實現 Comparable,核心方法包括:
forName(String)、availableCharsets()、isSupported(String)name()、aliases()、displayName()、isRegistered()、canEncode()newEncoder()、newDecoder()encode(CharBuffer)、encode(String)、decode(ByteBuffer)contains(Charset) —— 若 C1 包含 C2,則 C2 可編碼的字符流在 C1 中亦可無損編碼。字符集名稱不區分大小寫,標準名稱需符合 IANA 註冊規範;自定義字符集名稱應以「X-」或「x-」開頭。
CharsetEncoder 為狀態型轉換引擎,字符入、位元組出。基本步驟:
reset() 重置內部狀態。encode(CharBuffer in, ByteBuffer out, boolean endOfInput),將字符編碼寫入輸出緩衝區。flush(ByteBuffer out) 刷新剩餘狀態。編碼過程中可能遇到四種結果(CoderResult):
可透過 onMalformedInput()、onUnmappableCharacter() 設定錯誤處理動作:REPORT(預設,拋出異常/返回錯誤結果)、IGNORE(跳過)、REPLACE(用替換位元組序列代替)。
CharsetDecoder 為編碼器逆操作,位元組入、字符出,流程與編碼器對稱:reset() → 循環 decode() → flush()。同樣需處理緩衝區上溢/下溢及錯誤動作。
CharsetDecode 演示如何從 ReadableByteChannel(如標準輸入)讀取位元組流,解碼為字符並寫入 Writer(如標準輸出),妥善處理緩衝區上溢/下溢與解碼器刷新:
public static void decodeChannel(ReadableByteChannel source, Writer writer, Charset charset) throws IOException {
CharsetDecoder decoder = charset.newDecoder();
decoder.onMalformedInput(CodingErrorAction.REPLACE);
decoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
ByteBuffer bb = ByteBuffer.allocateDirect(16 * 1024);
CharBuffer cb = CharBuffer.allocate(57);
CoderResult result = CoderResult.UNDERFLOW;
boolean eof = false;
while (!eof) {
if (result == CoderResult.UNDERFLOW) {
bb.clear();
eof = (source.read(bb) == -1);
bb.flip();
}
result = decoder.decode(bb, cb, eof);
if (result == CoderResult.OVERFLOW) {
drainCharBuf(cb, writer);
}
}
while (decoder.flush(cb) == CoderResult.OVERFLOW) {
drainCharBuf(cb, writer);
}
drainCharBuf(cb, writer);
source.close();
writer.flush();
}
透過繼承 Charset、CharsetEncoder、CharsetDecoder 並委託基礎字符集(如 UTF-8)完成實際編解碼,在中間植入 Rot13 置換邏輯(僅針對 ASCII 字母),即可構建一個可插拔的「加密」字符集。
Rot13Charset 建構子調用 super(canonical, aliases) 註冊名稱,並持有基礎 Charset 參考。newEncoder()/newDecoder() 返回包裝基礎編解碼器的自定義實現。encodeLoop():複製輸入 CharBuffer、應用 Rot13、重置基礎編碼器並委託編碼,最後同步輸入緩衝區位置。decodeLoop():重置基礎解碼器、委託解碼、對輸出 CharBuffer 應用 Rot13(因 Rot13 自逆)。實作 CharsetProvider 子類(RonsoftCharsetProvider),覆寫 charsetForName() 與 charsets()。在 JAR 的 META-INF/services/java.nio.charset.spi.CharsetProvider 文件中列出提供者全限定類名,JVM 啟動時即可自動發現並註冊該字符集,之後即可像標準字符集一樣使用 "X-ROT13"。
Java NIO 的字符集框架將編碼字符集與編碼方案解耦,提供 Charset、CharsetEncoder、CharsetDecoder 三大核心抽象,並透過 SPI 機制開放擴展點。絕大多數開發者僅需正確選擇標準字符集(如 UTF-8)並妥善處理編解碼錯誤;少數場景(如歷史系統對接、私有協議、教學演示)可自定義字符集。掌握緩衝區交互、錯誤動作配置、以及編解碼器的狀態機模型,是解決疑難雜症亂碼、實現高性能文本處理的關鍵。