W3docs

Java String Pool

Comment fonctionne le Java String pool, pourquoi les littéraux sont internés et comment utiliser la méthode intern().

Un programme Java typique crée des milliers de chaînes, et une grande partie d'entre elles sont les mêmes caractères qu'une autre chaîne ailleurs dans le programme. Noms de méthodes. Clés de configuration. Messages d'erreur. Étiquettes de champs. La JVM considère cette redondance comme un problème valant la peine d'être résolu — elle conserve une région spéciale appelée le string pool (ou table d'interning de chaînes) et attribue à chaque littéral apparaissant dans le code source une entrée partagée. Deux littéraux ayant les mêmes caractères finissent par pointer vers le même objet.

Ce partage a des conséquences visibles sur la comparaison d'identité (==), l'utilisation de la mémoire, et un petit nombre de bugs subtils autour de intern(). Ce chapitre porte sur ces règles.

Les littéraux sont mis en pool, new String(...) ne l'est pas

Le fait le plus important :

String a = "hello";
String b = "hello";
System.out.println(a == b);   // true — same pooled object

String c = new String("hello");
System.out.println(a == c);   // false — new String, fresh object

Chaque littéral de chaîne que le compilateur rencontre est ajouté au pool la première fois qu'il est chargé. Les occurrences suivantes du même littéral — partout dans le programme, dans n'importe quelle classe — renvoient la même référence. Ainsi a et b sont le même objet.

new String("hello") force une nouvelle allocation sur le tas. L'argument "hello" est toujours dans le pool (parce que c'est un littéral), mais le constructeur le copie dans un nouvel objet en dehors du pool. c et a ont donc des contenus égaux mais des identités différentes.

C'est la raison pour laquelle « utilisez equals, pas == » est enseigné dans tous les manuels Java. La comparaison d'identité fonctionne par hasard pour les littéraux simples mais échoue dès qu'une chaîne provient de new, d'un parseur, d'une entrée réseau, ou d'une concaténation que le compilateur n'a pas pu réduire à la compilation.

Ce qui se trouve dans le pool

Le pool est alimenté par deux voies :

  1. Les littéraux de chaînes dans le code source. Le compilateur émet chaque littéral unique comme entrée CONSTANT_String dans le constant pool de la classe ; la JVM le résout en un véritable objet String dans le pool résidant sur le tas la première fois que la classe l'utilise.
  2. Les appels explicites à intern(). Toute String dont vous possédez une référence peut être ajoutée au pool en appelant s.intern(). La méthode retourne l'instance du pool — c'est la même référence pour tous les appelants qui internent un contenu identique.

Les chaînes calculées — a + b, s.substring(...), les résultats de String.format — ne sont pas mises en pool automatiquement. Elles vivent là où le GC les a placées et ont l'identité qu'elles se trouvent avoir.

String x = "java";
String y = "ja" + "va";          // compile-time constant — pooled, == x
String z = "ja" + new String("va"); // runtime computation — NOT pooled
System.out.println(x == y);      // true
System.out.println(x == z);      // false
System.out.println(x == z.intern()); // true — intern() returns the pooled instance

Le deuxième cas est le piège. y est calculé à partir de deux littéraux, mais le compilateur réduit la concaténation à la compilation, de sorte que le résultat n'est qu'un autre littéral — mis en pool. z implique un new au moment de l'exécution, le compilateur ne peut pas le réduire, et l'objet résultant vit hors du pool.

La méthode intern()

String#intern() fait deux choses en un seul appel :

  • Si une chaîne ayant les mêmes caractères est déjà dans le pool, retourner cette référence du pool.
  • Sinon, ajouter cette chaîne au pool et la retourner.

Ce second comportement est utile lorsque vous construisez des chaînes au moment de l'exécution à partir d'un vocabulaire restreint mais à haute fréquence — noms d'en-têtes HTTP analysés depuis des octets, tokens d'un lexer, noms de colonnes lus depuis un pilote de base de données. Les interner réduit N objets séparés à un seul et permet aux comparaisons en aval d'utiliser == si vous avez mesuré que cela en vaut la peine.

String s1 = new String("status").intern();
String s2 = new String("status").intern();
System.out.println(s1 == s2);   // true — both refer to the pooled "status"

L'inconvénient : chaque appel à intern() coûte une recherche de hash, et les chaînes mises en pool vivent dans une table de hachage de taille fixe qui ne rétrécit pas. Si vous internez des entrées non bornées (requêtes de recherche saisies par l'utilisateur, identifiants de requête), vous remplissez lentement le pool de chaînes qui ne seront jamais réutilisées — une fuite mémoire au ralenti. N'internez que lorsque (a) l'ensemble des valeurs est borné et (b) vous avez mesuré un problème qui vaut la peine d'être résolu.

Fonctionnement interne du pool (brièvement)

Le pool est implémenté comme une table de hachage à l'intérieur de la JVM. Sur HotSpot, c'est une StringTable avec une capacité par défaut qui a été augmentée au fil des années (actuellement 65 536 buckets sur la plupart des builds). Vous pouvez l'inspecter en ligne de commande :

java -XX:+PrintStringTableStatistics MyApp

Pour le code applicatif, l'implémentation est invisible : vous ne pouvez pas demander « cette chaîne est-elle dans le pool ? » via l'API publique, et vous n'en avez pas besoin. Le comportement visible est == sur des littéraux égaux, et intern() pour opter à inclure des chaînes calculées.

Pourquoi == reste incorrect pour les chaînes

Le pool peut faire paraître == comme s'il fonctionnait sur des entrées de test :

String a = "hello";
String b = "hello";
if (a == b) { ... }   // happens to be true

Puis quelqu'un fait passer la chaîne par BufferedReader.readLine() et == devient silencieusement faux. Le contrat que vous voulez est « ces chaînes ont-elles les mêmes caractères ? », et ce contrat s'écrit a.equals(b). Le pool est une optimisation mémoire, pas une stratégie de comparaison — ne vous en fiez jamais pour la correction.

Un exemple concret

L'exemple ci-dessous rend le comportement du pool visible. Chaque appel à printRef affiche le hash d'identité système (un raccourci en une ligne pour « quel objet est-ce ? ») afin que vous puissiez voir où les littéraux partagent du stockage et où les chaînes calculées ne le font pas.

java— editable, runs on the server

Lisez d'abord les hashs d'identité : les littéraux et le pliage à la compilation partagent le même. runtimeConcat et fresh ont chacun le leur. interned correspond à nouveau au littéral, parce que intern() a retourné l'instance du pool, pas celle allouée avec new. Les résultats de == découlent directement des identités ; equals retourne true pour tous, car, du point de vue du contenu, ils sont vraiment égaux.

Ce qui suit

Le pool existe parce que String est immuable — partager le même objet entre appelants n'est sûr que si personne ne peut modifier son contenu. Le prochain chapitre approfondit ce point : pourquoi l'immuabilité a été choisie, ce qu'elle apporte, et le compromis de conception qu'elle impose. Continuez vers l'immuabilité des chaînes Java.

Pratique

Pratique
Deux variables `String` `a` et `b` ont toutes deux été assignées au même littéral `'hi'` quelque part dans le programme. Une troisième, `c`, a été créée avec `new String('hi')`. Quel résultat est garanti ?
Deux variables `String` `a` et `b` ont toutes deux été assignées au même littéral `'hi'` quelque part dans le programme. Une troisième, `c`, a été créée avec `new String('hi')`. Quel résultat est garanti ?
Was this page helpful?