Jak znaleźć domyślny zestaw znaków / kodowanie w Javie?

92

Oczywistą odpowiedzią jest użycie, Charset.defaultCharset()ale niedawno odkryliśmy, że może to nie być właściwa odpowiedź. Powiedziano mi, że wynik różni się od rzeczywistego domyślnego zestawu znaków używanego przez klasy java.io w kilku przypadkach. Wygląda na to, że Java zachowuje 2 zestawy domyślnego zestawu znaków. Czy ktoś ma jakieś spostrzeżenia w tej sprawie?

Udało nam się odtworzyć jeden przypadek niepowodzenia. To rodzaj błędu użytkownika, ale nadal może ujawnić główną przyczynę wszystkich innych problemów. Oto kod,

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;
    }
}

Nasz serwer wymaga domyślnego zestawu znaków Latin-1, aby poradzić sobie z pewnym kodowaniem mieszanym (ANSI / Latin-1 / UTF-8) w starszym protokole. Wszystkie nasze serwery działają z tym parametrem maszyny JVM,

-Dfile.encoding=ISO-8859-1

Oto wynik na Javie 5,

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

Ktoś próbuje zmienić środowisko wykonawcze kodowania, ustawiając plik file.encoding w kodzie. Wszyscy wiemy, że to nie działa. Jednak to najwyraźniej wyrzuca defaultCharset (), ale nie wpływa na rzeczywisty domyślny zestaw znaków używany przez OutputStreamWriter.

Czy to błąd lub funkcja?

EDYCJA: Zaakceptowana odpowiedź przedstawia główną przyczynę problemu. Zasadniczo nie można ufać defaultCharset () w Javie 5, co nie jest domyślnym kodowaniem używanym przez klasy I / O. Wygląda na to, że Java 6 rozwiązuje ten problem.

ZZ Coder
źródło
To dziwne, ponieważ defaultCharset używa zmiennej statycznej, która jest ustawiana tylko raz (zgodnie z dokumentacją - podczas uruchamiania maszyny wirtualnej). Jakiego dostawcy maszyn wirtualnych używasz?
Bozho
Udało mi się to odtworzyć na Javie 5, zarówno w Sun / Linux, jak i Apple / OS X.
ZZ Coder
To wyjaśnia, dlaczego defaultCharset () nie zapisuje wyniku w pamięci podręcznej. Nadal muszę się dowiedzieć, jaki jest prawdziwy domyślny zestaw znaków używany przez klasy IO. Musi istnieć inny domyślny zestaw znaków w innym miejscu.
ZZ Coder
@ZZ Coder, wciąż tego badam. Jedyne, co wiem, to to, że Charset.defaulyCharset () nie jest wywoływana z sun.nio.cs.StreamEncoder w JVM 1.5. W JVM 1.6 wywoływana jest metoda Charset.defaulyCharset (), która daje oczekiwane wyniki. Implementacja StreamEncodera JVM 1.5 w jakiś sposób buforuje poprzednie kodowanie.
bruno conde

Odpowiedzi:

62

To naprawdę dziwne ... Po ustawieniu domyślny zestaw znaków jest przechowywany w pamięci podręcznej i nie jest zmieniany, gdy klasa jest w pamięci. Ustawienie "file.encoding"właściwości na System.setProperty("file.encoding", "Latin-1");nic nie robi. Za każdym razem, gdy Charset.defaultCharset()jest wywoływana, zwraca buforowany zestaw znaków.

Oto moje wyniki:

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

Jednak używam JVM 1.6.

(aktualizacja)

Ok. Odtworzyłem twój błąd w JVM 1.5.

Patrząc na kod źródłowy wersji 1.5, domyślny zestaw znaków w pamięci podręcznej nie jest ustawiany. Nie wiem, czy to błąd, czy nie, ale 1.6 zmienia tę implementację i używa buforowanego zestawu znaków:

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;
}

Kiedy ustawisz kodowanie pliku file.encoding=Latin-1na następne wywołanie Charset.defaultCharset(), co się stanie, ponieważ buforowany domyślny zestaw znaków nie jest ustawiony, spróbuje znaleźć odpowiedni zestaw znaków dla nazwy Latin-1. Ta nazwa nie została znaleziona, ponieważ jest nieprawidłowa i zwraca wartość domyślną UTF-8.

Jeśli chodzi o przyczyny, dla których klasy IO, takie jak OutputStreamWriterzwracają nieoczekiwany wynik,
implementacja sun.nio.cs.StreamEncoder(która jest używana przez te klasy IO) jest inna dla JVM 1.5 i JVM 1.6. Implementacja JVM 1.6 jest oparta na Charset.defaultCharset()metodzie uzyskiwania domyślnego kodowania, jeśli nie zostało ono dostarczone do klas IO. Implementacja JVM 1.5 korzysta z innej metody Converters.getDefaultEncodingName();pobierania domyślnego zestawu znaków. Ta metoda używa własnej pamięci podręcznej domyślnego zestawu znaków, który jest ustawiany podczas inicjalizacji maszyny 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);
}

Ale zgadzam się z komentarzami. Nie powinieneś polegać na tej właściwości . To szczegół implementacji.

bruno conde
źródło
Aby odtworzyć ten błąd, musisz korzystać z języka Java 5, a domyślne kodowanie środowiska JRE to UTF-8.
ZZ Coder
2
To jest pisanie do implementacji, a nie abstrakcja. Jeśli polegasz na nieudokumentowanych materiałach, nie zdziw się, jeśli Twój kod zepsuje się po uaktualnieniu do nowszej wersji platformy.
McDowell
24

Czy to błąd lub funkcja?

Wygląda na niezdefiniowane zachowanie. Wiem, że w praktyce możesz zmienić domyślne kodowanie za pomocą właściwości wiersza poleceń, ale nie sądzę, co się stanie, gdy to zrobisz, jest zdefiniowane.

Identyfikator błędu: 4153515 dotyczący problemów z ustawieniem tej właściwości:

To nie jest błąd. Właściwość „file.encoding” nie jest wymagana przez specyfikację platformy J2SE; jest to wewnętrzny szczegół implementacji firmy Sun i nie powinien być sprawdzany ani modyfikowany przez kod użytkownika. Ma być również przeznaczony tylko do odczytu; jest technicznie niemożliwe, aby wspierać ustawienie tej właściwości na dowolne wartości w linii poleceń lub w jakimkolwiek innym momencie podczas wykonywania programu.

Preferowanym sposobem zmiany domyślnego kodowania używanego przez maszynę wirtualną i system wykonawczy jest zmiana ustawień regionalnych platformy bazowej przed uruchomieniem programu Java.

Wzdrygam się, gdy widzę, jak ludzie ustawiają kodowanie w wierszu poleceń - nie wiesz, jaki kod to wpłynie.

Jeśli nie chcesz używać domyślnego kodowania, ustaw żądane kodowanie jawnie za pomocą odpowiedniej metody / konstruktora .

McDowell
źródło
4

Po pierwsze, Latin-1 jest tym samym, co ISO-8859-1, więc ustawienie domyślne było już dla Ciebie OK. Dobrze?

Pomyślnie ustawiłeś kodowanie na ISO-8859-1 za pomocą parametru wiersza poleceń. Ustawiasz również programowo na „Latin-1”, ale nie jest to rozpoznawana wartość kodowania pliku dla języka Java. Zobacz http://java.sun.com/javase/6/docs/technotes/guides/intl/encoding.doc.html

Kiedy to zrobisz, wygląda na to, że Charset resetuje się do UTF-8, patrząc na źródło. To przynajmniej wyjaśnia większość tego zachowania.

Nie wiem, dlaczego OutputStreamWriter wyświetla ISO8859_1. Deleguje do klas sun.misc. * O zamkniętym źródle. Domyślam się, że nie do końca radzi sobie z kodowaniem za pomocą tego samego mechanizmu, co jest dziwne.

Ale oczywiście zawsze powinieneś określać, jakie kodowanie masz na myśli w tym kodzie. Nigdy nie polegałem na domyślnej platformie.

Sean Owen
źródło
4

To zachowanie nie jest tak dziwne. Patrząc na realizację zajęć, jest to spowodowane:

  • Charset.defaultCharset() nie buforuje określonego zestawu znaków w Javie 5.
  • Ustawienie właściwości systemowej „file.encoding” i Charset.defaultCharset()ponowne wywołanie powoduje drugą ocenę właściwości systemowej, nie znaleziono zestawu znaków o nazwie „Latin-1”, więc Charset.defaultCharset()domyślnie jest to „UTF-8”.
  • OutputStreamWriterJest jednak buforowanie domyślny zestaw znaków i jest prawdopodobnie już używany podczas inicjalizacji VM, tak że domyślny zestaw znaków przekazy od Charset.defaultCharset()jeśli właściwość systemu „file.encoding” został zmieniony w czasie wykonywania.

Jak już wspomniano, nie jest udokumentowane, jak maszyna wirtualna musi zachowywać się w takiej sytuacji. Dokumentacja Charset.defaultCharset()interfejsu API nie jest zbyt dokładna, jeśli chodzi o sposób określania domyślnego zestawu znaków, wspominając tylko, że jest to zwykle wykonywane podczas uruchamiania maszyny wirtualnej, w oparciu o takie czynniki, jak domyślny zestaw znaków systemu operacyjnego lub domyślne ustawienia regionalne.

jarnbjo
źródło
3

Ustawiłem argument vm na serwerze WAS jako -Dfile.encoding = UTF-8, aby zmienić domyślny zestaw znaków serwera.

Davy Jones
źródło
1

czek

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

wygląda na to, że kodowanie jest takie samo, jak to używane w linii poleceń twojego systemu.

neoedmund
źródło