Immuabilité des chaînes Java
Pourquoi la classe String de Java est immuable — implications pour la sécurité, la mise en cache, le hachage et la sécurité des threads.
Un String en Java ne peut pas être modifié après sa création. Une fois que "hello" existe, aucune méthode, aucune astuce de réflexion, aucune affectation ingénieuse ne peut réécrire les caractères de cet objet particulier. Chaque opération qui « modifie » une chaîne renvoie en réalité un nouveau String. La classe l'impose : le champ qui contient les octets est private final, la classe elle-même est final, et il n'existe pas de setter public, pas d'append, pas de clear.
Ce choix — l'immuabilité — n'est pas une préférence stylistique. C'est la décision fondamentale qui rend le pool de chaînes sûr, le hachage fiable, le partage multithread gratuit, et rend possibles quelques garanties de sécurité subtiles.
Ce que « immuable » signifie vraiment
String s = "hello";
s.toUpperCase(); // returns "HELLO" — the return value is dropped
System.out.println(s); // prints "hello"
s = s.toUpperCase(); // s now *points at* a different String
System.out.println(s); // prints "HELLO"La variable s peut être réaffectée — c'est une propriété de la variable, pas de l'objet. L'objet créé à l'origine avec "hello" est inchangé partout, pour toujours, quelle que soit la valeur vers laquelle s pointe ensuite. Si une autre variable le référence encore, cette variable voit toujours "hello".
String a = "hello";
String b = a;
a = a.toUpperCase();
System.out.println(a); // "HELLO"
System.out.println(b); // "hello" — still the originalC'est ce que les gens entendent quand ils disent que les chaînes sont semblables à des valeurs : le contenu d'une référence String est aussi stable que le contenu d'un int.
Pourquoi les concepteurs de la JVM ont choisi l'immuabilité
Plusieurs propriétés découlent de l'immuabilité, et chacune représente une vraie amélioration de performance ou de sécurité.
Le pool de chaînes est sûr. Si "hello" pouvait être modifié en place, partager une instance mise en pool dans tout le programme serait désastreux : le modifier à un endroit le changerait silencieusement partout. L'immuabilité est ce qui rend le pool de chaînes possible.
hashCode() peut être mis en cache. String calcule son hash au premier appel et le stocke dans un champ privé. Cette valeur mise en cache serait incorrecte si les caractères pouvaient changer ensuite, ce qui casserait chaque HashMap<String, ?> indexée sur cette chaîne. Comme le contenu est stable, le cache est permanent.
Les lectures simultanées ne nécessitent aucune synchronisation. Deux threads lisant la même référence String ne peuvent jamais observer une valeur partiellement modifiée. Il n'y a pas de synchronized, pas de volatile, pas de barrière mémoire — il n'y a rien qui pourrait changer. Comparez cela à un tampon mutable, où il faudrait copier, verrouiller ou restreindre la propriété.
Le chargement de classes, la réflexion et les vérifications de sécurité peuvent faire confiance aux arguments string. Un ClassLoader résout les noms de classes à partir de Strings passés par l'appelant. Si la chaîne pouvait être modifiée par un autre thread entre la vérification de sécurité et l'ouverture du fichier, vous auriez une vulnérabilité de condition de course — le bug classique time-of-check / time-of-use. Avec des chaînes immuables, la valeur validée est identique à la valeur utilisée.
Les arguments de méthode n'ont pas besoin de copies défensives. Lorsque vous passez un String à une méthode, vous ne vous inquiétez pas qu'il soit muté et vous surprenne au retour. Le destinataire peut stocker la référence directement ; l'appelant peut continuer à utiliser sa référence également.
Le coût : la mutation en masse est coûteuse
Il y a un prix à payer. Construire une chaîne de 10 000 caractères un caractère à la fois avec += alloue un nouveau String à chaque étape, copiant chaque caractère déjà présent plus le nouveau. C'est un travail quadratique — O(n²) pour une tâche en O(n).
// Don't do this for large n
String s = "";
for (int i = 0; i < n; i++) {
s += i + ",";
}La réponse de la bibliothèque standard est les tampons mutables — StringBuilder pour le code mono-thread et StringBuffer pour le rare cas partagé. Ils maintiennent un tableau redimensionnable, ajoutent en O(1) amorti, et produisent un seul String immuable à la fin avec toString(). C'est le modèle canonique pour assembler des chaînes.
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append(i).append(',');
}
String s = sb.toString();Les JDK modernes optimisent les chaînes + courtes et statiquement formées via StringConcatFactory, donc "hello, " + name + "!" est bien. Le cas à éviter est += dans une boucle sur un nombre inconnu d'itérations.
Tenter de le casser
La réflexion peut techniquement atteindre le champ privé value et le remplacer. Ce faisant, il s'agit d'un comportement non défini du point de vue de la JVM : le JIT suppose que les chaînes sont immuables et intégrera le hashCode mis en cache, partagera les références dans le pool, et sautera les barrières de lecture sur la base de cette promesse. Muter un String par réflexion peut silencieusement corrompre du code non lié qui détient une référence au même objet. Ne le faites pas. Si vous avez besoin de mutabilité, vous avez StringBuilder pour cela.
Implications pour la sécurité
Deux cas concrets où l'immuabilité est importante pour la sécurité :
- Chemins de fichiers et noms de classes. Passés aux API qui effectuent une vérification d'accès avant d'ouvrir ou de charger. Si un chemin pouvait changer entre la vérification et l'utilisation, les sandboxes seraient contournables.
- Clés de
ClassLoaderet clés de mapString. Des codes de hachage stables signifient qu'un attaquant ne peut pas concevoir une clé qui « correspond » à un endroit et se relocalise silencieusement ailleurs.
La contrepartie : stocker des mots de passe dans un String est une mauvaise pratique pour la raison inverse. Une fois qu'un mot de passe est dans un String, vous ne pouvez pas l'effacer — les octets restent en mémoire heap jusqu'à ce que le GC les récupère, peut-être après qu'un dump heap ait été écrit. Pour les mots de passe, utilisez char[] (que vous pouvez remplir manuellement de zéros) ou — mieux — javax.crypto.SecretKey et ses amis. La méthode Console.readPassword() du JDK renvoie char[] précisément pour cette raison.
Un exemple concret
Ce programme crée une chaîne, la passe à plusieurs appelants, leur fait chacun la « muter », et affiche ce que chaque variable voit ensuite. L'objet original est visité par quatre références et survit intact. Le seul tampon mutable à la fin est l'alternative canonique lorsque vous avez vraiment besoin de construire une chaîne progressivement.
Regardez les deux comparaisons ==. original et alias sont littéralement le même objet, donc l'identité est vérifiée. original et upper ont des contenus liés mais upper est un nouvel objet — il n'y a aucun moyen pour upperCase d'avoir modifié celui qui lui a été passé. C'est la garantie sur laquelle chaque développeur Java s'appuie sans même y penser.
Prochaine étape
Lorsque vous avez besoin d'une chaîne que vous pouvez modifier, la bibliothèque standard propose un cousin mutable de String. C'est le moteur de travail derrière chaque chaîne + que le compilateur optimise, et la bonne réponse lorsque vous seriez autrement tenté d'utiliser += dans une boucle. Continuez vers Java StringBuilder.