String
est une classe fondamentale du langage Java, puisque c'est elle qui permet de gérer les chaînes de caractères. C'est une classe qui comporte une quarantaine de méthodes, et quelques caractéristiques importantes.
Tout d'abord cette classe est déclarée
final
. Ce qui signifie qu'il n'est pas possible de l'étendre. Toute méthode qui reçoit un objet de type
String
a donc la garantie que les méthodes qu'elle appelle sur cet objet ont bien le comportement nominal : elles n'ont pas pu être surchargées.
Ensuite, un objet
String
contient un tableau de
char
, qui stocke la chaîne de caractères proprement dite. La classe est conçue de telle manière, qu'une fois un objet
String
construit, il n'est plus possible de modifier le contenu de ce tableau. On dit que la classe
String
est
immutable
, ce qui signifie que la donnée qu'elle contient est en "lecture seule". Elle suit en cela un design pattern qui porte ce nom. Entre autres choses, cela permet deux optimisations, sur la méthode
hashCode()
et la méthode
equals()
que nous verrons en détails.
Exemple 8. Construction d'un objet
String
String s1 = "Bonjour le monde !" ; String s11 = "Bonjour le monde !" ; String s2 = new String("Bonjour le monde !") ;
(s1 == s11)
, on se rend compte que la valeur retournée est
true
. Ce qui signifie que la machine Java n'a en fait créé qu'un seul objet, qu'elle a affecté aux deux variables
s1
et
s11
. C'est une optimisation parfaitement valide : comme il n'est pas possible de changer la chaîne de caractères portée par l'objet créé par la JVM, on peut parfaitement n'en créer qu'un au lieu de deux.
En revanche, si l'on évalue
(s1 == s2)
, là le résultat est
false
, on a forcé la création d'un nouvelle chaîne de caractères par un appel explicite au
new
.
+
, comme on peut s'y attendre. Là encore, le principe de non modification d'une chaîne de caractères s'applique. Examinons le code suivant.
Exemple 9. Concaténation de deux
String
String s1 = "Bonjour" ; String s2 = "le monde !" ; String s3 = s1 + " " + s2 ;
s1
et
" "
, puis une deuxième, concaténation de cette première chaîne et de
s2
.
Si l'on n'y prend pas garde, on voit que ces opérations de concaténation peuvent mener à la création de nombreuses chaînes intermédiaires, ce qui a un coût non négligeable en calculs et en ressources mémoire, surtout si ces chaînes sont longues. Une bonne habitude à prendre, est donc de n'utiliser ces opérations de concaténation que dans les cas où l'on se fiche de consommer trop de ressoures (rares), et dans les cas où l'on n'a que peu de concaténations à faire.
Dans tous les autres cas (c'est à dire dans tous les cas ou presque), on préfèrera utiliser un
StringBuilder
, de la manière suivante.
Exemple 10. Concaténation de deux
String
avec
StringBuilder
String s1 = "Bonjour" ; String s2 = "le monde !" ; StringBuilder sb = new StringBuilder() ; sb.append(s1).append(" ").append(s2) ; String s3 = sb.toString() ;
s3
est identique au précédent, mais dans ce cas, aucune chaîne de caractères intermédiaire n'a été utilisée.
Notons que la concaténation peut opérer sur des objets qui ne sont pas des chaînes de caractères. Dans le cas des objets, la machine Java appelle la méthode
toString()
des objets à convertir. Dans le cas d'un type de base, un appel particulier à la méthode
String.valueOf()
est fait. Cette méthode est définie dans la classe
String
, il n'est donc pas possible de la surcharger.
Exemple 11. Concaténation de
String
et d'objets quelconques
Marin marin = new Marin("Surcouf", "Robert") ; String s1 = "Bonjour " + marin ; StringBuilder sb = new StringBuilder() ; sb.append("Bonjour ").append(marin) ; String s2 = sb.toString() ;
s1
et
s2
contiennent des résultats identiques. Encore une fois, il a été plus rapide de calculer
s2
que
s1
.
La première classe (historiquement) utilisée pour concaténer efficacement des chaînes de caractères est
StringBuffer
. La classe
StringBuilder
est apparue dans l'API de Java 5, elle n'est donc pas disponible en version 4 et précédentes. Les méthodes exposées par ces deux classes sont identiques, on peut donc utiliser l'une ou l'autre de façon équivalente, du point de vue de la compilation.
La seule différence entre ces deux classes est que
StringBuffer
est une classe synchronisée. Elle doit donc être utilisée en environnement
multithreadé
. La conséquence est que son utilisation est plus coûteuse que
StringBuilder
qui ne l'est pas.
Par défaut, on doit donc utiliser
StringBuilder
. Si la variable concernée est en concurrence d'accès, alors on doit utiliser
StringBuffer
.
StringBuilder
. On peut donc éviter d'écrire un
StringBuilder
à la main lorsque l'on concatène entre elles quelques chaînes de caractères.
Lorsque l'on doit concaténer des chaînes de caractères qui se trouvent dans une liste, on dispose de plusieurs patterns, tous introduits en Java 8. Le premier consiste à ouvrir un
stream
sur notre liste et a concaténer les éléments de ce
stream
en utilisant un collector, objet qui fait partie de l'API Stream et que nous verrons dans la suite de ce cours. L'API Stream apporte d'autres fonctionnalités très utiles, telles que la possibilité de retirer des chaînes de ce
stream
suivants certains critères, et celle de transformer ces chaînes.
Si l'on n'a pas besoin des fonctionnalités complémentaires de l'API Stream, on peut encore utiliser deux autre méthodes. La première est la méthode
join()
de la classe
String
.
Dans l'exemple suivant,
concat1
vaut
onetwothree
, car la concaténation se fait sans caractère de séparation. La chaîne
concat2
vaut
one, two, three
, du fait du caractère de séparation passé en premier paramètre.
Exemple 12. Concaténation d'une liste de
String
en utilisant
String.join()
String [] strings = {"one", "two, "three"} ; // déclaration d'un tableau de String String concat1 = String.join(strings) ; String concat2 = String.join(", ", strings) ;
StringJoiner
. Cette classe permet de concaténer des chaînes de caractères avec un séparateur et avec éventuellement un caractère de début de chaîne et un caractère de fin de chaîne. Le pattern est le suivant.
Exemple 13. Concaténation d'une liste de
String
en utilisant la classe
StringJoiner
StringJoiner stringJoiner = new StringJoiner(", ", "{", "}") ; stringJoiner.add("one") ; stringJoiner.add("two") ; stringJoiner.add("three") ; String concat = stringJoiner.toString() ;
concat
est
{one, two, three}
dans l'exemple précédent.
INVOKE_DYNAMIC
. Cela apporte des nouvelles performances, qui rendent obsolète l'utilisation de
StringBuilder
.
substring()
, qui existe en deux versions :
Exemple 14. Extraction d'une sous-chaîne
String s1 = "Bonjour le monde !" ; String s2 = s1.substring(8) ; // extrait une sous-chaîne à partir du 8ème caractère String s3 = s1.substring(8, 10) ; // extrait une sous-chaîne allant du 8ème au // 10ème caractère
substring()
est très peu coûteuse en calculs. On peut examiner son code à titre d'exemple.
Exemple 15. Extraction d'une sous-chaîne : code exécuté
public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > count) { throw new StringIndexOutOfBoundsException(endIndex); } if (beginIndex > endIndex) { throw new StringIndexOutOfBoundsException(endIndex - beginIndex); } return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value); }
return
.
Tout d'abord, si la sous-chaîne demandée est égale à la chaîne actuelle, alors aucun calcul n'est fait : on retourne la chaîne actuelle. On utilise ici l'optimisation rendue possible par le fait qu'un objet
String
est immutable.
Si ce n'est pas le cas, on construit un nouvel objet
String
, à partir du tableau de caractères de notre chaîne, et de deux index, qui délimitent le début et la fin de la chaîne dans ce tableau. Ce constructeur de
String
va simplement enregistrer la référence de ce tableau, et les deux index. Aucune duplication de tableau n'est faite, et les deux objets : la chaîne originale et la sous-chaînes référencent en fait le même tableau de caractères en mémoire. Encore une fois, cette optimisation est rendue possible par le fait qu'un objet de type
String
est immutable.
hashCode()
et
equals()
ont été écrites dans la classe
String
. Commençons par regarder le code de la méthode
hashCode()
.
Exemple 16. Code de la méthode
hashCode()
de la classe
String
private int hash ; // déclaré plus haut dans la classe String public int hashCode() { int h = hash; if (h == 0) { int off = offset; char val[] = value; int len = count; for (int i = 0; i < len; i++) { h = 31*h + val[off++]; } hash = h; } return h; }
String
enregistre une chaîne de caractères dans un tableau de
char
. Dans certains cas, notamment si la chaîne que l'on manipule a été construite par extraction d'une sous-chaîne d'une première chaîne, ce tableau n'est pas utilisé dans sa totalité : seule une portion, allant de l'index
offset
et de longueur
count
est prise en compte. Cela explique pourquoi le calcul ne parcourt le tableau que de
offset
à
offset + count
.
Lors du premier appel à cette méthode, la variable
hash
vaut 0. Donc, cette méthode calcule un code de hachage, sur la portion du tableau utilisée par cet objet
String
dans une variable
h
. La valeur de cette variable est ensuite sauvegardée dans la variable
hash
, qui est relue lors des appels suivants. Lors d'appels successifs à cette méthode, le calcul n'est donc pas repris.
Le fait qu'une chaîne de caractères soit immutable est ici utilisé : le code de hachage n'est calculé qu'une seule fois, lors du premier appel à
hashCode()
, et simplement relu lors des appels suivants.
Examinons à présent le code de
equals()
.
Exemple 17. Code de la méthode
equals()
de la classe
String
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
char v1[] = value;
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
while (n-- != 0) {
if (v1[i++] != v2[j++])
return false;
}
return true;
}
}
return false;
}
false
est retournée. Sinon, la méthode renvoie
true
.
La comparaison ne se fonde pas sur la comparaison des codes de hachage, car la spécification exacte est que deux chaînes égales ont même code de hachage, mais l'inverse n'est pas nécessairement vrai.
compareTo()
et
compareToIgnoreCase()
: ces deux méthodes retournent un entier (
int
), négatif si la chaîne comparée est inférieure au sens lexicographique, 0 si elles sont égales, positif si la chaîne comparée est supérieure.
startsWith()
,
endsWith()
: renvoient
true
si la chaîne commence ou se termine par la chaîne passée en paramètre.
matches()
,
regionMatches()
: renvoient
true
si la chaîne correspond à la chaîne passée en paramètre au sens des expressions régulières.
String s1 = "Bonjour" ; String s2 = "le monde !" ; int c = s1.compareTo(s2) ; -> c = -42 ; c = s2.compareToIgnoreCase(s1) ; -> c = 10 ; boolean b = s1.startsWith("a") ; -> b = true b = s1.matches("[a-z]*") ; -> b = false b = s1.matches("[a-zA-Z]*") ; -> b = true
indexOf()
et
lastIndexOf()
: ces deux méthodes permettent de localiser un caractère donné dans une chaîne.
indexOf()
existe en deux versions : la première permet de localiser la première occurrence d'un élément dans une chaîne, la seconde prend en plus un index en paramètre, à partir duquel on recherche l'élément passé. Ces deux méthodes retournent -1 si l'élément n'est pas trouvé, ou une valeur entière correspondant à la position de l'élément trouvé dans la chaîne.
substring()
: permet d'extraire une sous-chaîne d'une chaîne donnée.
charAt()
: permet d'extraire un caractère à une position donnée d'une chaîne.
Exemple 18. Recherche d'un caractère dans une chaîne
String texte = "Quand le ciel bas et lourd pèse comme un couvercle" ; int index = texte.indexOf("e") ; while (index > 0) { System.out.println("J'ai trouvé un e en position " + index) ; index = texte.indexOf("e", index + 1) ; }
indexOf()
prend alors en paramètre l'index de la lettre qui suit le dernier
e
que l'on a trouvé.
Le résultat de l'exécution de ce code est le suivant.
J'ai trouvé un e en position 7 J'ai trouvé un e en position 11 J'ai trouvé un e en position 18 J'ai trouvé un e en position 30 J'ai trouvé un e en position 36 J'ai trouvé un e en position 45 J'ai trouvé un e en position 49
replace()
, cette méthode prend des variables
char
en paramètre, et se borne à remplacer toutes les occurrences du premier caractère par le second.
replaceFirst()
et
replaceAll()
: ces deux méthodes prennent deux chaînes de caractères en paramètre. La première contient une expression régulière. Toutes les occurrences de cette expression sont remplacées par la deuxième chaîne.
toUpperCase()
,
toLowerCase()
: retourne une chaîne résultat de la mise en majuscules ou en minuscules de la chaîne originale.
trim()
: retourne une chaîne de laquelle les caractères blancs en tête ou en début de chaîne ont été retirés.
String s = " Quand le ciel " ;
s = s.trim() ;
Dans ce cas la chaîne
s
originale, qui n'est plus référencée par rien, sera effacée par le
garbage collector
.
getBytes()
,
getChars()
: retourne un tableau de
byte
ou de
char
correspondant à cette chaîne de caractères.
split()
: découpe une chaîne de caractères à l'aide d'une expression régulière passée en paramètre. Le résultat du découpage est retourné dans un tableau de
String
.
toCharArray()
: retourne un tableau de char correspondant à cette chaîne de caractères.
char
de Java est codé sur deux octets. Cette différence menait à des incohérences, par exemple dans la méthode
length()
, qui retourne, jusqu'en Java 5, le nombre de
char
, et non pas le nombre de caractères au sens d'Unicode. Ce point a été réglé en Java 6. Il n'a
a priori
pas d'impacts pour le codage des alphabets latins classiques.
length()
a changé :
length()
returns the length of this string. The length is equal to the number of 16-bit Unicode characters in the string.
length()
returns the length of this string. The length is equal to the number of Unicode code units in the string.
String
, c'est le cas par exemple pour la mise en majuscules ou en minuscules. Nous verrons ce point plus en détails quand sera présentée la classe
Locale
, centrale pour la question de l'internationalisation des applications.
String
StringBuffer
et
StringBuilder