Как найти кодировку / кодировку по умолчанию в 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.

Это ошибка или особенность?

РЕДАКТИРОВАТЬ: принятый ответ показывает основную причину проблемы. По сути, вы не можете доверять defaultCharset () в Java 5, которая не является кодировкой по умолчанию, используемой классами ввода-вывода. Похоже, Java 6 исправляет эту проблему.

ZZ Coder
источник
Это странно, поскольку defaultCharset использует статическую переменную, которая устанавливается только один раз (согласно документации - при запуске виртуальной машины). Какого поставщика ВМ вы используете?
Bozho
Мне удалось воспроизвести это на Java 5, как на Sun / Linux, так и на Apple / OS X.
ZZ Coder
Это объясняет, почему defaultCharset () не кэширует результат. Мне все еще нужно выяснить, какая кодировка по умолчанию используется классами ввода-вывода. В другом месте должна быть другая кодировка по умолчанию.
ZZ Coder
@ZZ Coder, я все еще изучаю это. Единственное, что я знаю, это то, что Charset.defaulyCharset () не вызывается из sun.nio.cs.StreamEncoder в JVM 1.5. В JVM 1.6 вызывается метод Charset.defaulyCharset (), который дает ожидаемые результаты. Реализация StreamEncoder в JVM 1.5 каким-то образом кэширует предыдущую кодировку.
bruno conde

Ответы:

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(), происходит то, что, поскольку кешированная кодировка по умолчанию не установлена, он попытается найти подходящую кодировку для имени Latin-1. Это имя не найдено, потому что оно неверно, и возвращает значение по умолчанию UTF-8.

Что касается того, почему классы ввода-вывода, например, OutputStreamWriterвозвращают неожиданный результат,
реализация sun.nio.cs.StreamEncoder(которая используется этими классами ввода-вывода) отличается также для JVM 1.5 и JVM 1.6. Реализация JVM 1.6 основана на Charset.defaultCharset()методе получения кодировки по умолчанию, если она не предоставляется классам ввода-вывода. Реализация 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 Coder
2
Это запись в реализацию, а не в абстракцию. Если вы полагаетесь на недокументированный материал, не удивляйтесь, если ваш код сломается при обновлении до более новой версии платформы.
McDowell
24

Это ошибка или особенность?

Похоже на неопределенное поведение. Я знаю, что на практике вы можете изменить кодировку по умолчанию, используя свойство командной строки, но я не думаю, что то, что происходит, когда вы это делаете, определено.

Идентификатор ошибки: 4153515 при проблемах с настройкой этого свойства:

Это не ошибка. Свойство "file.encoding" не требуется спецификацией платформы J2SE; это внутренняя деталь реализаций Sun и не должна проверяться или изменяться пользовательским кодом. Он также предназначен только для чтения; технически невозможно поддерживать установку этого свойства на произвольные значения в командной строке или в любое другое время во время выполнения программы.

Предпочтительный способ изменить кодировку по умолчанию, используемую виртуальной машиной и системой времени выполнения, - это изменить языковой стандарт базовой платформы перед запуском вашей программы Java.

Я съеживаюсь, когда вижу, как люди устанавливают кодировку в командной строке - вы не знаете, какой код это повлияет.

Если вы не хотите использовать кодировку по умолчанию, установите желаемую кодировку явно с помощью соответствующего метода / конструктора .

Макдауэлл
источник
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не менее, он кэширует набор символов по умолчанию и, вероятно, используется уже во время инициализации виртуальной машины, так что его набор символов по умолчанию отличается от него, Charset.defaultCharset()если системное свойство «file.encoding» было изменено во время выполнения.

Как уже указывалось, не задокументировано, как виртуальная машина должна вести себя в такой ситуации. Документация по Charset.defaultCharset()API не очень точна в отношении того, как определяется набор символов по умолчанию, только упоминается, что это обычно выполняется при запуске виртуальной машины на основе таких факторов, как набор символов по умолчанию для ОС или языковой стандарт по умолчанию.

Jarnbjo
источник
3

Я установил аргумент vm на сервере WAS как -Dfile.encoding = UTF-8, чтобы изменить набор символов по умолчанию для серверов.

Дэви Джонс
источник
1

проверять

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

похоже, это та же кодировка, что и в командной строке вашей системы.

Neoedmund
источник