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


92

显而易见的答案是使用,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供应商?
09年

我能够重现于Java 5本,无论是在Sun / Linux和苹果/ OS X.
ZZ编码器

这就解释了为什么defaultCharset()不缓存结果。我仍然需要找出IO类使用的真正默认字符集是什么。必须在其他地方缓存另一个默认字符集。
ZZ Coder 2009年

@ZZ Coder,我仍在对此进行研究。我唯一知道的想法是,JVM 1.5中的sun.nio.cs.StreamEncoder没有调用Charset.defaulyCharset()。在JVM 1.6中,调用Charset.defaulyCharset()方法可提供预期的结果。StreamEncoder的JVM 1.5实现以某种方式缓存了以前的编码。
bruno conde

Answers:


62

这真的很奇怪。。。一旦设置,默认的Charset将被缓存,并且在类在内存中时不会更改。将"file.encoding"属性设置为System.setProperty("file.encoding", "Latin-1");无。每次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;
}

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

至于为什么IO类等会OutputStreamWriter返回意外结果,
则执行sun.nio.cs.StreamEncoder对于JVM 1.5和JVM 1.6(这些IO类使用巫婆的)实现也有所不同。Charset.defaultCharset()如果未为IO类提供默认编码,则JVM 1.6实现基于该方法来获取默认编码。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。
ZZ编码器

2
这是在写实现,而不是抽象。如果您依赖未记载的内容,则在升级到较新版本的平台时如果代码中断,请不要感到惊讶。
McDowell

24

这是错误还是功能?

看起来像未定义的行为。我知道,实际上,您可以使用命令行属性来更改默认编码,但是我不认为定义了此操作会发生什么。

错误ID:4153515,有关设置此属性的问题:

这不是错误。J2SE平台规范不要求“ file.encoding”属性。它是Sun实现的内部细节,不应由用户代码检查或修改。它也打算是只读的。从技术上讲,不可能在命令行上或在程序执行期间的任何其他时间将此属性设置为任意值。

更改VM和运行时系统使用的默认编码的首选方法是在启动Java程序之前更改基础平台的语言环境。

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

如果您不想使用默认编码,请通过适当的method / 构造函数显式设置所需的编码。


4

首先,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。*类。我猜这不是通过相同的机制来处理编码,这很奇怪。

但是,当然,您应该始终指定此代码中的编码含义。我永远不会依赖平台默认设置。


4

行为并不那么奇怪。调查类的实现,原因是:

  • Charset.defaultCharset() 没有在Java 5中缓存确定的字符集。
  • 设置系统属性“ file.encoding”并Charset.defaultCharset()再次调用会导致对系统属性进行第二次评估,未找到名称为“ Latin-1”的字符集,因此Charset.defaultCharset()默认为“ UTF-8”。
  • OutputStreamWriter然而,被缓存的默认字符集,并可能VM初始化过程中已被使用,因此其默认字符集盗用了Charset.defaultCharset(),如果系统属性“file.encoding的”已在运行时更改。

如已经指出的那样,没有记录VM在这种情况下必须如何表现。该Charset.defaultCharset()API文档是不是默认字符集是如何确定的,只提的是,它通常是做了VM启动的基础上,如OS默认字符集或默认的语言环境因素非常精确。



1

检查

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.