ChatGPT解决这个技术问题 Extra ChatGPT

如何在 Java 中查找默认字符集/编码?

显而易见的答案是使用 Charset.defaultCharset(),但我们最近发现这可能不是正确的答案。有人告诉我,结果与 java.io 类在多个场合使用的真实默认字符集不同。看起来 Java 保留了 2 组默认字符集。有人对这个问题有任何见解吗?

我们能够重现一个失败案例。这是一种用户错误,但它仍然可能暴露所有其他问题的根本原因。这是代码,

public class CharSetTest {

    public static void main(String[] args) {
        System.out.println("Default Charset=" + Charset.defaultCharset());
        System.setProperty("file.encoding", "Latin-1");
        System.out.println("file.encoding=" + System.getProperty("file.encoding"));
        System.out.println("Default Charset=" + Charset.defaultCharset());
        System.out.println("Default Charset in Use=" + getDefaultCharSet());
    }

    private static String getDefaultCharSet() {
        OutputStreamWriter writer = new OutputStreamWriter(new ByteArrayOutputStream());
        String enc = writer.getEncoding();
        return enc;
    }
}

我们的服务器需要 Latin-1 中的默认字符集来处理旧协议中的一些混合编码(ANSI/Latin-1/UTF-8)。所以我们所有的服务器都使用这个 JVM 参数运行,

-Dfile.encoding=ISO-8859-1

这是 Java 5 上的结果,

Default Charset=ISO-8859-1
file.encoding=Latin-1
Default Charset=UTF-8
Default Charset in Use=ISO8859_1

有人试图通过在代码中设置 file.encoding 来更改编码运行时。我们都知道这是行不通的。但是,这显然会抛出 defaultCharset(),但不会影响 OutputStreamWriter 使用的真正默认字符集。

这是错误还是功能?

编辑:接受的答案显示了问题的根本原因。基本上,您不能信任 Java 5 中的 defaultCharset(),这不是 I/O 类使用的默认编码。看起来 Java 6 纠正了这个问题。

这很奇怪,因为 defaultCharset 使用仅设置一次的静态变量(根据文档 - 在 VM 启动时)。您使用的是哪个 VM 供应商?
我能够在 Sun/Linux 和 Apple/OS X 上的 Java 5 上重现这一点。
这就解释了为什么 defaultCharset() 不缓存结果。我仍然需要找出 IO 类使用的真正默认字符集是什么。必须在其他地方缓存另一个默认字符集。
@ZZ Coder,我仍在研究。我唯一知道的是 Charset.defaulyCharset() 不是从 JVM 1.5 中的 sun.nio.cs.StreamEncoder 调用的。在 JVM 1.6 中,调用 Charset.defaulyCharset() 方法给出了预期的结果。 StreamEncoder 的 JVM 1.5 实现以某种方式缓存了以前的编码。
仅供参考,有一个正式提议“将 UTF-8 指定为 Java SE API 的默认字符集,以便依赖于默认字符集的 API 在所有 JDK 实现中的行为一致,并且独立于用户的操作系统、语言环境和配置。”请参阅 JEP 400: UTF-8 by Default,于 2021 年 3 月 30 日更新。

M
Moshe Slavin

这真的很奇怪......一旦设置,默认的字符集就会被缓存,并且当类在内存中时它不会改变。用 System.setProperty("file.encoding", "Latin-1"); 设置 "file.encoding" 属性没有任何作用。每次调用 Charset.defaultCharset() 时,它都会返回缓存的字符集。

这是我的结果:

Default Charset=ISO-8859-1
file.encoding=Latin-1
Default Charset=ISO-8859-1
Default Charset in Use=ISO8859_1

我使用的是 JVM 1.6。

(更新)

好的。我确实用 JVM 1.5 重现了你的错误。

查看 1.5 的源代码,未设置缓存的默认字符集。我不知道这是否是一个错误,但 1.6 更改了这个实现并使用了缓存的字符集:

JVM 1.5:

public static Charset defaultCharset() {
    synchronized (Charset.class) {
        if (defaultCharset == null) {
            java.security.PrivilegedAction pa =
                    new GetPropertyAction("file.encoding");
            String csn = (String) AccessController.doPrivileged(pa);
            Charset cs = lookup(csn);
            if (cs != null)
                return cs;
            return forName("UTF-8");
        }
        return defaultCharset;
    }
}

JVM 1.6:

public static Charset defaultCharset() {
    if (defaultCharset == null) {
        synchronized (Charset.class) {
            java.security.PrivilegedAction pa =
                    new GetPropertyAction("file.encoding");
            String csn = (String) AccessController.doPrivileged(pa);
            Charset cs = lookup(csn);
            if (cs != null)
                defaultCharset = cs;
            else
                defaultCharset = forName("UTF-8");
        }
    }
    return defaultCharset;
}

当您在下次调用 Charset.defaultCharset() 时将文件编码设置为 file.encoding=Latin-1 时,会发生什么情况,因为未设置缓存的默认字符集,它将尝试为名称 Latin-1 找到适当的字符集。未找到此名称,因为它不正确,并返回默认的 UTF-8

至于为什么 OutputStreamWriter 等 IO 类会返回意外结果,
sun.nio.cs.StreamEncoder(这些 IO 类使用 witch)的实现在 JVM 1.5 和 JVM 1.6 中也是不同的。 JVM 1.6 实现基于 Charset.defaultCharset() 方法来获取默认编码(如果未提供给 IO 类)。 JVM 1.5 实现使用不同的方法 Converters.getDefaultEncodingName(); 来获取默认字符集。此方法使用它自己的在 JVM 初始化时设置的默认字符集的缓存:

JVM 1.6:

public static StreamEncoder forOutputStreamWriter(OutputStream out,
        Object lock,
        String charsetName)
        throws UnsupportedEncodingException
{
    String csn = charsetName;
    if (csn == null)
        csn = Charset.defaultCharset().name();
    try {
        if (Charset.isSupported(csn))
            return new StreamEncoder(out, lock, Charset.forName(csn));
    } catch (IllegalCharsetNameException x) { }
    throw new UnsupportedEncodingException (csn);
}

JVM 1.5:

public static StreamEncoder forOutputStreamWriter(OutputStream out,
        Object lock,
        String charsetName)
        throws UnsupportedEncodingException
{
    String csn = charsetName;
    if (csn == null)
        csn = Converters.getDefaultEncodingName();
    if (!Converters.isCached(Converters.CHAR_TO_BYTE, csn)) {
        try {
            if (Charset.isSupported(csn))
                return new CharsetSE(out, lock, Charset.forName(csn));
        } catch (IllegalCharsetNameException x) { }
    }
    return new ConverterSE(out, lock, csn);
}

但我同意评论。你不应该依赖这个属性。这是一个实现细节。


要重现此错误,您必须使用 Java 5,并且您的 JRE 默认编码必须是 UTF-8。
这是写实现,而不是抽象。如果您依赖未记录的内容,当您升级到更新版本的平台时,如果您的代码中断,请不要感到惊讶。
M
McDowell

这是错误还是功能?

看起来像未定义的行为。我知道,在实践中,您可以使用命令行属性更改默认编码,但我认为没有定义执行此操作时会发生什么。

Bug ID: 4153515 关于设置此属性的问题:

这不是错误。 J2SE 平台规范不需要“file.encoding”属性;它是 Sun 实现的内部细节,不应由用户代码检查或修改。它也是只读的;技术上不可能支持在命令行上或在程序执行期间的任何其他时间将此属性设置为任意值。更改 VM 和运行时系统使用的默认编码的首选方法是在启动 Java 程序之前更改底层平台的语言环境。

当我看到人们在命令行上设置编码时,我感到畏缩——你不知道会影响什么代码。

如果您不想使用默认编码,请通过适当的方法/constructor 明确设置您想要的编码。


A
Andrii Abramov

这种行为并不是那么奇怪。查看类的实现,它是由以下原因引起的:

Charset.defaultCharset() 没有缓存 Java 5 中确定的字符集。

设置系统属性“file.encoding”并再次调用 Charset.defaultCharset() 会导致系统属性的第二次评估,没有找到名称为“Latin-1”的字符集,因此 Charset.defaultCharset() 默认为“UTF -8"。

然而,OutputStreamWriter 正在缓存默认字符集,并且可能已经在 VM 初始化期间使用,因此如果系统属性“file.encoding”在运行时已更改,则其默认字符集从 Charset.defaultCharset() 转移。

正如已经指出的那样,没有记录 VM 在这种情况下必须如何表现。 Charset.defaultCharset() API 文档对如何确定默认字符集不是很精确,只提到它通常在 VM 启动时完成,具体取决于操作系统默认字符集或默认语言环境等因素。


S
Sean Owen

首先,Latin-1 与 ISO-8859-1 相同,所以默认值对您来说已经可以了。正确的?

您使用命令行参数成功地将编码设置为 ISO-8859-1。您还以编程方式将其设置为“Latin-1”,但是,这不是 Java 文件编码的公认值。请参阅http://java.sun.com/javase/6/docs/technotes/guides/intl/encoding.doc.html

当您这样做时,从查看源代码来看,Charset 似乎重置为 UTF-8。这至少解释了大部分行为。

我不知道为什么 OutputStreamWriter 显示 ISO8859_1。它委托给闭源 sun.misc.* 类。我猜它并没有完全通过相同的机制处理编码,这很奇怪。

但当然,您应该始终在此代码中指定您所指的编码。我永远不会依赖平台默认值。


D
Davy Jones

我已将 WAS 服务器中的 vm 参数设置为 -Dfile.encoding=UTF-8 以更改服务器的默认字符集。


n
neoedmund

查看

System.getProperty("sun.jnu.encoding")

它似乎与系统命令行中使用的编码相同。