跳到主要内容

JEP 400:默认为 UTF-8

概括

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

目标

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

  • 澄清标准 Java API 在何处使用默认字符集。

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

非目标

  • 定义新的标准 Java API 或受支持的 JDK API 并不是我们的目标,尽管这一努力可能会发现新的便利方法可能使现有 API 更易于使用或更易于使用的机会。

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

动机

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

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

由于默认字符集在各处并不相同,因此使用默认字符集的 API 会带来许多不明显的危险,即使对于经验丰富的开发人员也是如此。

考虑一个应用程序创建一个java.io.FileWriter而不传递字符集,然后使用它向文件写入一些文本。生成的文件将包含使用运行应用程序的 JDK 的默认字符集编码的字节序列。第二个应用程序在不同的计算机上运行或由同一计算机上的不同用户运行,创建一个java.io.FileReader不传递字符集的文件,并使用它来读取该文件中的字节。生成的文本包含使用运行第二个应用程序的 JDK 的默认字符集解码的字符序列。如果第一个应用程序的 JDK 和第二个应用程序的 JDK 之间的默认字符集不同,则生成的文本可能会默默地损坏或不完整,因为无法_判断_FileReader它使用相对于.以下是这种危险的一个示例,其中在 macOS 上编码的日语文本文件在美式英语或日语语言环境的 Windows 上读取时会损坏:FileWriter``UTF-8

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-1252windows-31j。该方法java.nio.charsets.Charset.defaultCharset()返回默认字符集。查看当前 JDK 的默认字符集的快速方法是使用以下命令:

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

一些标准 Java API 使用默认字符集,包括:

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

  • 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 包括上面列出的 API,但不包括System.outSystem.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 上之前,强烈建议开发人员通过在java -Dfile.encoding=UTF-8 ...当前 JDK (8-17) 上启动 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.encoding和— 用于标准输出流 ( ) 和标准错误流 ( ) 以及 API 中的sun.stderr.encoding字符集名称。System.out``System.err``java.io.Console

  • 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 字符,则这些文字可能会javac在 JDK 18 或更高版本中被误解,除非-encoding使用 。

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

遗留default字符集

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

当默认字符集指定为 时, JDK 18 保留default为别名会非常混乱。当用户通过在命令行上设置将默认字符集配置为其 JDK 18 之前的值时,这也会令人困惑。重新定义为不是默认字符集而是默认字符集(无论是用户配置的)的别名将导致调用.US-ASCII``UTF-8``default``US-ASCII``-Dfile.encoding=COMPAT``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()

测试

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

  • -Dfile.encoding=UTF-8开发人员可以通过在任何具有此更改的早期访问或 GA 版本之前运行来检查现有 JDK 版本是否存在问题。

风险和假设

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

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

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

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

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

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

  • 在使用多个 JDK 版本的环境中,用户可能无法交换文件数据。例如,如果一个用户使用较旧的 JDK 版本(windows-31j默认为 UTF-8),而另一个用户使用较新的 JDK(默认为 UTF-8),则第一个用户创建的文本文件可能无法被第二个用户读取。在这种情况下,使用较旧 JDK 版本的用户可以-Dfile.encoding=UTF-8在启动应用程序时指定,或者使用较新版本的用户可以指定-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 指定为默认字符集,而不提供任何更改它的方法- 此更改对兼容性的影响太大。