Charset / CharsetDecoder / CharsetEncoder 编码解码源码
概述
java.nio.charset 提供字符集(Charset)与双向转换器:CharsetDecoder 把字节序列解码成字符(ByteBuffer → CharBuffer),CharsetEncoder 反向编码。设计上采用流式状态机:每次调用 decode/encode 处理一段缓冲区,返回 CoderResult 告知"输入不足、输出不足或数据非法",调用方据此扩容或报错,天然适配分块 IO。
字符集本身通过 SPI(CharsetProvider)扩展:JDK 内建 UTF-8、ISO-8859-1 等标准集,扩展集(如 GBK)在 jdk.charsets 模块中加载。本文基于 OpenJDK 21 源码拆解查找、解码、编码与状态机细节。
核心源码解析
① Charset.forName(String charsetName) 的查找
java
public static Charset forName(String charsetName) {
Charset cs = lookup2(charsetName); // ① 优先查缓存
if (cs != null) return cs;
throw new UnsupportedCharsetException(charsetName);
}
private static Charset lookup2(String charsetName) {
if (charsetName == null) throw new IllegalArgumentException("charsetName");
Charset cs = cache1.get(charsetName); // ② 一级缓存
if (cs != null) return cs;
cs = cache2.get(charsetName); // ③ 二级缓存
if (cs == null) {
// ④ 规范化别名后查 cache2
cs = lookup(charsetName);
if (cs != null) cache2.put(charsetName, cs);
}
return cs;
}- 两级缓存:
cache1是固定大小的小缓存(HashMap加哨兵替换),cache2是HashMap加锁缓存;热点字符集几乎无锁命中。 lookup(charsetName)遍历所有CharsetProvider(先内建后 SPI),用charset.forName的别名表匹配——所以"UTF-8"、"utf8"、"UTF8"都能命中同一个实例。- 查不到抛
UnsupportedCharsetException;查到的Charset是进程内单例(每个字符集只有一份实例)。
② StandardCharsets.UTF_8 的常量
java
public final class StandardCharsets {
public static final Charset US_ASCII = new sun.nio.cs.US_ASCII();
public static final Charset ISO_8859_1 = new sun.nio.cs.ISO_8859_1();
public static final Charset UTF_8 = new sun.nio.cs.UTF_8();
public static final Charset UTF_16 = new sun.nio.cs.UTF_16();
...
}StandardCharsets在类加载时直接new出 6 个常量,不走forName缓存,所以StandardCharsets.UTF_8 == Charset.forName("UTF-8")为真(同一个单例)。UTF_8的类sun.nio.cs.UTF_8注册在 JDK 内建 providersun.nio.cs.StandardCharsets(也是CharsetProvider子类)中;new UTF_8()只是普通构造,但内部已初始化编解码器实现类。- 为什么推荐
StandardCharsets.UTF_8:无查找开销、类型确定、避免运行时依赖字符集是否可用。
③ CharsetDecoder.decode(ByteBuffer in) 的解码流程
java
public final CharBuffer decode(ByteBuffer in) throws CharacterCodingException {
int n = (int)(in.remaining() * averageCharsPerByte()); // ① 按平均比例估容量
CharBuffer out = CharBuffer.allocate(n);
if ((n == 0) && (in.remaining() == 0)) return out;
reset();
for (;;) {
CoderResult cr = in.hasRemaining()
? decodeLoop(in, out) // ② 主循环
: CoderResult.UNDERFLOW;
if (cr.isUnderflow()) break; // ③ 输入耗尽
if (cr.isOverflow()) {
out = CharBuffer.allocate((int)(out.capacity() * 1.5 + 1)); // ④ 扩容
continue;
}
cr.throwException(); // ⑤ 非法输入
}
...
return out.flip(); // ⑥ 翻转供读取
}- 一次
decode内部可能是多轮decodeLoop:容量按averageCharsPerByte估算,UTF-8 为 1.0(一字节一字符),中文字符集往往低于 0.5,溢出后按 1.5 倍扩容重试。 decodeLoop是抽象方法,由具体字符集实现,返回CoderResult:UNDERFLOW表示输入耗尽或解码完成,OVERFLOW表示输出缓冲区不足,MALFORMED/UNMAPPABLE表示数据非法。in.remaining()为零而仍有字符待输出(多字节序列只读了一半)时,会追加implFlush处理收尾。
④ UTF_8 的 decodeLoop 逐字节解析
java
// sun.nio.cs.UTF_8
private CoderResult decodeArrayLoop(ByteBuffer src, CharBuffer dst) {
byte[] sa = src.array();
int sp = src.arrayOffset() + src.position();
char[] da = dst.array();
...
while (sp < sl) {
int b1 = sa[sp];
if (b1 >= 0) { // ① 首字节 < 0x80:ASCII 1 字节
if (dp >= dl) return CoderResult.OVERFLOW;
da[dp++] = (char) b1;
sp++;
continue;
}
if ((b1 & 0xE0) == 0xC0) { // ② 0xC0-0xDF:2 字节
...
} else if ((b1 & 0xF0) == 0xE0) { // ③ 0xE0-0xEF:3 字节
...
} else if ((b1 & 0xF8) == 0xF0) { // ④ 0xF0-0xF7:4 字节
...
} else {
return CoderResult.malformedForLength(1); // ⑤ 非法首字节
}
// 校验后续字节合法性(10xxxxxx)并合并码点
}
return CoderResult.UNDERFLOW;
}- 判定靠首字节高位前缀:
0xxxxxxx单字节、110xxxxx双字节、1110xxxx三字节、11110xxx四字节,码点范围与 UTF-8 规范完全对应。 - 多字节情形还会校验续字节必须是
10xxxxxx,并检查码点是否过度编码(如 3 字节编码 U+0800 以下的字符),非法时返回malformedForLength(1)定位错误字节。 - 命中
OVERFLOW即返回,由上层decode扩容后重新进入decodeLoop——不推进position的字节会重读,状态机保证不丢字节。
⑤ CharsetEncoder.encode(CharBuffer in) 的编码流程
java
public final ByteBuffer encode(CharBuffer in) throws CharacterCodingException {
int n = (int)(in.remaining() * averageBytesPerChar()); // ① 估算输出容量
ByteBuffer out = ByteBuffer.allocate(n);
...
reset();
for (;;) {
CoderResult cr = in.hasRemaining()
? encodeLoop(in, out) // ② 主循环
: CoderResult.UNDERFLOW;
if (cr.isUnderflow()) {
cr = flush(out); // ③ 收尾(如 UTF-16 BOM)
if (cr.isUnderflow()) break;
}
if (cr.isOverflow()) {
out = ByteBuffer.allocate((int)(out.capacity() * 1.5 + 1)); // ④ 扩容
continue;
}
cr.throwException();
}
return out.flip();
}- 与解码对称:
encodeLoop由字符集实现,把CharBuffer转成字节;averageBytesPerChar是经验估算(UTF-8 为 1.0,纯中文场景会偏低触发扩容)。 - 输入耗尽后调用
implFlush(out):部分字符集需要收尾输出(如UTF-16编码时若首字节是 BOM 则在此写入,ISO-2022-JP在此输出终止序列)。 - 编码同样区分非法输入:
MALFORMED(孤立代理项等非法码点)与UNMAPPABLE(字符在目标字符集中不存在)会走CodingErrorAction配置的REPORT/REPLACE/IGNORE。
⑥ CoderResult 的 4 种状态
java
public class CoderResult {
private static final int CR_UNDERFLOW = 0;
private static final int CR_OVERFLOW = 1;
private static final int CR_ERROR_MIN = 2;
private static final int CR_MALFORMED = 2;
private static final int CR_UNMAPPABLE = 3;
public boolean isUnderflow() { return (type == CR_UNDERFLOW); }
public boolean isOverflow() { return (type == CR_OVERFLOW); }
public boolean isMalformed() { return (type == CR_MALFORMED); }
public boolean isUnmappable() { return (type == CR_UNMAPPABLE); }
}UNDERFLOW:输入处理完毕(或解码器需要更多输入),正常推进。OVERFLOW:输出缓冲区已满,调用方应扩容后再次调用(解码器保留未消费输入)。MALFORMED:输入字节/字符本身非法(UTF-8 非法字节序列、孤立代理项),malformedForLength(n)携带错误长度。UNMAPPABLE:输入合法但目标字符集无对应映射(如把中文编进ISO-8859-1),unmappableForLength(n)同样带长度。throwException()把四种状态分别转成BufferUnderflowException、BufferOverflowException、MalformedInputException、UnmappableCharacterException。
⑦ CharsetProvider 的 SPI 加载
java
// sun.nio.cs.ext.ExtendedCharsets(jdk.charsets 模块)
public final class ExtendedCharsets extends AbstractCharsetProvider {
public ExtendedCharsets() {
super("charsets.spi"); // 配置目录名
charset("Big5", "Big5", new String[] { "big5", ... });
charset("GBK", "GBK", new String[] { "GBK", "gbk", "CP936", ... }); // ① 注册
...
}
}
// Charset.lookup 遍历 provider
private static Charset lookup(String charsetName) {
Charset cs = lookupViaProviders(charsetName); // ② 遍历所有 provider
if (cs != null) return cs;
return lookupExtendedCharset(charsetName); // ③ 扩展字符集
}CharsetProvider是公开 SPI:ServiceLoader.load(CharsetProvider.class)找到模块声明的 provider 并调用其charsetForName。- 扩展字符集(
Big5、GBK、GB2312等)位于jdk.charsets模块的sun.nio.cs.ext.ExtendedCharsets,构造时批量charset(...)注册名称与别名。 GBK的别名表包含gbk、CP936等——forName("GBK")经别名归一化后命中同一实例;未内置的字符集还可由应用在 classpath 提供自己的 provider 动态注册。
⑧ Charset.availableCharsets() 的可用编码
java
public static SortedMap<String, Charset> availableCharsets() {
return (SortedMap<String, Charset>) AccessController.doPrivileged(
new PrivilegedAction<Object>() {
public Object run() {
CharsetProvider[] providers = {
new sun.nio.cs.StandardCharsets(), // ① 内建标准集
new sun.nio.cs.ext.ExtendedCharsets(), // ② 扩展集
... // ③ SPI 加载的第三方
};
SortedMap<String, Charset> m =
new TreeMap<>(String.CASE_INSENSITIVE_ORDER); // ④ 大小写不敏感排序
for (CharsetProvider provider : providers) {
for (Charset cs : provider.charsets()) {
if (!m.containsKey(cs.name()))
m.put(cs.name(), cs); // ⑤ 名字去重
String[] aliases = cs.aliases();
...
}
}
return m;
}
});
}- 返回
TreeMap<String, Charset>,键按String.CASE_INSENSITIVE_ORDER排序——输出列表是按名称排序的,方便查找与展示。 - 汇总三类来源:内建标准集、扩展集、
ServiceLoader加载的第三方 provider;同名时内建优先,后续 provider 不覆盖。 - 该方法每次调用都会重新构建 Map(代价较高),JDK 内部不缓存结果,频繁调用时应自行缓存。