String #
En Java, le type String
représente une séquence de caractères. Il est très utilisé pour manipuler du texte : noms, messages, fichiers, etc. Une particularité essentielle à comprendre est que les objets de type String
sont immuables : une fois créés, ils ne peuvent pas être modifiés. Toute opération qui semble modifier une chaîne (comme la concaténation, le remplacement ou la suppression de caractères) crée en réalité un nouvel objet String
en mémoire, sans changer l’original.
Par exemple :
String s = "Bonjour";
s = s + " le monde"; // Crée un nouvel objet String
Ici, la chaîne “Bonjour” n’est pas modifiée : une nouvelle chaîne “Bonjour le monde” est créée et la variable s
pointe vers ce nouvel objet. L’ancienne chaîne reste inchangée (et sera éventuellement libérée par le ramasse-miettes).
Cette immuabilité rend les String
sûres et efficaces pour le partage, mais peut entraîner des problèmes de performance si on fait beaucoup de modifications : dans ce cas, il vaut mieux utiliser StringBuilder
.
En Java, les chaînes de caractères (String
) sont représentées en mémoire selon l’encodage UTF-16. Cela signifie que chaque élément du tableau interne d’une chaîne est un « code unit » de 16 bits (un char
Java), mais tous les caractères Unicode ne tiennent pas forcément dans un seul char
.
L’UTF-16 est un encodage qui permet de représenter tous les caractères Unicode. La plupart des caractères courants (latin, accentués, etc.) sont codés sur un seul char
(16 bits), mais certains caractères spéciaux ou emojis, appelés « supplémentaires », nécessitent deux char
consécutifs (appelés une paire de substitution ou surrogate pair).
La méthode charAt(int index)
retourne le char
à la position donnée dans la chaîne, mais ce char
ne correspond pas toujours à un caractère complet pour l’utilisateur. Si la chaîne contient un caractère supplémentaire (hors du plan multilingue de base), charAt
peut retourner seulement une partie de ce caractère (un des deux éléments de la paire de substitution).
Pour manipuler correctement les caractères Unicode, il faut utiliser les méthodes codePointAt
, codePoints()
ou les classes de l’API Character
, qui tiennent compte des paires de substitution et permettent de traiter chaque caractère Unicode comme une entité logique.
String s = "A😊B";
System.out.println(s.length()); // Affiche 4 (car 😊 occupe deux char)
System.out.println(s.charAt(1)); // Affiche un char de la paire surrogate, pas le smiley complet
System.out.println(s.codePointAt(1));// Affiche le code Unicode complet du smiley
Ainsi, il faut être vigilant lors du traitement de chaînes contenant des emojis ou des caractères spéciaux, car la longueur d’une chaîne (length) et l’accès par charAt
ne correspondent pas toujours au nombre réel de caractères.
Utilisez l’application suivante pour explorer la représentation des chaînes de caractères en format UTF-16. Vous pouvez taper des caractères et voir comment la chaîne de caractère est représentée en mémoire.
Voici un exemple en Java qui illustre la plupart des propriétés et méthodes de la classe String.
La méthode split de la classe String en Java est utilisée pour diviser une chaîne en un tableau de sous-chaînes en fonction d’un délimiteur spécifié, qui peut être une chaîne simple ou une expression régulière. Par exemple, split("\\s+")
divise une chaîne sur un ou plusieurs espaces, tandis que split(",")
utilise une virgule comme séparateur. L’expression \\s+
signifie ‘un ou plusieurs espaces.
Nous pourrions utiliser split(";")
pour diviser sur un point-virgule.
Dans ce cours, nous ne présenterons pas la notion d’expression régulière davantage. Elle est traitée dans d’autres cours, notamment au sein du cours INF 6460 Recherche et filtrage d’informations.
StringBuilder #
Le type StringBuilder
en Java permet de construire et de modifier efficacement des chaînes de caractères. Contrairement à la classe String
, qui est immuable (chaque modification crée un nouvel objet), StringBuilder
permet d’ajouter, de modifier ou de supprimer des caractères sans créer de nouveaux objets à chaque opération. Cela le rend particulièrement utile lorsqu’on doit faire de nombreuses modifications ou concaténations de chaînes, par exemple lors de la lecture d’un fichier ou la construction dynamique d’un texte.
L’utilisation de StringBuilder
améliore considérablement les performances, surtout dans les boucles : concaténer des chaînes avec +
dans une boucle crée à chaque fois une nouvelle chaîne, ce qui consomme beaucoup de mémoire et ralentit le programme. StringBuilder
évite ce problème en travaillant sur une seule zone mémoire.
Exemple :
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
sb.append("Ligne ").append(i).append("\n");
}
String resultat = sb.toString();
System.out.println(resultat);
Dans cet exemple, toutes les lignes sont ajoutées efficacement à la même chaîne. Pour des opérations répétées ou sur de gros volumes de texte, StringBuilder
est donc le choix recommandé pour de bonnes performances.
Voici un exemple en Java qui illustre la plupart des propriétés et méthodes de la classe StringBuilder.
CharSequence et subSequence() #
L’interface CharSequence
représente une séquence de caractères lisible : elle est implémentée par plusieurs classes Java comme String
, StringBuilder
et StringBuffer
. Cela permet d’écrire des méthodes qui acceptent n’importe quel type de séquence de caractères, et pas seulement des chaînes immuables.
La méthode subSequence(int start, int end)
permet d’obtenir une portion (sous-séquence) de la séquence de caractères, allant de l’indice start
(inclus) à end
(exclu). C’est utile pour extraire une partie d’un texte sans créer une nouvelle chaîne si ce n’est pas nécessaire.
Exemple avec String :
String texte = "Bonjour le monde";
CharSequence sousTexte = texte.subSequence(8, 14); // "le mon"
System.out.println(sousTexte);
Exemple avec StringBuilder :
StringBuilder sb = new StringBuilder("abcdefg");
CharSequence sousSeq = sb.subSequence(2, 5); // "cde"
System.out.println(sousSeq);
Utiliser CharSequence
rend le code plus flexible : on peut manipuler des chaînes, des buffers ou des builders de la même façon, et extraire facilement des sous-parties avec subSequence()
. La méthode subSequence
évite de faire une copie inutile.
Allocation de mémoire et ramasse-miettes #
Comprendre l’allocation de mémoire et le ramasse-miettes n’est pas obligatoire dans ce cours.
Lorsque vous créez un objet en Java, la mémoire nécessaire est automatiquement allouée dans une zone appelée le « tas » (heap). Contrairement à certains langages comme C ou C++, il n’est pas nécessaire de libérer explicitement la mémoire des objets qui ne sont plus utilisés. Java intègre un mécanisme appelé ramasse-miettes (ou garbage collector) qui se charge de détecter et de libérer automatiquement la mémoire occupée par les objets devenus inaccessibles. Il partage cette caractéristique avec d’autres langages comme C#, JavaScript et Python.
Le ramasse-miettes fonctionne en arrière-plan : il identifie les objets qui ne sont plus référencés par aucune variable ou structure de données, puis récupère la mémoire correspondante pour la rendre disponible à de nouveaux objets. Cela simplifie la gestion de la mémoire et réduit les risques de fuites de mémoire (memory leaks) ou d’erreurs de libération (comme les double free en C).
Cependant, il est important de comprendre que la libération de la mémoire n’est pas instantanée : le ramasse-miettes intervient à des moments choisis par la machine virtuelle Java (JVM), ce qui peut parfois entraîner de légères pauses dans l’exécution du programme. Pour la plupart des applications, ce fonctionnement automatique est un avantage, car il permet de se concentrer sur la logique du programme sans se soucier de la gestion manuelle de la mémoire.
L’allocation de mémoire en Java est automatique et la libération est assurée par le ramasse-miettes, ce qui contribue à la robustesse et à la sécurité des programmes Java.
Par contre, le ramasse-miettes a des inconvénients : il peut provoquer des pauses imprévisibles dans l’exécution du programme, appelées « pauses de collecte », lorsque la JVM décide de libérer la mémoire. Ces pauses sont généralement courtes, mais peuvent devenir perceptibles dans des applications nécessitant une grande réactivité (jeux, systèmes temps réel, etc.). De plus, le développeur a moins de contrôle sur le moment précis où la mémoire est libérée, ce qui peut compliquer l’optimisation des performances dans certains cas particuliers. Enfin, le ramasse-miettes consomme lui-même des ressources processeur, ce qui peut avoir un effet sur l’efficacité globale du programme.
Malgré l’existence du ramasse-miettes, il faut donc tenter de minimiser l’allocation de mémoire. Il faut éviter de créer des objets temporaires quand on peut réutiliser un objet déjà alloué.
Considérons l’exemple suivant.
- Approche 1 (String) : À chaque itération, l’opérateur += crée un nouvel objet String, car les objets String sont immuables en Java. Cela génère de nombreux objets temporaires qui doivent être gérés par le ramasse-miettes, augmentant la charge mémoire et le temps d’exécution.
- Approche 2 (StringBuilder) : En utilisant StringBuilder, un seul objet est créé et modifié à chaque itération. Cela réduit considérablement le nombre d’allocations mémoire et la charge sur le ramasse-miettes, ce qui améliore les performances.