Como encontrar o Charset / codificação padrão em Java?

92

A resposta óbvia é usar, Charset.defaultCharset()mas descobrimos recentemente que essa pode não ser a resposta certa. Disseram-me que o resultado é diferente do conjunto de caracteres padrão real usado pelas classes java.io em várias ocasiões. Parece que o Java mantém 2 conjuntos de charset padrão. Alguém tem alguma ideia sobre este assunto?

Conseguimos reproduzir um caso de falha. É uma espécie de erro do usuário, mas ainda pode expor a causa raiz de todos os outros problemas. Aqui está o código,

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

Nosso servidor requer conjunto de caracteres padrão em Latin-1 para lidar com alguma codificação mista (ANSI / Latin-1 / UTF-8) em um protocolo legado. Portanto, todos os nossos servidores funcionam com este parâmetro JVM,

-Dfile.encoding=ISO-8859-1

Aqui está o resultado em Java 5,

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

Alguém tenta alterar o tempo de execução da codificação definindo file.encoding no código. Todos nós sabemos que isso não funciona. No entanto, isso aparentemente joga fora defaultCharset (), mas não afeta o charset padrão real usado por OutputStreamWriter.

Isso é um bug ou recurso?

EDIT: A resposta aceita mostra a causa raiz do problema. Basicamente, você não pode confiar em defaultCharset () em Java 5, que não é a codificação padrão usada pelas classes de E / S. Parece que o Java 6 corrige esse problema.

ZZ Coder
fonte
Isso é estranho, pois o defaultCharset usa uma variável estática que é definida apenas uma vez (de acordo com os documentos - na inicialização da VM). Qual fornecedor de VM você está usando?
Bozho
Consegui reproduzir isso no Java 5, tanto no Sun / Linux quanto no Apple / OS X.
ZZ Coder
Isso explica porque defaultCharset () não armazena em cache o resultado. Ainda preciso descobrir qual é o conjunto de caracteres padrão real usado pelas classes IO. Deve haver outro conjunto de caracteres padrão em cache em outro lugar.
ZZ Coder
@ZZ Coder, ainda estou pesquisando sobre isso. A única coisa que sei é que Charset.defaulyCharset () não é chamado de sun.nio.cs.StreamEncoder no JVM 1.5. Na JVM 1.6, o método Charset.defaulyCharset () é chamado, fornecendo os resultados esperados. A implementação JVM 1.5 de StreamEncoder está armazenando em cache a codificação anterior, de alguma forma.
bruno conde

Respostas:

62

Isso é realmente estranho ... Uma vez definido, o Charset padrão é armazenado em cache e não é alterado enquanto a classe está na memória. Definir a "file.encoding"propriedade com System.setProperty("file.encoding", "Latin-1");não faz nada. Cada vez que Charset.defaultCharset()é chamado, ele retorna o conjunto de caracteres em cache.

Aqui estão meus resultados:

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

Estou usando JVM 1.6.

(atualizar)

Está bem. Eu reproduzi seu bug com JVM 1.5.

Olhando para o código-fonte de 1.5, o conjunto de caracteres padrão em cache não está sendo definido. Não sei se isso é um bug ou não, mas 1.6 muda essa implementação e usa o conjunto de caracteres em cache:

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

Quando você definir a codificação do arquivo para file.encoding=Latin-1a próxima vez que você chamar Charset.defaultCharset(), o que acontece é que, como o conjunto de caracteres padrão em cache não foi definido, ele tentará encontrar o conjunto de caracteres apropriado para o nome Latin-1. Este nome não foi encontrado porque está incorreto e retorna o padrão UTF-8.

Quanto ao motivo de as classes IO OutputStreamWriterretornarem um resultado inesperado,
a implementação de sun.nio.cs.StreamEncoder(que é usada por essas classes IO) é diferente também para JVM 1.5 e JVM 1.6. A implementação da JVM 1.6 é baseada no Charset.defaultCharset()método para obter a codificação padrão, se uma não for fornecida para as classes de E / S. A implementação do JVM 1.5 usa um método diferente Converters.getDefaultEncodingName();para obter o conjunto de caracteres padrão. Este método usa seu próprio cache do conjunto de caracteres padrão que é definido na inicialização da 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);
}

Mas concordo com os comentários. Você não deve confiar nesta propriedade . É um detalhe de implementação.

bruno conde
fonte
Para reproduzir esse erro, você deve estar no Java 5 e a codificação padrão do JRE deve ser UTF-8.
ZZ Coder
2
Isso é escrever para a implementação, não para a abstração. Se você depende de coisas não documentadas, não se surpreenda se o seu código quebrar ao atualizar para uma versão mais recente da plataforma.
McDowell
24

Isso é um bug ou recurso?

Parece um comportamento indefinido. Eu sei que, na prática, você pode alterar a codificação padrão usando uma propriedade de linha de comando, mas não acho que o que acontece quando você faz isso está definido.

ID do bug: 4153515 em problemas ao definir esta propriedade:

Este não é um bug. A propriedade "file.encoding" não é exigida pela especificação da plataforma J2SE; é um detalhe interno das implementações da Sun e não deve ser examinado ou modificado pelo código do usuário. Ele também deve ser somente leitura; é tecnicamente impossível oferecer suporte à configuração dessa propriedade para valores arbitrários na linha de comando ou em qualquer outro momento durante a execução do programa.

A maneira preferida de alterar a codificação padrão usada pela VM e o sistema de tempo de execução é alterar a localidade da plataforma subjacente antes de iniciar seu programa Java.

Eu me encolho quando vejo pessoas definindo a codificação na linha de comando - você não sabe qual código isso afetará.

Se você não quiser usar a codificação padrão, defina a codificação que deseja explicitamente por meio do método / construtor apropriado .

McDowell
fonte
4

Primeiro, Latin-1 é o mesmo que ISO-8859-1, então, o padrão já estava OK para você. Certo?

Você definiu com sucesso a codificação para ISO-8859-1 com seu parâmetro de linha de comando. Você também o define programaticamente para "Latin-1", mas esse não é um valor reconhecido de uma codificação de arquivo para Java. Consulte http://java.sun.com/javase/6/docs/technotes/guides/intl/encoding.doc.html

Quando você faz isso, parece que o Charset é redefinido para UTF-8, olhando para a fonte. Isso pelo menos explica a maior parte do comportamento.

Não sei por que OutputStreamWriter mostra ISO8859_1. Ele delega para classes sun.misc. * De código-fonte fechado. Suponho que não esteja lidando exatamente com a codificação pelo mesmo mecanismo, o que é estranho.

Mas é claro que você deve sempre especificar qual codificação você quer dizer neste código. Eu nunca confiaria no padrão da plataforma.

Sean Owen
fonte
4

O comportamento não é tão estranho. Olhando para a implementação das classes, é causado por:

  • Charset.defaultCharset() não está armazenando em cache o conjunto de caracteres determinado em Java 5.
  • Definir a propriedade do sistema "file.encoding" e chamar Charset.defaultCharset()novamente causa uma segunda avaliação da propriedade do sistema, nenhum caractere definido com o nome "Latin-1" é encontrado, então o Charset.defaultCharset()padrão é "UTF-8".
  • No OutputStreamWriterentanto, o está armazenando em cache o conjunto de caracteres padrão e provavelmente já é usado durante a inicialização da VM, de modo que seu conjunto de caracteres padrão diverge Charset.defaultCharset()se a propriedade do sistema "file.encoding" tiver sido alterada no tempo de execução.

Como já apontado, não está documentado como a VM deve se comportar em tal situação. A Charset.defaultCharset()documentação da API não é muito precisa sobre como o conjunto de caracteres padrão é determinado, apenas mencionando que isso geralmente é feito na inicialização da VM, com base em fatores como o conjunto de caracteres padrão do sistema operacional ou localidade padrão.

Jarnbjo
fonte
3

Eu defini o argumento vm no servidor WAS como -Dfile.encoding = UTF-8 para alterar o conjunto de caracteres padrão dos servidores.

Davy Jones
fonte
1

Verifica

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

parece ser a mesma codificação usada na linha de comando do seu sistema.

neoedmund
fonte