在Java中确定平台的默认字符集

10

我正在使用Java编程。

我的代码如下:

byte[] b = test.getBytes();

在API中指定了,如果我们不指定字符编码,则会采用默认的平台字符编码。
“默认平台字符编码”是什么意思?
它是指Java编码还是操作系统编码?
如果它是指操作系统编码,那么我如何检查Windows和Linux的默认字符编码?有没有办法使用命令行获取默认字符编码?

你应该明确你的意思。首先说明为什么需要这些信息。 - Matthew Flaschen
如果您找不到之前提出的问题,只需单击任何显示为链接的名称,例如此处:Anand和顶部栏中的名称。您可以在那里找到问题,您几乎未接受问题(注意:问题分布在多个页面上!)。再次查看它们并投票/接受一些。 - BalusC
3个回答

33
系统属性file.encoding是JVM供应商特定的。在这种情况下,它只适用于Sun JVM,并且可能无法在非Sun供应商的JVM上使用。更好的选择是使用Java SE API提供的Charset#defaultCharset()
Charset defaultCharset = Charset.defaultCharset();

3
刚刚我遇到了一个问题。我正在调试一个使用UTF-8编码的eclipse项目。看起来eclipse会自动将新的运行配置(lauch)JVM编码设置成与项目资源编码相同(在我的情况下是UTF-8)。但是,我的开发机器操作系统(Win7)的编码是Cp1252。因此,只要我能控制JVM供应商,我就会使用"file.encoding"系统属性,避免使用Charset.defaultCharset()。顺便说一下,我正在使用jdk 1.7.0_17(32位)。 - Reto Höhener
我收回之前评论的结论。我刚刚注意到在我上述描述的情况下,“file.encoding”也是UTF-8。现在我完全不知道如何找出真正的操作系统编码了。 - Reto Höhener
BalusC!非常感谢您在社区中的辛勤工作。您是我的个人英雄。无论如何,您是否有官方文档的URL,描述了file.encoding属性? - Martin Andersson
3
换句话说,当前被接受的答案完全错误,提问者和回答者都应该感到羞耻。 - BalusC

4

这意味着你正在运行的JVM的默认字符编码,

要检查默认编码,您可以执行以下操作:

System.getProperty("file.encoding");

这将返回默认编码(也是上面getBytes()使用的编码)。


3
但是没必要费心了。使用String.getBytes()的有效理由非常少,如果您确实需要使用它,您应该始终指定编码而不是依赖默认值。对于new String(byte [])构造函数也是如此。 - Alan Moore
这个答案完全是错误的。file.encoding返回的是像COMPAT这样的东西。将其读出来对于试图弄清楚任何事情的客户端代码来说毫无意义。 - rzwitserloot

2

这个问题的答案随着Java 18的发布而改变了.

自Java 18起,JVM的默认字符集始终为UTF-8,不管底层操作系统的平台默认字符集是什么(JEP-400)。

这几乎影响到所有内容:

  • Charset.defaultCharset()(始终返回与StandardCharsets.UTF8相同的内容,如果您想要使用后者,请使用它。
  • System.getProperty("file.encoding")的值。实际上,这不是任何相关的编码,因为这是您发送给JVM的参数,而不是您打算读取的内容。它一直以这种方式工作-例如,它可以获取到值COMPAT。这不是一个编码,而是表示:嘿,JVM,不要使用UTF-8,而要使用操作系统的默认编码。因此,请不要读取此值
  • 将字节转换为字符或反之的每个方法的行为,并且不接受显式的Charset参数。例如new String(bytes)someStr.getBytes()new FileWriter(filePath)等等。然而,但是java.nio.file.Files中执行此操作的所有方法都使用UTF-8,并且一直如此,无论Charset.defaultCharset()如何。不过,您可以对其进行更改。例如,通过调用java -Dfile.encoding=COMPAT - 然后这些方法都会使用您平台的本地编码。
你的问题可以分解为两个微妙不同的问题:
1. 如果我使用这些 `new String(byteArr)` 样式的方法,Java 实际上使用的是哪种编码方式? 2. 操作系统的平台编码是什么 - 如果我写一个文本文件,用户以后想要通过 `cat thatfile.txt` 命令来查看它,我应该用什么编码方式来写入,以便他们能够阅读,假设他们没有更改操作系统的默认编码(如果他们更改了,那就无法预测了)。
对于第一个问题,正确的答案是 `Charset.defaultCharSet()`。
对于第二个问题,情况要复杂得多。因为这取决于 JVM 的版本 - 这意味着除非你建立一个复杂的测试框架,在多个 JVM 版本上运行单元测试,否则很难确保你做得正确。
Java 18+ 的处理方式如下:
Charset c = System.console().charset();

从技术上讲,它并不返回"操作系统的本地编码",而是返回"控制台使用的编码"。一个特定的问题是,各个平台可能没有控制台(上述代码会导致NullPointerException - console()方法的规范说明可能返回null)。通常,集成开发环境(IDE)会有一个类似控制台的东西,可以直接在IDE内部进行键盘输入和输出,但它们经常没有控制台,或者实现有问题。因此,我强烈建议您不要使用上述代码,而是使用以下代码:
Charset c = Charset.forName(System.getProperty("native.encoding"));

此外,System.console().charset() 在 JDK16 及以下版本会导致 NoSuchMethodError 错误 - 因为它是在 JDK17 中引入的。这是使用 native.encoding 系统属性的另一个原因。
Java 8
不幸的是,native.encoding 系统属性只在 JDK18 中添加,而 Console.charset() 则在 JDK17 中添加。因此,在 Java 8 中,上述代码将失败,因为该属性的值为 null(因为它不存在)/ 无法编译 / 会导致 NoSuchMethodError。你只需使用 Charset.defaultCharset(),因为他们还没有经过 JEP400。
Java 的任何版本
因此,你会陷入这个彻底混乱的局面,以获取与 JVM 版本无关的平台本地编码。
String nativeEncodingDescriptor = System.getProperty("native.encoding");
Charset nativeEncoding = nativeEncodingDescriptor == null ?
   Charset.defaultCharset() :
   Charset.forName(nativeEncodingDescriptor);

你会认为会有一个实用方法来解决这个问题。当然,它并不存在:如果核心Java库要添加它,你就无法在JDK8上调用它,因为它在那时还不存在。因此,你需要自己编写这个实用方法。或许可以从每个人都会搞砸这一事实中找到一些安慰。
关于输出
绝大多数需要确定本地编码的需求是为了向System.out输出;毕竟,那是与系统交互的,而且它们都是基于字节的(in和out都是)。
幸运的是,代表System.out的PrintWriter已经正确配置了;它的字符集编码将是本地编码。你只需在文本文件中调用System.out.println("☃"),使用UTF-8编码保存它,使用javac命令编译时加上-encoding UTF8参数,然后运行生成的class文件,假设你的控制台字体支持Unicode雪人,你就能看到一个雪人。在任何JDK版本上都可以实现。

这个规则更加棘手,如果你直接将文本数据传输给基于字节的PrintStream方法。你需要应用正确的编码;使用上面的代码片段来确定。

System.in是一个更大的问题

这是一个简单的InputStream,它没有任何读取字符的方法。想要读取键盘输入的人往往会把System.in传给Scanner。这是一个错误(Scanner并不是真正设计用来解析键盘输入的;这就是为什么每第五个带java标签的问题都涉及对其工作原理的误解的原因)。

在JDK18+上有问题。因为scanner会应用默认字符集(即UTF-8,在JDK18+上)。是的,最常见的Java代码之一:new Scanner(System.in),现在已经无法正常工作了。

“第一步Java”正在进行改进,其中一个改进就是希望放弃Scanner,写一个更好的“与控制台交互”的概念。相关OpenJDK邮件列表上的最新信息似乎表明这可能会发生。

一旦这些API发布,就可以使用它们。在那之前,这确实是编写一个非常简单的Java控制台应用程序的唯一方法:

public class ExampleApp {
  public static void main(String[] args) throws Exception {
    Scanner keyboard = getKeyboard();
  }

  static Scanner getKeyboard() {
    Scanner s = new Scanner(System.in. getNativeCharset());
    s.useDelimiter("\\R"); // Fix the nextLine v nextX nuttiness.
  }

  static Charset getNativeCharset() {
    String nativeEncodingDescriptor = System.getProperty("native.encoding");
    return nativeEncodingDescriptor == null ?
      Charset.defaultCharset() :
      Charset.forName(nativeEncodingDescriptor);
}

此外,在java.nio.file包之外的核心库中,每个将字节转换为字符或反之亦然并默认使用字符集的方法(例如new String(bytes)或整个FileWriter)都是错误的且无法挽回:如果您的IDE或代码检查工具支持禁用某些绝对不能使用的方法/构造函数的列表,那么所有这些方法都应该在列表中。
每次编写将字节转换为字符或反之亦然的代码时,您应该调用允许您明确定义字符集的变体。然后编写代码以消除任何混淆;使用StandardCharsets.UTF_8,或者如果您打算使用本地编码,则使用您在项目中添加的.getNativeEncoding()实用方法,该方法从上面的片段中粘贴而来。 java.nio.file是一个例外 - 如果您想要使用UTF-8,可以调用不带字符集的变体。我可以理解为什么还要调用带字符集的版本,以避免混淆。毕竟,没有字符集的版本在JDK17-和JDK18+上的行为不同,因此是错误的。

如果您的命令行 Java 应用程序不像那样,或者调用了其中任何一个方法,它可能存在细微的错误。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接