Concaténation de chaînes: concat () vs opérateur «+»

499

En supposant que la chaîne a et b:

a += b
a = a.concat(b)

Sous le capot, sont-ce la même chose?

Voici concat décompilé comme référence. J'aimerais pouvoir décompiler le+ opérateur pour voir ce que cela fait.

public String concat(String s) {

    int i = s.length();
    if (i == 0) {
        return this;
    }
    else {
        char ac[] = new char[count + i];
        getChars(0, count, ac, 0);
        s.getChars(0, i, ac, count);
        return new String(0, count + i, ac);
    }
}
shsteimer
la source
3
Je ne suis pas sûr que l' +on puisse décompiler.
Galen Nare
1
Utilisez javap pour désassembler un fichier de classe Java.
Hot Licks
En raison de l '«immuabilité», vous devriez probablement utiliser StringBufferou StringBuilder- (thread dangereux donc plus rapide, à la place
Ujjwal Singh

Réponses:

560

Non, pas tout à fait.

Premièrement, il y a une légère différence de sémantique. Si aest null, a.concat(b)lance alors un NullPointerExceptionmais a+=btraitera la valeur d'origine de acomme si elle l'était null. De plus, la concat()méthode accepte uniquement les Stringvaleurs tandis que l' +opérateur convertit silencieusement l'argument en chaîne (en utilisant la toString()méthode pour les objets). Alors leconcat() méthode est donc plus stricte dans ce qu'elle accepte.

Pour regarder sous le capot, écrivez un cours simple avec a += b;

public class Concat {
    String cat(String a, String b) {
        a += b;
        return a;
    }
}

Désassemblez maintenant avec javap -c(inclus dans le Sun JDK). Vous devriez voir une liste comprenant:

java.lang.String cat(java.lang.String, java.lang.String);
  Code:
   0:   new     #2; //class java/lang/StringBuilder
   3:   dup
   4:   invokespecial   #3; //Method java/lang/StringBuilder."<init>":()V
   7:   aload_1
   8:   invokevirtual   #4; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   11:  aload_2
   12:  invokevirtual   #4; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   15:  invokevirtual   #5; //Method java/lang/StringBuilder.toString:()Ljava/lang/    String;
   18:  astore_1
   19:  aload_1
   20:  areturn

Donc, a += best l'équivalent de

a = new StringBuilder()
    .append(a)
    .append(b)
    .toString();

La concatméthode devrait être plus rapide. Cependant, avec plus de chaînes, la StringBuilderméthode gagne, au moins en termes de performances.

Le code source de Stringet StringBuilder(et sa classe de base package-private) est disponible dans src.zip du Sun JDK. Vous pouvez voir que vous créez un tableau de caractères (redimensionnant si nécessaire), puis le jetez lorsque vous créez la finale String. Dans la pratique, l'allocation de mémoire est étonnamment rapide.

Mise à jour: Comme le note Pawel Adamski, les performances ont changé dans le HotSpot plus récent. javacproduit toujours exactement le même code, mais le compilateur de bytecode triche. Les tests simples échouent complètement car le corps entier du code est jeté. La somme System.identityHashCode(non String.hashCode) montre que le StringBuffercode a un léger avantage. Sous réserve de modifications lors de la publication de la prochaine mise à jour ou si vous utilisez une autre machine virtuelle Java. De @lukaseder , une liste des éléments intrinsèques de la JVM HotSpot .

Tom Hawtin - sellerie
la source
4
@HyperLink Vous pouvez voir le code à l'aide javap -cd'une classe compilée qui l'utilise. (Oh, comme dans la réponse. Il suffit d'interpréter le démontage du bytecode, ce qui ne devrait pas être si difficile.)
Tom Hawtin - tackline
1
Vous pouvez consulter la spécification JVM pour comprendre les différents bytecodes. Le truc que vous voudriez référencer est dans le chapitre 6. Un peu obscur, mais vous pouvez obtenir l'essentiel assez facilement.
Hot Licks
1
Je me demande pourquoi le compilateur Java utilise StringBuildermême lorsqu'il joint deux chaînes? S'il Stringinclut des méthodes statiques pour concaténer jusqu'à quatre chaînes, ou toutes les chaînes dans un String[], le code peut ajouter jusqu'à quatre chaînes avec deux allocations d'objet (le résultat Stringet son support char[], ni une redondante), ni un nombre quelconque de chaînes avec trois allocations ( le String[]résultat Stringet le support char[], seul le premier étant redondant). En l'état, utiliser au mieux laStringBuilder volonté , nécessitent quatre allocations, et il faudra copier tous les caractères deux fois.
supercat
Cette expression, a + = b. Cela ne signifie-t-il pas: a = a + b?
plus vénérable monsieur le
3
Les choses ont changé depuis la création de cette réponse. Veuillez lire ma réponse ci-dessous.
Paweł Adamski
90

Niyaz est correct, mais il convient également de noter que l'opérateur spécial + peut être converti en quelque chose de plus efficace par le compilateur Java. Java a une classe StringBuilder qui représente une chaîne mutable non thread-safe. Lors de l'exécution d'un tas de concaténations de chaînes, le compilateur Java convertit silencieusement

String a = b + c + d;

dans

String a = new StringBuilder(b).append(c).append(d).toString();

ce qui pour les grandes cordes est nettement plus efficace. Pour autant que je sache, cela ne se produit pas lorsque vous utilisez la méthode concat.

Cependant, la méthode concat est plus efficace lors de la concaténation d'une chaîne vide sur une chaîne existante. Dans ce cas, la JVM n'a pas besoin de créer un nouvel objet String et peut simplement retourner celui existant. Voir la documentation concat pour le confirmer.

Donc, si vous êtes très préoccupé par l'efficacité, vous devez utiliser la méthode concat lors de la concaténation de chaînes éventuellement vides, et utiliser + sinon. Cependant, la différence de performances devrait être négligeable et vous ne devriez probablement jamais vous en soucier.

Eli Courtwright
la source
concat infact ne fait pas ça. J'ai édité mon article avec une décompilation de la méthode
concat
10
en fait il le fait. Regardez les premières lignes de votre code de concaténation. Le problème avec concat est qu'il génère toujours une nouvelle chaîne ()
Marcio Aguiar
2
@MarcioAguiar: peut-être que vous voulez dire que + génère toujours un nouveau String- comme vous le dites, concata une exception lorsque vous concatérez un vide String.
Blaisorblade
45

J'ai exécuté un test similaire à @marcio mais avec la boucle suivante à la place:

String c = a;
for (long i = 0; i < 100000L; i++) {
    c = c.concat(b); // make sure javac cannot skip the loop
    // using c += b for the alternative
}

Juste pour faire bonne mesure, je l'ai également ajouté StringBuilder.append(). Chaque test a été exécuté 10 fois, avec 100 000 répétitions pour chaque série. Voici les résultats:

  • StringBuildergagne haut la main. Le résultat de l'horloge était de 0 pour la plupart des descentes et le plus long a pris 16 ms.
  • a += b prend environ 40000 ms (40 s) pour chaque exécution.
  • concat ne nécessite que 10000 ms (10 s) par cycle.

Je n'ai pas encore décompilé la classe pour voir les éléments internes ou l'exécuter via le profileur, mais je soupçonne que je a += bpasse beaucoup de temps à créer de nouveaux objets StringBuilderpuis à les reconvertir String.

ckpwong
la source
4
Le temps de création d'objet est vraiment important. C'est pourquoi dans de nombreuses situations, nous utilisons directement StringBuilder plutôt que de profiter du StringBuilder derrière +.
coolcfan
1
@coolcfan: Quand +est utilisé pour deux chaînes, y a-t-il des cas où l'utilisation StringBuilderest meilleure que ce serait le cas String.valueOf(s1).concat(s2)? Une idée de la raison pour laquelle les compilateurs n'utiliseraient pas ce dernier [ou omettraient l' valueOfappel dans les cas où l' s1on sait qu'ils ne sont pas nuls]?
supercat
1
@supercat désolé je ne sais pas. Peut-être que les gens qui sont derrière ce sucre sont les meilleurs pour y répondre.
coolcfan
25

La plupart des réponses proviennent de 2008. Il semble que les choses ont changé au fil du temps. Mes derniers benchmarks réalisés avec JMH montrent que sur Java 8 +est environ deux fois plus rapide queconcat .

Ma référence:

@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS)
public class StringConcatenation {

    @org.openjdk.jmh.annotations.State(Scope.Thread)
    public static class State2 {
        public String a = "abc";
        public String b = "xyz";
    }

    @org.openjdk.jmh.annotations.State(Scope.Thread)
    public static class State3 {
        public String a = "abc";
        public String b = "xyz";
        public String c = "123";
    }


    @org.openjdk.jmh.annotations.State(Scope.Thread)
    public static class State4 {
        public String a = "abc";
        public String b = "xyz";
        public String c = "123";
        public String d = "!@#";
    }

    @Benchmark
    public void plus_2(State2 state, Blackhole blackhole) {
        blackhole.consume(state.a+state.b);
    }

    @Benchmark
    public void plus_3(State3 state, Blackhole blackhole) {
        blackhole.consume(state.a+state.b+state.c);
    }

    @Benchmark
    public void plus_4(State4 state, Blackhole blackhole) {
        blackhole.consume(state.a+state.b+state.c+state.d);
    }

    @Benchmark
    public void stringbuilder_2(State2 state, Blackhole blackhole) {
        blackhole.consume(new StringBuilder().append(state.a).append(state.b).toString());
    }

    @Benchmark
    public void stringbuilder_3(State3 state, Blackhole blackhole) {
        blackhole.consume(new StringBuilder().append(state.a).append(state.b).append(state.c).toString());
    }

    @Benchmark
    public void stringbuilder_4(State4 state, Blackhole blackhole) {
        blackhole.consume(new StringBuilder().append(state.a).append(state.b).append(state.c).append(state.d).toString());
    }

    @Benchmark
    public void concat_2(State2 state, Blackhole blackhole) {
        blackhole.consume(state.a.concat(state.b));
    }

    @Benchmark
    public void concat_3(State3 state, Blackhole blackhole) {
        blackhole.consume(state.a.concat(state.b.concat(state.c)));
    }


    @Benchmark
    public void concat_4(State4 state, Blackhole blackhole) {
        blackhole.consume(state.a.concat(state.b.concat(state.c.concat(state.d))));
    }
}

Résultats:

Benchmark                             Mode  Cnt         Score         Error  Units
StringConcatenation.concat_2         thrpt   50  24908871.258 ± 1011269.986  ops/s
StringConcatenation.concat_3         thrpt   50  14228193.918 ±  466892.616  ops/s
StringConcatenation.concat_4         thrpt   50   9845069.776 ±  350532.591  ops/s
StringConcatenation.plus_2           thrpt   50  38999662.292 ± 8107397.316  ops/s
StringConcatenation.plus_3           thrpt   50  34985722.222 ± 5442660.250  ops/s
StringConcatenation.plus_4           thrpt   50  31910376.337 ± 2861001.162  ops/s
StringConcatenation.stringbuilder_2  thrpt   50  40472888.230 ± 9011210.632  ops/s
StringConcatenation.stringbuilder_3  thrpt   50  33902151.616 ± 5449026.680  ops/s
StringConcatenation.stringbuilder_4  thrpt   50  29220479.267 ± 3435315.681  ops/s
Paweł Adamski
la source
Je me demande pourquoi Java Stringn'a jamais inclus de fonction statique pour former une chaîne en concaténant les éléments de a String[]. Utiliser +pour concaténer 8 chaînes en utilisant une telle fonction nécessiterait la construction et l'abandon ultérieur String[8], mais ce serait le seul objet qui devrait être construit abandonné, tandis que l'utilisation d'un StringBuildernécessiterait la construction et l'abandon d'une StringBuilderinstance et d' au moins un char[]magasin de sauvegarde.
supercat
@supercat Certaines String.join()méthodes statiques ont été ajoutées dans Java 8, en tant que wrappers de syntaxe rapide autour de la java.util.StringJoinerclasse.
Ti Strga
@TiStrga: La gestion de a-t-elle +changé pour utiliser de telles fonctions?
supercat
@supercat Cela casserait la rétrocompatibilité binaire, donc non. C'était simplement en réponse à votre commentaire "pourquoi String n'a jamais inclus de fonction statique": maintenant il y a une telle fonction. Le reste de votre proposition (refactoring +pour l'utiliser) nécessiterait plus que ce que les développeurs Java sont prêts à changer, malheureusement.
Ti Strga
@TiStrga: Existe-t-il un moyen pour qu'un fichier de bytecode Java indique "Si la fonction X est disponible, appelez-la; sinon faites autre chose" d'une manière qui pourrait être résolue lors du chargement d'une classe? Générer du code avec une méthode statique qui peut soit être chaînée à la méthode statique de Java, soit utiliser un constructeur de chaînes s'il n'est pas disponible semble la solution optimale.
supercat
22

Tom a raison de décrire exactement ce que fait l'opérateur +. Il crée un temporaire StringBuilder, ajoute les pièces et se termine partoString() .

Cependant, jusqu'à présent, toutes les réponses ignorent les effets des optimisations d'exécution HotSpot. Plus précisément, ces opérations temporaires sont reconnues comme un modèle commun et sont remplacées par un code machine plus efficace au moment de l'exécution.

@marcio: Vous avez créé un micro-benchmark ; avec les JVM modernes, ce n'est pas un moyen valide de profiler le code.

La raison pour laquelle l'optimisation de l'exécution est importante est que bon nombre de ces différences de code - y compris la création d'objets - sont complètement différentes une fois que HotSpot démarre. La seule façon de savoir avec certitude est de profiler votre code in situ .

Enfin, toutes ces méthodes sont en fait incroyablement rapides. Cela pourrait être un cas d'optimisation prématurée. Si vous avez du code qui concatène beaucoup de chaînes, la façon d'obtenir la vitesse maximale n'a probablement rien à voir avec les opérateurs que vous choisissez et à la place l'algorithme que vous utilisez!

Jason Cohen
la source
Je suppose que par «ces opérations temporaires», vous entendez l'utilisation de l'analyse d'échappement pour allouer des objets «tas» sur la pile lorsque cela est prouvé. Bien que l'analyse d'échappement soit présente dans HotSpot (utile pour supprimer une certaine synchronisation), je ne le crois pas, c'est au moment de la rédaction, u
Tom Hawtin - tackline le
21

Que diriez-vous de quelques tests simples? Utilisé le code ci-dessous:

long start = System.currentTimeMillis();

String a = "a";

String b = "b";

for (int i = 0; i < 10000000; i++) { //ten million times
     String c = a.concat(b);
}

long end = System.currentTimeMillis();

System.out.println(end - start);
  • La "a + b"version exécutée en 2500ms .
  • L' a.concat(b)exécuté en 1200ms .

Testé plusieurs fois. L' concat()exécution de la version a pris la moitié du temps en moyenne.

Ce résultat m'a surpris car la concat()méthode crée toujours une nouvelle chaîne (elle retourne un " new String(result)". Il est bien connu que:

String a = new String("a") // more than 20 times slower than String a = "a"

Pourquoi le compilateur n'était-il pas capable d'optimiser la création de chaînes dans le code "a + b", sachant que cela aboutissait toujours à la même chaîne? Cela pourrait éviter une nouvelle création de chaîne. Si vous ne croyez pas la déclaration ci-dessus, testez par vous-même.

Marcio Aguiar
la source
J'ai testé sur java jdk1.8.0_241 votre code, Pour moi le code "a + b" donne des résultats optimisés. Avec concat (): 203 ms et avec "+": 113 ms . Je suppose que dans la version précédente, ce n'était pas optimisé.
Akki
6

Fondamentalement, il existe deux différences importantes entre + et la concatméthode.

  1. Si vous utilisez la méthode concat , vous ne pourrez concaténer des chaînes que dans le cas de l' opérateur + , vous pouvez également concaténer la chaîne avec n'importe quel type de données.

    Par exemple:

    String s = 10 + "Hello";

    Dans ce cas, la sortie doit être 10Hello .

    String s = "I";
    String s1 = s.concat("am").concat("good").concat("boy");
    System.out.println(s1);

    Dans le cas ci-dessus, vous devez fournir deux chaînes obligatoires.

  2. La deuxième et principale différence entre + et concat est que:

    Cas 1: Supposons que je concatende les mêmes chaînes avec l' opérateur concat de cette manière

    String s="I";
    String s1=s.concat("am").concat("good").concat("boy");
    System.out.println(s1);

    Dans ce cas, le nombre total d'objets créés dans le pool est de 7 comme ceci:

    I
    am
    good
    boy
    Iam
    Iamgood
    Iamgoodboy

    Cas 2:

    Maintenant, je vais concaténer les mêmes chaînes via l' opérateur +

    String s="I"+"am"+"good"+"boy";
    System.out.println(s);

    Dans le cas ci-dessus, le nombre total d'objets créés n'est que de 5.

    En fait, lorsque nous concaténons les chaînes via l' opérateur + , il maintient une classe StringBuffer pour effectuer la même tâche comme suit: -

    StringBuffer sb = new StringBuffer("I");
    sb.append("am");
    sb.append("good");
    sb.append("boy");
    System.out.println(sb);

    De cette façon, il ne créera que cinq objets.

Donc, les gars, ce sont les différences de base entre + et la méthode concat . Prendre plaisir :)

Deepak Sharma
la source
Ma chère, Vous savez très bien que tout littéral de chaîne traité comme un objet String lui-même qui est stocké dans le pool de chaînes. Dans ce cas, nous avons 4 littéraux de chaîne. Il est donc évident qu'au moins 4 objets doivent être créés dans le pool.
Deepak Sharma
1
Je ne pense pas: String s="I"+"am"+"good"+"boy"; String s2 = "go".concat("od"); System.out.println(s2 == s2.intern());imprime true, ce qui signifie qu'il "good"n'était pas dans le pool de chaînes avant d'appelerintern()
fabian
Je ne parle que de cette ligne String s = "I" + "am" + "good" + "boy"; Dans ce cas, les 4 littéraux de chaîne sont conservés dans un pool, d'où la création de 4 objets dans le pool.
Deepak Sharma
4

Par souci d'exhaustivité, je voulais ajouter que la définition de l'opérateur '+' peut être trouvée dans le JLS SE8 15.18.1 :

Si une seule expression d'opérande est de type String, la conversion de chaîne (§5.1.11) est effectuée sur l'autre opérande pour produire une chaîne au moment de l'exécution.

Le résultat de la concaténation de chaînes est une référence à un objet String qui est la concaténation des deux chaînes d'opérandes. Les caractères de l'opérande de gauche précèdent les caractères de l'opérande de droite dans la chaîne nouvellement créée.

L'objet String est nouvellement créé (§12.5) sauf si l'expression est une expression constante (§15.28).

À propos de l'implémentation, le JLS dit ce qui suit:

Une implémentation peut choisir d'effectuer la conversion et la concaténation en une seule étape pour éviter de créer puis de supprimer un objet String intermédiaire. Pour augmenter les performances de la concaténation de chaînes répétée, un compilateur Java peut utiliser la classe StringBuffer ou une technique similaire pour réduire le nombre d'objets String intermédiaires créés par l'évaluation d'une expression.

Pour les types primitifs, une implémentation peut également optimiser la création d'un objet wrapper en convertissant directement d'un type primitif en chaîne.

Donc, à en juger par «un compilateur Java peut utiliser la classe StringBuffer ou une technique similaire pour réduire», différents compilateurs peuvent produire différents octets-code.

dingalapadum
la source
2

L' opérateur + peut fonctionner entre une chaîne et une valeur de type de données chaîne, char, entier, double ou flottant. Il convertit simplement la valeur en sa représentation sous forme de chaîne avant la concaténation.

L' opérateur concat ne peut être effectué que sur et avec des chaînes. Il vérifie la compatibilité des types de données et génère une erreur s'ils ne correspondent pas.

Sauf ceci, le code que vous avez fourni fait la même chose.

Niyaz
la source
2

Je ne pense pas.

a.concat(b)est implémenté dans String et je pense que l'implémentation n'a pas beaucoup changé depuis les premières machines java. L' +implémentation de l'opération dépend de la version Java et du compilateur. +Est actuellement mis en œuvre en utilisantStringBuffer pour rendre l'opération aussi rapide que possible. Peut-être qu'à l'avenir, cela va changer. Dans les versions précédentes de Java +, le fonctionnement sur Strings était beaucoup plus lent car il produisait des résultats intermédiaires.

Je suppose que cela +=est mis en œuvre en utilisant +et optimisé de manière similaire.

Bartosz Bierkowski
la source
7
"Actuellement + est implémenté à l'aide de StringBuffer" False It's StringBuilder. StringBuffer est l'impl threadsafe de StringBuilder.
Frédéric Morin
1
Il s'agissait de StringBuffer avant Java 1.5, car c'était la version lorsque StringBuilder a été introduit pour la première fois.
ccpizza
0

Lorsque vous utilisez +, la vitesse diminue à mesure que la longueur de la chaîne augmente, mais lorsque vous utilisez concat, la vitesse est plus stable, et la meilleure option est d'utiliser la classe StringBuilder qui a une vitesse stable pour ce faire.

Je suppose que vous pouvez comprendre pourquoi. Mais la meilleure façon de créer de longues chaînes est d'utiliser StringBuilder () et append (), l'une ou l'autre vitesse sera inacceptable.

iamreza
la source
1
l'utilisation de l'opérateur + équivaut à l'utilisation de StringBuilder ( docs.oracle.com/javase/specs/jls/se8/html/… )
ihebiheb