PAYATHON 2026

如何让 JDK 21 使用 windows-1252 字符编码?

排障赵师傅

结论

JDK 21 可以通过启动参数,临时恢复旧版 Java 根据平台选择默认编码的行为:

java -Dfile.encoding=COMPAT -jar game.jar

在使用西欧代码页的 Windows 环境中,COMPAT 通常会将默认字符集恢复为 windows-1252。现有的已编译代码也能继续兼容,因为 FileReader(String) 会在运行时使用默认字符集。

启动后,先确认实际生效的编码:

java -Dfile.encoding=COMPAT -XshowSettings:properties -version

检查输出中的以下属性:

file.encoding = windows-1252
native.encoding = windows-1252

如果 native.encoding 不是 windows-1252,COMPAT 就无法达到预期效果。具体结果可能受 Windows 区域设置及“使用 Unicode UTF-8 提供全球语言支持”选项影响。

为什么升级后会出现乱码

从 JDK 18 开始,Java 将 UTF-8 设为标准默认字符集。JDK 8 通常跟随操作系统的活动代码页,所以西欧 Windows 环境经常使用 windows-1252。

原有代码没有显式指定字符集:

FileReader reader = new FileReader(filename);

这里的 FileReader 使用 JVM 默认字符集。这段代码能在 JDK 8 中正常工作,很可能是因为当时的默认字符集恰好与文件编码一致。升级到 JDK 21 后,默认字符集变为 UTF-8,同一批字节会按另一种编码解释,于是产生乱码。

临时兼容已编译程序

参数必须放在 -jar 或主类名称之前:

java -Dfile.encoding=COMPAT -jar game.jar

通过类路径启动程序时:

java -Dfile.encoding=COMPAT -cp game.jar com.example.Main

使用批处理文件时可以这样写:

@echo off
java -Dfile.encoding=COMPAT -jar game.jar

COMPAT 是 JDK 18 以后提供的兼容选项,它会让 JVM 根据本机编码确定默认字符集。在当前场景下,只有当 native.encoding 确实为 windows-1252 时,这种方式才有效。

网上也可能看到这种写法:

java -Dfile.encoding=windows-1252 -jar game.jar

它在某些 JDK 实现中可能生效,但 JDK 21 对 file.encoding 明确定义的启动配置主要是 UTF-8 和 COMPAT。其他取值缺乏可靠的可移植性,不适合作为正式方案。

长期方案:在代码中明确指定字符集

文件编码是文件格式的一部分,不应依赖 JVM、操作系统区域设置或启动方式。修改代码时,可以改用带 Charset 参数的 API:

import java.io.FileReader;
import java.nio.charset.Charset;

Charset charset = Charset.forName("windows-1252");

try (FileReader reader = new FileReader(filename, charset)) {
    int ch;
    while ((ch = reader.read()) != -1) {
        buf.append((char) ch);
    }
}

String str = buf.toString();

也可以使用 NIO:

import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;

Charset charset = Charset.forName("windows-1252");

String str = Files.readString(Path.of(filename), charset);

文件较大时,更适合按行读取:

import java.io.BufferedReader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;

Charset charset = Charset.forName("windows-1252");

try (BufferedReader reader =
         Files.newBufferedReader(Path.of(filename), charset)) {
    String line;
    while ((line = reader.readLine()) != null) {
        // 处理每一行
    }
}

原代码中的 reader.ready() 不能用来判断是否到达文件末尾。它只表示当前读取操作是否会阻塞,并不表示文件中是否还有数据。判断读取结束应以 read() 返回 -1 为准。

.dat 文件需要单独判断

下面的代码读取的是 Java 对象序列化数据:

ObjectInputStream os =
    new ObjectInputStream(new FileInputStream(datfilename));
ExampleComponent =
    (ExampleComponent) os.readObject();

ObjectInputStream 读取的是 Java 序列化协议,不会将整个 .dat 文件作为 windows-1252 文本处理。正常序列化的 String 也不依赖 JVM 当前的默认字符集。因此,仅修改 file.encoding 通常无法解决对象反序列化中的版本不兼容、serialVersionUID 不一致或类结构变化等问题。

以下情况中,默认字符集才可能间接影响 .dat 文件:

  • ExampleComponent 自定义了 readObject(),并在其中使用默认字符集转换字节;
  • 程序在完成反序列化后又读取了外部文本文件;
  • 对象中保存的是需要再次解码的原始字节,并非正常序列化的 String。

如果 .dat 文件由标准的 ObjectOutputStream 生成,其中已有的字符串出现乱码,应检查文件生成过程、自定义序列化代码,以及负责显示字符串的终端或界面。此时不能直接归因于 FileReader 使用了默认字符集。

迁移时的注意事项

-Dfile.encoding=COMPAT 会影响整个 JVM 中所有依赖默认字符集的代码,影响范围并不限于这些旧文件。如果程序还会读取 UTF-8 文件,启用该参数后可能反而出现错误解码。

可以将它作为等待新版本发布期间的临时启动方式。后续应为每类文本文件明确指定字符集,或者统一将文件转换为 UTF-8,并在转换前保留备份。迁移数据库时,也要明确导入数据的源编码,避免先按错误字符集解码,再把已经损坏的字符串写入数据库。

备注:内容仅供参考。