Comment trouver le jeu de caractères / encodage par défaut en Java?

92

La réponse évidente est d'utiliser Charset.defaultCharset()mais nous avons récemment découvert que ce n'était peut-être pas la bonne réponse. On m'a dit que le résultat est différent du jeu de caractères par défaut réel utilisé par les classes java.io à plusieurs reprises. On dirait que Java conserve 2 ensembles de jeux de caractères par défaut. Quelqu'un a-t-il des idées sur ce problème?

Nous avons pu reproduire un cas d'échec. C'est une sorte d'erreur de l'utilisateur, mais cela peut toujours exposer la cause première de tous les autres problèmes. Voici le code,

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

Notre serveur nécessite un jeu de caractères par défaut en Latin-1 pour gérer un encodage mixte (ANSI / Latin-1 / UTF-8) dans un protocole hérité. Donc tous nos serveurs fonctionnent avec ce paramètre JVM,

-Dfile.encoding=ISO-8859-1

Voici le résultat sur Java 5,

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

Quelqu'un essaie de modifier le runtime d'encodage en définissant le fichier file.encoding dans le code. Nous savons tous que cela ne fonctionne pas. Cependant, cela annule apparemment defaultCharset () mais n'affecte pas le jeu de caractères par défaut réel utilisé par OutputStreamWriter.

Est-ce un bug ou une fonctionnalité?

EDIT: La réponse acceptée montre la cause première du problème. Fondamentalement, vous ne pouvez pas faire confiance à defaultCharset () dans Java 5, qui n'est pas l'encodage par défaut utilisé par les classes d'E / S. Il semble que Java 6 corrige ce problème.

Codeur ZZ
la source
C'est étrange, car defaultCharset utilise une variable statique qui n'est définie qu'une seule fois (selon la documentation - au démarrage de la VM). Quel fournisseur de VM utilisez-vous?
Bozho
J'ai pu reproduire cela sur Java 5, à la fois sur Sun / Linux et Apple / OS X.
ZZ Coder
Cela explique pourquoi defaultCharset () ne met pas en cache le résultat. J'ai encore besoin de savoir quel est le véritable jeu de caractères par défaut utilisé par les classes IO. Il doit y avoir un autre jeu de caractères par défaut mis en cache ailleurs.
ZZ Coder
@ZZ Coder, je fais toujours des recherches là-dessus. La seule chose que je pense savoir, c'est que Charset.defaulyCharset () n'est pas appelé depuis sun.nio.cs.StreamEncoder dans JVM 1.5. Dans JVM 1.6, la méthode Charset.defaulyCharset () est appelée pour donner les résultats attendus. L'implémentation JVM 1.5 de StreamEncoder met en cache l'encodage précédent, d'une manière ou d'une autre.
bruno conde

Réponses:

62

C'est vraiment étrange ... Une fois défini, le jeu de caractères par défaut est mis en cache et il n'est pas modifié tant que la classe est en mémoire. Définir la "file.encoding"propriété avec System.setProperty("file.encoding", "Latin-1");ne fait rien. Chaque fois qu'il Charset.defaultCharset()est appelé, il renvoie le jeu de caractères mis en cache.

Voici mes résultats:

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

J'utilise JVM 1.6 cependant.

(mettre à jour)

D'accord. J'ai reproduit votre bogue avec JVM 1.5.

En regardant le code source de 1.5, le jeu de caractères par défaut mis en cache n'est pas défini. Je ne sais pas s'il s'agit d'un bogue ou non, mais 1.6 modifie cette implémentation et utilise le jeu de caractères mis en 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;
}

Lorsque vous définissez le codage du fichier sur file.encoding=Latin-1la prochaine fois que vous appelez Charset.defaultCharset(), ce qui se passe, c'est que le jeu de caractères par défaut mis en cache n'est pas défini, il essaiera de trouver le jeu de caractères approprié pour le nom Latin-1. Ce nom est introuvable, car il est incorrect et renvoie la valeur par défaut UTF-8.

Quant à savoir pourquoi les classes IO telles que OutputStreamWriterretournent un résultat inattendu,
l'implémentation de sun.nio.cs.StreamEncoder(witch est utilisé par ces classes IO) est également différente pour JVM 1.5 et JVM 1.6. L'implémentation de JVM 1.6 est basée sur la Charset.defaultCharset()méthode permettant d'obtenir le codage par défaut, s'il n'est pas fourni aux classes IO. L'implémentation JVM 1.5 utilise une méthode différente Converters.getDefaultEncodingName();pour obtenir le jeu de caractères par défaut. Cette méthode utilise son propre cache du jeu de caractères par défaut qui est défini lors de l'initialisation de la 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);
}

Mais je suis d'accord avec les commentaires. Vous ne devriez pas vous fier à cette propriété . C'est un détail de mise en œuvre.

bruno conde
la source
Pour reproduire cette erreur, vous devez être sur Java 5 et votre encodage par défaut JRE doit être UTF-8.
ZZ Coder
2
Il s'agit d'écrire dans l'implémentation, pas dans l'abstraction. Si vous comptez sur des éléments non documentés, ne soyez pas surpris si votre code se rompt lorsque vous passez à une version plus récente de la plate-forme.
McDowell
24

Est-ce un bug ou une fonctionnalité?

Ressemble à un comportement indéfini. Je sais que, dans la pratique, vous pouvez modifier le codage par défaut en utilisant une propriété de ligne de commande, mais je ne pense pas que ce qui se passe lorsque vous faites cela est défini.

ID de bogue: 4153515 sur les problèmes de définition de cette propriété:

Ce n'est pas un bug. La propriété "file.encoding" n'est pas requise par la spécification de la plate-forme J2SE; c'est un détail interne des implémentations de Sun et ne doit pas être examiné ou modifié par le code utilisateur. Il est également destiné à être en lecture seule; il est techniquement impossible de prendre en charge la définition de cette propriété sur des valeurs arbitraires sur la ligne de commande ou à tout autre moment pendant l'exécution du programme.

La meilleure façon de modifier le codage par défaut utilisé par la machine virtuelle et le système d'exécution consiste à modifier les paramètres régionaux de la plate-forme sous-jacente avant de démarrer votre programme Java.

Je grince des dents quand je vois des gens définir l'encodage sur la ligne de commande - vous ne savez pas quel code cela va affecter.

Si vous ne souhaitez pas utiliser l'encodage par défaut, définissez explicitement l'encodage souhaité via la méthode / le constructeur approprié .

McDowell
la source
4

Premièrement, Latin-1 est identique à ISO-8859-1, donc la valeur par défaut était déjà OK pour vous. Droite?

Vous avez correctement défini le codage sur ISO-8859-1 avec votre paramètre de ligne de commande. Vous le définissez également par programme sur "Latin-1", mais ce n'est pas une valeur reconnue d'un encodage de fichier pour Java. Voir http://java.sun.com/javase/6/docs/technotes/guides/intl/encoding.doc.html

Lorsque vous faites cela, il semble que Charset se réinitialise à UTF-8, en regardant la source. Cela explique au moins la plupart des comportements.

Je ne sais pas pourquoi OutputStreamWriter affiche ISO8859_1. Il délègue aux classes sun.misc. * Source fermée. Je suppose qu'il ne s'agit pas tout à fait de l'encodage via le même mécanisme, ce qui est étrange.

Mais bien sûr, vous devriez toujours spécifier le codage que vous entendez dans ce code. Je ne compterais jamais sur la plate-forme par défaut.

Sean Owen
la source
4

Le comportement n'est pas vraiment si étrange. En regardant dans l'implémentation des classes, cela est causé par:

  • Charset.defaultCharset() ne met pas en cache le jeu de caractères déterminé dans Java 5.
  • La définition de la propriété système "file.encoding" et son Charset.defaultCharset()nouvel appel provoquent une deuxième évaluation de la propriété système, aucun jeu de caractères avec le nom "Latin-1" n'est trouvé, donc la valeur par Charset.defaultCharset()défaut est "UTF-8".
  • Le OutputStreamWritermet cependant en cache le jeu de caractères par défaut et est probablement déjà utilisé lors de l'initialisation de la VM, de sorte que son jeu de caractères par défaut dévie Charset.defaultCharset()si la propriété système "file.encoding" a été modifiée lors de l'exécution.

Comme déjà souligné, il n'est pas documenté comment la VM doit se comporter dans une telle situation. La Charset.defaultCharset()documentation de l'API n'est pas très précise sur la façon dont le jeu de caractères par défaut est déterminé, mentionnant seulement qu'il est généralement effectué au démarrage de la VM, en fonction de facteurs tels que le jeu de caractères par défaut du système d'exploitation ou les paramètres régionaux par défaut.

Jarnbjo
la source
3

J'ai défini l'argument vm dans le serveur WAS comme -Dfile.encoding = UTF-8 pour changer le jeu de caractères par défaut des serveurs.

Davy Jones
la source
1

vérifier

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

il semble être le même encodage que celui utilisé dans la ligne de commande de votre système.

Neoedmund
la source