Skip to content

JEP 400: UTF-8 by Default | 默认使用 UTF-8

摘要

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

目标

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

  • 明确标准 Java API 中使用默认字符集的位置。

  • 在标准 Java API 中(控制台 I/O 除外)统一使用 UTF-8。

非目标

  • 并不旨在定义新的标准 Java API 或受支持的 JDK API,尽管这一努力可能会发现新的便利方法可以使现有 API 更易于接近或使用。

  • 没有意图弃用或删除依赖于默认字符集而不是显式字符集参数的标准 Java API。

动机

Java 标准 API 用于读写文件和处理文本时,允许将 字符集(charset)作为参数传递。字符集控制原始字节与 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 的 US-English 或 Japanese 区域设置中读取时会损坏:

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

熟悉此类风险的开发人员可以使用明确接受字符集参数的方法和构造函数。但是,需要传递参数会阻止在流管道中通过方法引用(::)使用这些方法和构造函数。

开发人员有时会尝试通过在命令行上设置系统属性 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 也越来越倾向于在例如 NIO API属性文件 中使用 UTF-8。因此,将所有 Java API 的默认字符集指定为 UTF-8 是有意义的。

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

描述

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

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

多个标准 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 包括上面列出的 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 的运行时值不同。

在以下“风险和假设”部分中,我们讨论了如何缓解由于 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 字符的字符串字面量,则除非使用 -encoding,否则这些字面量在 JDK 18 或更高版本的 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() 的结果。

测试

  • 需要进行大量测试,以了解此更改对兼容性的影响程度。需要地理位置不同的用户群体的开发人员或组织进行测试。

  • 在包含此更改的任何早期访问或全面发行版之前,开发人员可以通过运行带有 -Dfile.encoding=UTF-8 的现有 JDK 版本来检查问题。

风险和假设

我们假设在许多环境中,Java 选择 UTF-8 对应用程序没有影响:

  • 在 macOS 上,默认字符集自多个版本以来一直是 UTF-8,除非配置为使用 POSIX C 语言环境。

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

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

在其他环境中,在 20 多年后更改默认字符集为 UTF-8 的风险可能很大。最明显的风险是,隐式依赖默认字符集的应用程序(例如,不向 API 传递显式字符集参数)在处理默认字符集未指定时生成的数据时,行为将不正确。另一个风险是数据可能会默默损坏。我们预计主要影响将是亚洲语言环境的 Windows 用户,以及亚洲和其他语言环境的某些服务器环境。可能的情况包括:

  • 如果默认字符集为 windows-31j 的应用程序在升级到使用 UTF-8 作为默认字符集的 JDK 版本后,在读取以 windows-31j 编码的文件时会遇到问题。在这种情况下,可以更改应用程序代码,以便在打开此类文件时传递 windows-31j 字符集。如果无法更改代码,则可以通过使用 -Dfile.encoding=COMPAT 启动 Java 运行时环境,将默认字符集强制为 windows-31j,直到更新应用程序或将文件转换为 UTF-8。

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

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

java
String encoding = System.getProperty("native.encoding");  // 在 Java 18 及更高版本中填充
Charset cs = (encoding != null) ? Charset.forName(encoding) : Charset.defaultCharset();
var reader = new FileReader("file.txt", cs);

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

替代方案

  • 维持现状 — 这不会消除上述描述的风险。

  • 弃用 Java API 中所有使用默认字符集的方法 — 这将鼓励开发人员使用接受字符集参数的构造函数和方法,但生成的代码将更加冗长。

  • 将 UTF-8 指定为默认字符集且不提供任何更改方式 — 此更改的兼容性影响将过高。