String : immutabilité et concaténation

Publié le · Mis à jour le

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 (@NotBlank sur un champ du DTO, avec @Valid) fait ce travail pour toi. Préfère-la à des if dans 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

  1. Compare toujours avec equals, jamais avec ==. Et mets le texte connu à gauche : "date".equals(tri).
  2. Récupère le résultat des méthodes de String : email = email.trim();.
  3. isBlank() plutôt que isEmpty() pour vérifier une saisie, ou mieux, @NotBlank sur le DTO.
  4. + sur une ligne, StringBuilder dans une boucle, joining pour une liste.
  5. Des {} dans les logs, pas de concaténation.
  6. Jamais de valeur concaténée dans une requête SQL. Toujours des paramètres.

À retenir

  • Une String ne se modifie jamais : chaque méthode renvoie une nouvelle chaîne, qu'il faut récupérer.
  • == compare les objets, equals compare le texte. Pour du texte, c'est toujours equals.
  • == 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 : utilise StringBuilder ou joining.
  • 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é.

Avis sur l'article

Pas encore de note : soyez le premier à donner votre avis.

Les avis sont publiés après validation par le modérateur.

Votre note