¿Cómo encontrar el conjunto de caracteres / codificación predeterminado en Java?

92

La respuesta obvia es usar, Charset.defaultCharset()pero recientemente descubrimos que esta podría no ser la respuesta correcta. Me dijeron que el resultado es diferente del conjunto de caracteres predeterminado real utilizado por las clases java.io en varias ocasiones. Parece que Java mantiene 2 juegos de caracteres predeterminados. ¿Alguien tiene alguna idea sobre este tema?

Pudimos reproducir un caso fallido. Es una especie de error del usuario, pero aún puede exponer la causa raíz de todos los demás problemas. Aquí está el 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;
    }
}

Nuestro servidor requiere un juego de caracteres predeterminado en Latin-1 para manejar algunas codificaciones mixtas (ANSI / Latin-1 / UTF-8) en un protocolo heredado. Entonces todos nuestros servidores se ejecutan con este parámetro de JVM,

-Dfile.encoding=ISO-8859-1

Aquí está el resultado en Java 5,

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

Alguien intenta cambiar el tiempo de ejecución de la codificación configurando file.encoding en el código. Todos sabemos que eso no funciona. Sin embargo, esto aparentemente elimina defaultCharset () pero no afecta el juego de caracteres predeterminado real utilizado por OutputStreamWriter.

¿Es esto un error o una característica?

EDITAR: La respuesta aceptada muestra la causa raíz del problema. Básicamente, no puede confiar en defaultCharset () en Java 5, que no es la codificación predeterminada utilizada por las clases de E / S. Parece que Java 6 corrige este problema.

Codificador ZZ
fuente
Eso es extraño, ya que el defaultCharset usa una variable estática que se establece solo una vez (según los documentos, al inicio de la VM). ¿Qué proveedor de VM está utilizando?
Bozho
Pude reproducir esto en Java 5, tanto en Sun / Linux como en Apple / OS X.
ZZ Coder
Eso explica por qué defaultCharset () no almacena en caché el resultado. Todavía necesito averiguar cuál es el conjunto de caracteres predeterminado real utilizado por las clases IO. Debe haber otro juego de caracteres predeterminado almacenado en caché en otro lugar.
ZZ Coder
@ZZ Coder, todavía estoy investigando sobre eso. Lo único que sé es que Charset.defaulyCharset () no se llama desde sun.nio.cs.StreamEncoder en JVM 1.5. En JVM 1.6, se llama al método Charset.defaulyCharset () dando los resultados esperados. La implementación de JVM 1.5 de StreamEncoder está almacenando en caché la codificación anterior, de alguna manera.
bruno conde

Respuestas:

62

Esto es realmente extraño ... Una vez configurado, el conjunto de caracteres predeterminado se almacena en caché y no se cambia mientras la clase está en la memoria. Establecer la "file.encoding"propiedad con System.setProperty("file.encoding", "Latin-1");no hace nada. Cada vez que Charset.defaultCharset()se llama, devuelve el juego de caracteres en caché.

Aquí están mis resultados:

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

Aunque estoy usando JVM 1.6.

(actualizar)

Okay. Reproduje tu error con JVM 1.5.

Mirando el código fuente de 1.5, el juego de caracteres predeterminado en caché no se está configurando. No sé si esto es un error o no, pero 1.6 cambia esta implementación y usa el juego de caracteres en caché:

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

Cuando configura la codificación del archivo para file.encoding=Latin-1la próxima vez que llama Charset.defaultCharset(), lo que sucede es que, debido a que el juego de caracteres predeterminado en caché no está configurado, intentará encontrar el juego de caracteres apropiado para el nombre Latin-1. Este nombre no se encuentra porque es incorrecto y devuelve el predeterminado UTF-8.

En cuanto a por qué las clases de E / S como OutputStreamWriterdevuelven un resultado inesperado,
la implementación de sun.nio.cs.StreamEncoder(que utilizan estas clases de E / S) también es diferente para JVM 1.5 y JVM 1.6. La implementación de JVM 1.6 se basa en el Charset.defaultCharset()método para obtener la codificación predeterminada, si no se proporciona una a las clases de E / S. La implementación de JVM 1.5 utiliza un método diferente Converters.getDefaultEncodingName();para obtener el juego de caracteres predeterminado. Este método utiliza su propia caché del juego de caracteres predeterminado que se establece en la inicialización de 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);
}

Pero estoy de acuerdo con los comentarios. No deberías confiar en esta propiedad . Es un detalle de implementación.

bruno conde
fuente
Para reproducir este error, debe estar en Java 5 y su codificación predeterminada de JRE debe ser UTF-8.
ZZ Coder
2
Esto está escribiendo en la implementación, no en la abstracción. Si confía en cosas indocumentadas, no se sorprenda si su código se rompe cuando actualiza a una versión más nueva de la plataforma.
McDowell
24

¿Es esto un error o una característica?

Parece un comportamiento indefinido. Sé que, en la práctica, puede cambiar la codificación predeterminada utilizando una propiedad de línea de comandos, pero no creo que lo que sucede cuando lo hace está definido.

ID de error: 4153515 sobre problemas al establecer esta propiedad:

Esto no es un error. La propiedad "file.encoding" no es requerida por la especificación de la plataforma J2SE; es un detalle interno de las implementaciones de Sun y no debe ser examinado ni modificado por el código de usuario. También está diseñado para ser de solo lectura; es técnicamente imposible admitir la configuración de esta propiedad en valores arbitrarios en la línea de comando o en cualquier otro momento durante la ejecución del programa.

La forma preferida de cambiar la codificación predeterminada utilizada por la VM y el sistema de tiempo de ejecución es cambiar la configuración regional de la plataforma subyacente antes de iniciar su programa Java.

Me estremezco cuando veo que la gente configura la codificación en la línea de comandos; no sabes qué código afectará.

Si no desea utilizar la codificación predeterminada, configure la codificación que desee explícitamente mediante el método / constructor apropiado .

McDowell
fuente
4

Primero, Latin-1 es lo mismo que ISO-8859-1, por lo que el valor predeterminado ya estaba bien para usted. ¿Correcto?

Estableció correctamente la codificación en ISO-8859-1 con su parámetro de línea de comando. También lo configura mediante programación en "Latin-1", pero ese no es un valor reconocido de una codificación de archivo para Java. Ver http://java.sun.com/javase/6/docs/technotes/guides/intl/encoding.doc.html

Cuando haces eso, parece que Charset se restablece a UTF-8, al mirar la fuente. Eso al menos explica la mayor parte del comportamiento.

No sé por qué OutputStreamWriter muestra ISO8859_1. Delega a clases sun.misc. * De código cerrado. Supongo que no se trata del todo con la codificación a través del mismo mecanismo, lo cual es extraño.

Pero, por supuesto, siempre debe especificar qué codificación quiere decir en este código. Nunca confiaría en la plataforma predeterminada.

Sean Owen
fuente
4

El comportamiento no es tan extraño. En cuanto a la implementación de las clases, se debe a:

  • Charset.defaultCharset() no almacena en caché el conjunto de caracteres determinado en Java 5.
  • Establecer la propiedad del sistema "file.encoding" y Charset.defaultCharset()volver a invocar provoca una segunda evaluación de la propiedad del sistema, no se encuentra ningún juego de caracteres con el nombre "Latin-1", por lo que el valor Charset.defaultCharset()predeterminado es "UTF-8".
  • Sin OutputStreamWriterembargo, está almacenando en caché el juego de caracteres predeterminado y probablemente ya se usa durante la inicialización de la VM, por lo que su juego de caracteres predeterminado se desvía Charset.defaultCharset()si la propiedad del sistema "file.encoding" se ha cambiado en tiempo de ejecución.

Como ya se señaló, no está documentado cómo debe comportarse la VM en tal situación. La Charset.defaultCharset()documentación de la API no es muy precisa sobre cómo se determina el juego de caracteres predeterminado, solo menciona que generalmente se realiza al inicio de la VM, en función de factores como el juego de caracteres predeterminado del sistema operativo o la configuración regional predeterminada.

jarnbjo
fuente
3

He configurado el argumento vm en el servidor WAS como -Dfile.encoding = UTF-8 para cambiar el juego de caracteres predeterminado de los servidores.

Davy Jones
fuente
1

cheque

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

parece ser la misma codificación que la utilizada en la línea de comandos de su sistema.

neoedmund
fuente