String : immutabilité et concaténation
L'idée en une phrase
En Java, une String ne se modifie jamais : toute « modification » crée une nouvelle chaîne. Ce seul principe explique les trois erreurs les plus courantes avec le texte : comparer avec ==, oublier de récupérer le résultat d'une méthode, et concaténer dans une boucle.
Une image pour comprendre : la plaque gravée
Une String, c'est comme une plaque gravée. On ne peut pas effacer une lettre : pour changer le texte, il faut graver une nouvelle plaque.
Et deux plaques peuvent porter exactement le même texte tout en étant deux objets différents. C'est toute la différence entre equals (« même texte ? ») et == (« même plaque ? »).
À l'inverse, un StringBuilder, c'est un tableau blanc : on écrit, on complète, on efface. Et quand le texte est prêt, on le grave sur une plaque avec toString().
Comparer deux chaînes : toujours equals
@GetMapping("/commandes")
public List<CommandeDto> lister(@RequestParam String tri) {
if (tri == "date") { // ❌ presque toujours false
// ...
}
if (tri.equals("date")) { // ✅ compare le texte
// ...
}
}
| Écriture | Ce qu'elle compare |
|---|---|
a == b |
est-ce le même objet ? (la même plaque) |
a.equals(b) |
est-ce le même texte ? |
a.equalsIgnoreCase(b) |
le même texte, sans tenir compte des majuscules |
Pourquoi == « marche » parfois ?
Quand le même texte est écrit plusieurs fois dans ton code, Java réutilise le même objet. Dans un test comme celui-ci, == renvoie donc true par hasard :
String a = "date";
String b = "date";
a == b; // true… par chance
Mais un texte qui vient de l'extérieur (requête HTTP, base de données, fichier) est toujours un nouvel objet. Le même == renvoie alors false. C'est le bug classique : ça marche dans le test, ça échoue en production.
Astuce : IntelliJ IDEA souligne les comparaisons de chaînes avec
==. Si tu vois cet avertissement, corrige-le.
Et si la chaîne peut être null ?
String tri = null;
tri.equals("date"); // 💥 NullPointerException
"date".equals(tri); // ✅ false, sans erreur
Objects.equals(tri, attendu); // ✅ sans erreur, même si les deux peuvent être null
Mets le texte connu à gauche : il ne peut pas être null.
Les méthodes renvoient une nouvelle chaîne
Puisqu'une String ne change jamais, trim(), toUpperCase(), replace()… ne modifient pas la chaîne. Elles en renvoient une nouvelle, qu'il faut récupérer :
String email = " Alice@Exemple.fr ";
email.trim().toLowerCase(); // ❌ le résultat est perdu, email n'a pas changé
email = email.trim().toLowerCase(); // ✅ "alice@exemple.fr"
Vérifier qu'un texte n'est pas vide
Une saisie « vide » peut être null, "" ou ne contenir que des espaces. Les méthodes ne voient pas toutes la même chose :
| Méthode | "" |
" " |
|---|---|---|
isEmpty() |
vide | pas vide |
isBlank() (Java 11) |
vide | vide ✅ |
if (nom == null || nom.isBlank()) {
throw new IllegalArgumentException("Le nom est obligatoire");
}
Note : pour les données reçues par un contrôleur, la validation Spring (
@NotBlanksur un champ du DTO, avec@Valid) fait ce travail pour toi. Préfère-la à desifdans le code.
Assembler du texte
Une seule ligne : + suffit
String libelle = commande.client() + " – " + commande.montant() + " €"; // ✅
C'est lisible et Java l'optimise très bien. Pas besoin de StringBuilder ici.
Dans une boucle : StringBuilder
String rapport = "";
for (Commande c : commandes) {
rapport += c.client() + " : " + c.montant() + " €\n"; // ❌ recopie tout le texte à chaque tour
}
À chaque tour, Java grave une nouvelle plaque avec tout le texte déjà accumulé, plus la nouvelle ligne. Avec 10 lignes, personne ne voit rien. Avec 50 000 commandes, c'est des milliards de caractères recopiés.
Avec un StringBuilder (le tableau blanc), on complète le même texte :
StringBuilder rapport = new StringBuilder();
for (Commande c : commandes) {
rapport.append(c.client()).append(" : ").append(c.montant()).append(" €\n"); // ✅
}
String resultat = rapport.toString();
Avec un séparateur : joining
Pour assembler une liste avec des virgules, c'est encore plus simple, et le cas du dernier élément (« pas de virgule après ») est géré pour toi :
String clients = commandes.stream()
.map(Commande::client)
.collect(Collectors.joining(", ")); // "Alice, Bruno, Chloé"
String chemin = String.join(" / ", "Accueil", "Commandes", "Détail"); // "Accueil / Commandes / Détail"
| Situation | Outil |
|---|---|
| une expression sur une ligne | + |
| construire du texte dans une boucle | StringBuilder |
| une liste avec un séparateur | Collectors.joining ou String.join |
Le texte sur plusieurs lignes : les text blocks
Depuis Java 15, les text blocks (entre """) écrivent du texte sur plusieurs lignes, sans \n ni guillemets échappés. Le cas le plus courant en Spring Boot : une requête dans un repository.
@Query("""
SELECT c FROM Commande c
WHERE c.statut = :statut
AND c.montant > :montantMin
ORDER BY c.date DESC
""")
List<Commande> chercher(Statut statut, BigDecimal montantMin);
Java retire automatiquement l'indentation commune : le texte reste aligné avec ton code sans espaces en trop.
Deux usages à connaître en Spring Boot
Dans les logs : des {}, pas des +
log.debug("Commande " + id + " validée pour " + client); // ❌ le texte est construit même si DEBUG est désactivé
log.debug("Commande {} validée pour {}", id, client); // ✅ construit seulement si le log est affiché
Les {} sont remplacés dans l'ordre par les valeurs qui suivent. C'est plus lisible, et le texte n'est assemblé que si le message est réellement écrit.
Dans une requête SQL : jamais de concaténation
// ❌ DANGER : injection SQL
String sql = "SELECT * FROM client WHERE nom = '" + nom + "'";
Si un utilisateur envoie ' OR '1'='1 comme nom, la requête renvoie toute la table. Avec un texte bien choisi, il peut même supprimer des données. C'est l'injection SQL, une des failles les plus connues du web.
Passe toujours les valeurs en paramètres : la base les traite comme des valeurs, jamais comme du code.
@Query("SELECT c FROM Client c WHERE c.nom = :nom") // ✅ :nom est un paramètre
List<Client> chercherParNom(String nom);
Ou plus simplement, laisse Spring Data écrire la requête :
List<Client> findByNom(String nom); // ✅
Les bonnes habitudes
- Compare toujours avec
equals, jamais avec==. Et mets le texte connu à gauche :"date".equals(tri). - Récupère le résultat des méthodes de
String:email = email.trim();. isBlank()plutôt queisEmpty()pour vérifier une saisie, ou mieux,@NotBlanksur le DTO.+sur une ligne,StringBuilderdans une boucle,joiningpour une liste.- Des
{}dans les logs, pas de concaténation. - Jamais de valeur concaténée dans une requête SQL. Toujours des paramètres.
À retenir
- Une
Stringne se modifie jamais : chaque méthode renvoie une nouvelle chaîne, qu'il faut récupérer. ==compare les objets,equalscompare le texte. Pour du texte, c'est toujoursequals.==qui « marche en test » est un piège : les textes venus de l'extérieur sont toujours de nouveaux objets.- Dans une boucle,
+=recopie tout le texte à chaque tour : utiliseStringBuilderoujoining. - Les text blocks (
""") rendent lisibles les requêtes et textes sur plusieurs lignes. - Une requête SQL construite par concaténation est une faille de sécurité.