Java strings

Les chaînes de caractères sont manipulées en java à l'aide de trois classes : String, StringBuffer et StringBuilder.
La classe String est immuable, alors que StringBuffer et StringBuilder sont modifiables.

La classe String

Une String est un objet, que l'on peut donc créer en utilisant l'opérateur new :
String s1 = new String("abc");
Remarquer l'utilisation des doubles quotes pour créer une String (alors qu'on utilise les simples quotes pour créer un char).

Java permet d'utiliser d'utiliser une syntaxe plus compacte : les string litterals :
String s1 = "abc";
Depuis java 15, on peut initialiser une chaîne sur plusieurs lignes :
class MultiLine {
    public static void main(String[] args){
        String html = """
<html>
    <body>
        <h1>Hello, world</h1>
    </body>
</html>
""";
        System.out.println(html);    
    }
}
(code dans MultiLine.java)

Les 3 premiers guillemets (""") doivent être suivis d'un retour à la ligne.
Dans le string litteral, si une ligne se termine par antislash (\), la String ne contient pas de newline (\n).
(pratique pour les lignes très longues).
String singleLine = """
          Hello \
          World
          """;

Tableau de caractères

En interne, une String est représentée par un tableau de char (16 bits) ; depuis java 9, si une String ne contient que des caractères ISO-8859-1, la représentation est optimisée pour utiliser des byte (8 bits) ; on parle de compact strings.
String s1 = "abc";
est équivalent à :
char[] data = {'a', 'b', 'c'};
String str = new String(data); // noter constructeur par recopie
Mais on ne peut pas directement manipuler une chaîne comme un tableau :
// ne passe pas à la compilation
String s2 = "une chaîne de caractères";
for(int i=0; i < s2.length; i++){
    System.out.println(s2[i]);
}
On doit utiliser les méthodes length() et charAt() :
String s2 = "une chaîne de caractères";
for(int i=0; i < s2.length(); i++){
    System.out.println(s2.charAt(i));
}
(code dans CharAt.java)

Comparaison de chaînes

Comme pour les autres objets, l'opérateur == teste l'égalité des références et ne renverra pas true si le contenu de deux chaînes est identique (ce qui est le résultat auquel on s'attend intuitivement).
Dans le cas général, Object.equals() revient au même que ==, mais equals() est overridé dans String pour faire une comparaison caractère par caractère et renvoyer un résultat conforme à l'intuition (voir la page java.lang.Object pour la comparaison des objets en général).
Il faut donc toujours utiliser la méthode equals() pour comparer deux chaînes.
public class Equals1{
    public static void main(String[] args){
        String s1 = "abc", s2 = "def"; // noter initialisations multiples pour des variables de même type
        String s3 = s1 + s2;
        String s4 = "abcdef";
        System.out.println("s3 == s4 : " + (s3 == s4) );
        System.out.println("s3.equals(s4) : " + s3.equals(s4) );
        System.out.println("s4.equals(s3) : " + s4.equals(s3) );
    }
}
(code dans Equals1.java)
java Equals1
s3 == s4 : false
s3.equals(s4) : true
s4.equals(s3) : true
Illustre bien le comportement de String :
== teste l'égalité entre références, et renvoie false même si deux chaînes ont même contenu ;
String.equals() a été overridé, et renvoie true lorsque deux chaînes ont même contenu.

Optimisation du compilateur

Mais == peut parfois renvoyer true, suite à une optimisation du compilateur.
public class Equals2{
    public static void main(String[] args){
        String s1 = "azerty";
        String s2 = "azerty";
        System.out.println("s1 == s2 : " + (s1 == s2) );
    }
}
(code dans Equals2.java)
java Equals2
s1 == s2 : true
D'après le test dans Equals1, on s'attend à ce que s1 == s2 soit false.
Mais ici, le compilateur a repéré que le contenu de s1 et s2 est exactement identique, et les deux références pointent vers la même adresse mémoire, donc == renvoie true.

Concaténation

La classe String contient une méthode concat(), pas très pratique pour une opération si fréquente.
Java permet donc la concaténation de chaînes avec l'opérateur +.
Une fonctionnalité très utile est qu'avec l'opérateur +, une String peut être concaténée avec un objet de n'importe quel type (référence ou primitif), ce qui n'est pas le cas avec concat().
En effet, lorsque l'opérateur + est utilisé, la méthode toString() est implicitement appelée sur chacun des opérandes, précédée d'un autoboxing si nécessaire.
public class Concat{

    public static void main(String[] args){
        String s1 = "abc", s2 = "def"
        String s3;
        
        s3 = s1.concat(s2);
        System.out.println("Avec concat : s3 = " + s3);
        
        s3 = s1 + s2;
        System.out.println("Avec + : s3 = " + s3);
        
        int i = 4;
        String s4; 
        
        // s4 = s3.concat(i); // pas possible
        s4 = s3 + i;
        System.out.println("s4 = " + s4);
    }
}
(code dans Concat.java)
java Concat
Avec concat : s3 = abcdef
Avec + : s3 = abcdef
s4 = abcdef4

Performance

On a vu qu'une String est immuable. Donc lorsqu'on fait :
s3 = s1 + s2;
ce n'est pas le contenu de la zone mémoire référencé par s3 qui change. Une autre String est fabriquée, et s3 est modifiée pour pointer vers une autre zone de la mémoire. Le compilateur fait une opération du genre :
s3 = new StringBuilder(s1).append(s2).toString();
On voit que ça implique la création de 2 objets supplémentaires :
- une StringBuilder,
- une autre String, avec toString().
s1 pointera vers cette nouvelle String.
De plus, le contenu de la chaîne est copié deux fois en mémoire :
- Une fois lorsqu'on fait s3 = new StringBuilder(s1)
- Une fois lors du toString()

Dans une boucle, la différence peut devenir très importante :
public class BenchConcat{
    
    private final static int N = 100000;
    private final static String test = "A";

    public static void main(String[] args){
        
        long t1, t2, dt;
        
        t1 = System.currentTimeMillis();
        concatString();
        t2 = System.currentTimeMillis();
        dt = t2 - t1;
        System.out.println("concat String : dt = " + dt);
        
        t1 = System.currentTimeMillis();
        concatStringBuilder();
        t2 = System.currentTimeMillis();
        dt = t2 - t1;
        System.out.println("concat StringBuilder : dt = " + dt);
        
    }
    
    private static void concatString(){
        String res = "";
        for(int i = 0; i < N; i++){
            res += test;
        }
    }

    private static void concatStringBuilder(){
        StringBuilder res = new StringBuilder("");
        for(int i = 0; i < N; i++){
            res.append(test);
        }
    }

}
(code dans BenchConcat.java)
java BenchConcat 
concat String : dt = 911
concat StringBuilder : dt = 8

Méthodes fréquentes

La classe String contient de nombreuses méthodes pour manipuler les chaînes ; voir la liste complète dans la documentation de l'API java.

Classes StringBuilder et StringBuffer

Ces deux classes représentent aussi des chaînes de caractères, mais sont mutables.
Il est donc conseillé de les utiliser pour des raisons de performance lorsqu'on a du code qui manipule des chaînes.
Les deux méthodes les plus utiles sont append() et insert().

StringBuffer a été introduite en java 1, et elle est "thread safe" : lorsqu'on utilise une méthode de StringBuffer, on a la garantie qu'elle s'exécutera jusqu'au bout, sans qu'un autre thread ne puisse venir perturber son exécution.
Cet aspect thread safe n'étant pas utile dans de nombreuses situations, une nouvelle classe a été introduite en java 5 : StringBuilder, dont l'API est compatible avec StringBuffer.

Il est donc conseillé d'utiliser en général StringBuilder lorsqu'on n'a pas besoin de l'aspect "thread safe".
Exercice : ToString.
Exercice : Palindrome.

String formating

Un équivalent de printf() existe en java :
System.out.printf("format-string" [, arg1, arg2, ... ]);
La même syntaxe peut être utilisée avec String.format().
        String str = "toto";
        System.out.printf("str = %s%n", str); // str = toto
        System.out.printf("str = %S%n", str); // str = TOTO
        
        double dbl = 12.0;
        System.out.printf("dbl = %f%n", dbl); // dbl = 12,000000
        System.out.printf("dbl = % f%n", dbl); // dbl =  12,000000
        System.out.printf("dbl = %+f%n", dbl); // dbl = +12,000000
        System.out.printf("dbl = %.2f%n", dbl); // dbl = 12,00
        
        var locale = new Locale("en");
        System.out.printf(locale, "dbl = %f%n", dbl); // dbl = 12.000000
        
        int i = 4500;
        System.out.printf("i = %d%n", i); // i = 4500
        System.out.printf("i = %,d%n", i); // i = 4 500
        System.out.printf(locale, "i = %,d%n", i); // i = 4,500
        
        String str2;
        str2 = String.format("%s", str);
        System.out.println(str2); // toto
(code dans Format.java)

La référence complète se trouve dans le javadoc de la classe java.util.Formatter