JEP 400:默认使用 UTF-8
总结
指定 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)
熟悉此类危险的开发者可以使用明确接收字符集参数的方法和构造函数。然而,必须传递参数这一点会阻止方法和构造函数通过方法引用(::
)在流管道中使用。
开发者有时会尝试通过在命令行设置系统属性 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-1252
或 windows-31j
。方法 java.nio.charsets.Charset.defaultCharset()
返回默认字符集。查看当前 JDK 默认字符集的快速方法是使用以下命令:
java -XshowSettings:properties -version 2>&1 | grep file.encoding
几个标准的 Java API 使用默认字符集,包括:
-
在
java.io
包中,InputStreamReader
、FileReader
、OutputStreamWriter
、FileWriter
和PrintStream
定义了构造函数,用于创建使用默认字符集进行编码或解码的读取器、写入器和打印流。 -
在
java.util
包中,Formatter
和Scanner
定义了结果使用默认字符集的构造函数。 -
在
java.net
包中,URLEncoder
和URLDecoder
定义了使用默认字符集的已弃用方法。
我们建议修改 Charset.defaultCharset()
的规范,规定默认字符集为 UTF-8,除非通过某种特定于实现的方式另行配置。(有关如何配置 JDK,请参见下文。)UTF-8 字符集由 RFC 2279 规定;其基于的转换格式在 ISO 10646-1 的附录 2 中规定,并在 Unicode 标准 中也有描述。不要将其与 Modified UTF-8 混淆。
我们将更新所有使用默认字符集的标准 Java API 的规范,以交叉引用 Charset.defaultCharset()
。这些 API 包括上面列出的那些,但不包括 System.out
和 System.err
,它们的字符集将由 Console.charset()
指定。
file.encoding
和 native.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.encoding
和sun.stderr.encoding
— 用于标准输出流(System.out
)和标准错误流(System.err
)的字符集名称,也用于java.io.Console
API。 -
sun.jnu.encoding
— 当编码或解码文件名路径时(与文件内容相对),java.nio.file
实现所使用的字符集名称。在 macOS 上其值为"UTF-8"
;在其他平台上通常为默认字符集。
源文件编码
在默认字符集为 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-ASCII
或 Charset.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 启动配置,那么就需要检查应用代码,手动确定其是否能在 JDK 18 上兼容运行。
替代方案
-
保持现状 — 这并不能消除上述危害。
-
弃用 Java API 中使用默认字符集的所有方法 — 这会鼓励开发者使用带有字符集参数的构造函数和方法,但这样产生的代码会更加冗长。
-
指定 UTF-8 为默认字符集,且不提供任何更改它的方法 — 此变更的兼容性影响太大。