跳到主要内容

JEP 400:默认使用 UTF-8

QWen Max 中英对照 JEP 400: UTF-8 by Default

总结

指定 UTF-8 作为标准 Java API 的默认字符集。通过这一更改,依赖于默认字符集的 API 将在所有实现、操作系统、区域设置和配置中表现一致。

目标

  • 当 Java 程序的代码依赖默认字符集时,使其更加可预测和可移植。

  • 澄清标准 Java API 在哪些地方使用了默认字符集。

  • 在整个标准 Java API 中标准化使用 UTF-8,控制台 I/O 除外。

非目标

  • 本项目的目标并非定义新的标准 Java API 或受支持的 JDK API,尽管这项工作可能会发现一些机会,表明新增便捷方法可以让现有 API 更易于接触或使用。

  • 并不打算弃用或移除那些依赖默认字符集而不是接受显式字符集参数的标准 Java API。

动机

标准的 Java API 用于读取和写入文件以及处理文本时,允许将字符集作为参数传递。字符集控制原始字节与 Java 编程语言中 16 位 char 值之间的转换。支持的字符集包括,例如,US-ASCII、UTF-8 和 ISO-8859-1。

如果未传递字符集参数,则标准 Java API 通常会使用默认字符集。JDK 在启动时根据运行时环境选择默认字符集:操作系统、用户的区域设置以及其他因素。

由于默认字符集在各处并不相同,使用默认字符集的 API 会带来许多不易察觉的风险,即使对经验丰富的开发者也是如此。

考虑一个应用程序创建了一个 java.io.FileWriter,但没有传递字符集,然后使用它将一些文本写入文件。生成的文件将包含使用运行该应用程序的 JDK 的默认字符集编码的字节序列。第二个应用程序,在不同的机器上运行,或者在同一台机器上由不同的用户运行,创建了一个 java.io.FileReader 而没有传递字符集,并使用它来读取该文件中的字节。生成的文本包含使用运行第二个应用程序的 JDK 的默认字符集解码的字符序列。如果第一个应用程序的 JDK 和第二个应用程序的 JDK 的默认字符集不同,那么生成的文本可能会被静默破坏或不完整,因为 FileReader 无法判断它使用相对于 FileWriter 来说是错误的字符集解码了文本。下面是一个这种风险的例子,其中在 macOS 上以 UTF-8 编码的日文文本文件在 Windows 的美式英语或日语区域中读取时遭到破坏:

java.io.FileReader(“hello.txt”) -> “こんにちは” (macOS)
java.io.FileReader(“hello.txt”) -> “ã?“ã‚“ã?«ã?¡ã?(Windows (en-US))
java.io.FileReader(“hello.txt”) -> “縺ォ縺。縺ッ” (Windows (ja-JP)
java

熟悉此类危险的开发者可以使用明确接收字符集参数的方法和构造函数。然而,必须传递参数这一点会阻止方法和构造函数通过方法引用(::)在流管道中使用。

开发者有时会尝试通过在命令行设置系统属性 file.encoding 来配置默认字符集(即,java -Dfile.encoding=...),但这种做法从未被官方支持。此外,在 Java 运行时启动之后,试图通过编程方式(即,System.setProperty(...))设置该属性也是无效的。

并非所有标准 Java API 都会遵从 JDK 默认字符集的选择。例如,java.nio.file.Files 中没有 Charset 参数但用于读取或写入文件的方法被指定为始终使用 UTF-8。较新的 API 默认使用 UTF-8,而较旧的 API 默认使用默认字符集,这对于混合使用多种 API 的应用程序来说是一个隐患。

如果默认字符集在任何地方都指定为相同的话,整个 Java 生态系统都将受益。不关心可移植性的应用程序受到的影响很小,而通过传递字符集参数来拥抱可移植性的应用程序则不会受到影响。UTF-8 已经长期以来是万维网上最常见的字符集。UTF-8 是海量 Java 程序处理的 XML 和 JSON 文件的标准,而且 Java 自己的 API 也越来越倾向于使用 UTF-8,例如,在NIO API属性文件中。因此,将 UTF-8 指定为所有 Java API 的默认字符集是有意义的。

我们认识到,对于迁移到 JDK 18 的程序来说,这一变化可能会对广泛的兼容性产生影响。因此,始终可以恢复到 JDK 18 之前的行为,即默认字符集取决于环境。

描述

在 JDK 17 及更早版本中,默认字符集在 Java 运行时启动时确定。在 macOS 上,除了 POSIX C 区域设置外,默认字符集为 UTF-8。在其他操作系统上,它取决于用户的区域设置和默认编码,例如,在 Windows 上,它是基于代码页的字符集,例如 windows-1252windows-31j。方法 java.nio.charsets.Charset.defaultCharset() 返回默认字符集。查看当前 JDK 默认字符集的快速方法是使用以下命令:

java -XshowSettings:properties -version 2>&1 | grep file.encoding
shell

几个标准的 Java API 使用默认字符集,包括:

  • java.io 包中,InputStreamReaderFileReaderOutputStreamWriterFileWriterPrintStream 定义了构造函数,用于创建使用默认字符集进行编码或解码的读取器、写入器和打印流。

  • java.util 包中,FormatterScanner 定义了结果使用默认字符集的构造函数。

  • java.net 包中,URLEncoderURLDecoder 定义了使用默认字符集的已弃用方法。

我们建议修改 Charset.defaultCharset() 的规范,规定默认字符集为 UTF-8,除非通过某种特定于实现的方式另行配置。(有关如何配置 JDK,请参见下文。)UTF-8 字符集由 RFC 2279 规定;其基于的转换格式在 ISO 10646-1 的附录 2 中规定,并在 Unicode 标准 中也有描述。不要将其与 Modified UTF-8 混淆。

我们将更新所有使用默认字符集的标准 Java API 的规范,以交叉引用 Charset.defaultCharset()。这些 API 包括上面列出的那些,但不包括 System.outSystem.err,它们的字符集将由 Console.charset() 指定。

file.encodingnative.encoding 系统属性

根据 Charset.defaultCharset() 的规范设想,JDK 将允许将默认字符集配置为 UTF-8 以外的其他字符集。我们将修改系统属性 file.encoding 的处理方式,使得在命令行中设置该属性成为配置默认字符集的受支持方法。我们将在 System.getProperties() 的实现说明中将其规定如下:

  • 如果 file.encoding 被设置为 "COMPAT"(即,java -Dfile.encoding=COMPAT),那么默认字符集将是由 JDK 17 及更早版本中的算法根据用户的操作系统、区域设置及其他因素选择的字符集。file.encoding 的值将被设置为该字符集的名称。

  • 如果 file.encoding 被设置为 "UTF-8"(即,java -Dfile.encoding=UTF-8),那么默认字符集将是 UTF-8。这个无操作的值是为了保持现有命令行的行为而定义的。

  • 对于 "COMPAT""UTF-8" 以外的值的处理方式并未明确规定。这些值不受支持,但如果某个这样的值在 JDK 17 中有效,那么它很可能在 JDK 18 中仍然有效。

在部署到默认字符集为 UTF-8 的 JDK 之前,强烈建议开发人员通过在当前 JDK(8-17)上使用 java -Dfile.encoding=UTF-8 ... 启动 Java 运行时来检查字符集问题。

JDK 17 引入了 native.encoding 系统属性,作为程序获取 JDK 算法所选择字符集的标准方式,无论默认字符集是否实际配置为该字符集。在 JDK 18 中,如果在命令行中将 file.encoding 设置为 COMPAT,那么 file.encoding 的运行时值将与 native.encoding 的运行时值相同;如果在命令行中将 file.encoding 设置为 UTF-8,那么 file.encoding 的运行时值可能与 native.encoding 的运行时值不同。

在下面的Risks and Assumptions中,我们讨论了如何缓解由于 file.encoding 的这一更改以及 native.encoding 系统属性和应用程序建议而产生的可能不兼容问题。

JDK 内部使用了三个与字符集相关的系统属性。它们仍然是未指定且不受支持的,但为了完整起见,在此进行记录:

  • sun.stdout.encodingsun.stderr.encoding — 用于标准输出流(System.out)和标准错误流(System.err)的字符集名称,也用于 java.io.Console API。

  • sun.jnu.encoding — 当编码或解码文件名路径时(与文件内容相对),java.nio.file 实现所使用的字符集名称。在 macOS 上其值为 "UTF-8";在其他平台上通常为默认字符集。

源文件编码

Java 语言允许源代码以 UTF-16 编码 表达 Unicode 字符,而这一特性不会受到将 UTF-8 作为默认字符集的影响。然而,javac 编译器会受到影响,因为它假定 .java 源文件是使用默认字符集编码的,除非通过 -encoding 选项 进行了其他配置。如果源文件是以非 UTF-8 编码保存,并且使用早期的 JDK 编译过,那么在 JDK 18 或更高版本上重新编译时可能会出现问题。例如,如果一个非 UTF-8 源文件中包含带有非 ASCII 字符的字符串字面量,在 JDK 18 或更高版本中,除非使用 -encoding,否则 javac 可能会错误地解析这些字面量。

在默认字符集为 UTF-8 的 JDK 上编译之前,强烈建议开发者通过在当前 JDK(8-17)上使用 javac -encoding UTF-8 ... 来检查字符集问题。或者,偏好使用非 UTF-8 编码保存源文件的开发者,可以通过将 -encoding 选项设置为 JDK 17 及更高版本上的 native.encoding 系统属性值,防止 javac 默认使用 UTF-8。

传统的 default 字符集

在 JDK 17 及更早版本中,名称 default 被识别为 US-ASCII 字符集的别名。也就是说,Charset.forName("default") 的结果与 Charset.forName("US-ASCII") 相同。default 别名是在 JDK 1.5 中引入的,以确保使用了 sun.io 转换器的旧代码能够迁移到 JDK 1.4 中引入的 java.nio.charset 框架。

如果默认字符集被指定为 UTF-8,那么对于 JDK 18 来说,保留 default 作为 US-ASCII 的别名将会造成极大的混淆。当用户通过在命令行设置 -Dfile.encoding=COMPAT 将默认字符集配置为其 JDK 18 之前的值时,default 表示 US-ASCII 同样会令人困惑。重新定义 default,使其不再是 US-ASCII 的别名,而是默认字符集(无论是 UTF-8 还是用户配置的字符集)的别名,这将会导致调用 Charset.forName("default") 的(少数)程序出现细微的行为变化。

我们相信,在 JDK 18 中继续承认 default 将会延长一个糟糕的决定。它既不是由 Java SE 平台定义的,也不被 IANA 认可为任何字符集的名称或别名。实际上,对于基于 ASCII 的网络协议,IANA 鼓励使用规范名称 US-ASCII,而不是仅仅使用 ASCII 或者像 ANSI_X3.4-1968 这样的晦涩别名 —— 显然,使用 JDK 特定的别名 default 与此建议背道而驰。Java 程序可以使用枚举常量 StandardCharsets.US_ASCII 来明确表达其意图,而不是将字符串传递给 Charset.forName(...)

相应地,在 JDK 18 中,Charset.forName("default") 将抛出 UnsupportedCharsetException。这将使开发人员有机会检测到该惯用法的使用,并迁移到 US-ASCIICharset.defaultCharset() 的结果。

测试

  • 需要进行大量的测试,以了解此变更对兼容性影响的程度。拥有地域多样化用户群体的开发者或组织需要进行测试。

  • 开发者可以通过在任何带有此变更的早期访问或 GA 版本发布之前,使用 -Dfile.encoding=UTF-8 参数运行现有的 JDK 发行版来检查问题。

风险与假设

我们假设在许多环境中的应用程序不会受到 Java 选择 UTF-8 的影响:

  • 在 macOS 上,除了配置为使用 POSIX C 语言环境外,UTF-8 已经是多个版本的默认字符集。

  • 在许多 Linux 发行版中,尽管不是全部,但默认字符集是 UTF-8,因此在这些环境中不会察觉到任何变化。

  • 许多服务器应用程序已经使用 -Dfile.encoding=UTF-8 启动,因此它们不会经历任何变化。

在其他环境中,在超过 20 年的时间之后将默认字符集更改为 UTF-8 可能会带来显著的风险。最明显的风险是,那些隐式依赖默认字符集的应用程序(例如,未向 API 传递明确的字符集参数)在处理之前默认字符集未指定时生成的数据时会出现行为错误。另一个风险是可能会悄无声息地发生数据损坏。我们预计主要影响将是使用亚洲语言环境的 Windows 用户,以及可能一些亚洲及其他语言环境中的服务器环境。可能的情景包括:

  • 如果一个使用 windows-31j 作为默认字符集运行多年的应用程序升级到使用 UTF-8 作为默认字符集的 JDK 版本,那么在读取以 windows-31j 编码的文件时会出现问题。在这种情况下,可以修改应用程序代码,在打开此类文件时传递 windows-31j 字符集。如果代码无法修改,那么在启动 Java 运行时添加 -Dfile.encoding=COMPAT 参数将强制默认字符集保持为 windows-31j,直到应用程序更新或文件转换为 UTF-8。

  • 在多个 JDK 版本共存的环境中,用户可能无法交换文件数据。例如,一个用户使用较旧的 JDK 版本(默认字符集为 windows-31j),而另一个用户使用较新的 JDK 版本(默认字符集为 UTF-8),那么第一个用户创建的文本文件可能无法被第二个用户读取。在这种情况下,使用旧版 JDK 的用户可以在启动应用程序时指定 -Dfile.encoding=UTF-8,或者使用新版 JDK 的用户可以指定 -Dfile.encoding=COMPAT

如果可以更改应用程序代码,那么我们建议将其更改为向构造函数传递字符集参数。如果某个应用程序对字符集没有特别的偏好,并且对默认字符集的传统环境驱动选择感到满意,那么可以使用以下代码 在所有 Java 版本中 获取由环境确定的字符集:

String encoding = System.getProperty("native.encoding");  // Populated on Java 18 and later
Charset cs = (encoding != null) ? Charset.forName(encoding) : Charset.defaultCharset();
var reader = new FileReader("file.txt", cs);
java

如果既不能更改应用代码,也不能更改 Java 启动配置,那么就需要检查应用代码,手动确定其是否能在 JDK 18 上兼容运行。

替代方案

  • 保持现状 — 这并不能消除上述危害。

  • 弃用 Java API 中使用默认字符集的所有方法 — 这会鼓励开发者使用带有字符集参数的构造函数和方法,但这样产生的代码会更加冗长。

  • 指定 UTF-8 为默认字符集,且不提供任何更改它的方法 — 此变更的兼容性影响太大。