文字列の連結:concat()と「+」演算子


499

文字列aとbを想定:

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

内部的には、同じものですか?

これは、リファレンスとして逆コンパイルされた連結です。+演算子を逆コンパイルして、それが何をするかを確認したいのですが。

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);
    }
}


3
+逆コンパイルできるかわかりません。
Galen Nare 2013年

1
javapを使用して、Javaクラスファイルを逆アセンブルします。
ホットリックス

「不変性」に起因するあなたは、おそらく使用する必要がありますStringBufferStringBuilder- (代わりに、これより早くスレッド安全ではない
Ujjwal Singhの

回答:


560

いいえ、違います。

まず、セマンティクスにわずかな違いがあります。場合anull、その後、a.concat(b)投げNullPointerExceptionたがa+=b、元の値を扱いますa、それはであるかのようにnull。さらに、concat()メソッドはString値のみを受け入れますが、+演算子は(toString()オブジェクトのメソッドを使用して)サイレントに引数を文字列に変換します。そのため、このconcat()方法では、受け入れるものがより厳密になります。

内部で見るには、次のように簡単なクラスを記述します a += b;

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

javap -c(Sun JDKに含まれている)で逆アセンブルします。以下を含むリストが表示されます。

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

したがって、a += b同等のものです

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

concatこの方法は、より高速にする必要があります。ただし、文字列の数が多いほどStringBuilder、少なくともパフォーマンスの点では、このメソッドが優先されます。

Stringand StringBuilder(およびそのパッケージプライベート基本クラス)のソースコードは、Sun JDKのsrc.zipにあります。char配列を構築し(必要に応じてサイズ変更)、finalを作成するときにそれを破棄していることがわかりますString。実際には、メモリ割り当ては驚くほど高速です。

更新: Pawel Adamskiが指摘するように、パフォーマンスは最近のHotSpotで変更されました。javacまったく同じコードを生成しますが、バイトコードコンパイラは不正を行います。コードの本体全体が破棄されるため、単純なテストは完全に失敗します。合計System.identityHashCode(ではないString.hashCode)は、StringBufferコードにわずかな利点があることを示しています。次のアップデートがリリースされたとき、または別のJVMを使用している場合は、変更される可能性があります。@lukasederHotSpotのJVMの組み込み関数のリスト


4
@HyperLink javap -cこれを使用するコンパイル済みクラスで使用しているコードを確認できます。(ああ、答えのように。バイトコードの逆アセンブリを解釈する必要があるだけで、それほど難しいことではありません。)
Tom Hawtin-タックライン

1
JVM仕様を調べて、個々のバイトコードを理解することができます。参照したい内容は第6章にあります。ややあいまいですが、その要点はかなり簡単に取得できます。
Hot Licks

1
StringBuilder2つの文字列を結合するときでもJavaコンパイラが使用するのはなぜでしょうか。String最大4つの文字列、またはのすべての文字列を連結する静的メソッドが含まれている場合String[]、コードは、2つのオブジェクト割り当て(結果Stringとそのバッキングchar[]、どちらも冗長ではない)を持つ最大4つの文字列、および3つの割り当てを持つ任意の数の文字列( 、String[]結果String、バッキングchar[]、最初のものだけが冗長です)。現状では、使用StringBuilderするには最大で 4つの割り当てが必要で、すべての文字を2回コピーする必要があります。
スーパーキャット2014

その式、a + = b。つまり、a = a + bですか?
最も名誉ある卿

3
この回答が作成されて以来、状況は変化しています。以下の私の答えを読んでください。
パヴェル・アダムスキー

90

Niyazは正しいですが、特別な+演算子をJavaコンパイラーによってより効率的なものに変換できることにも注意してください。Javaには、スレッドセーフではない、変更可能な文字列を表すStringBuilderクラスがあります。一連の文字列連結を実行すると、Javaコンパイラはサイレントに変換します

String a = b + c + d;

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

これは、大きな文字列の場合、はるかに効率的です。私の知る限り、これは、concatメソッドを使用した場合には起こりません。

ただし、空の文字列を既存の文字列に連結する場合、concatメソッドはより効率的です。この場合、JVMは新しいStringオブジェクトを作成する必要はなく、単に既存のオブジェクトを返すことができます。これを確認するには、concatのドキュメントを参照してください。

したがって、効率について非常に懸念がある場合は、空の可能性がある文字列を連結するときにconcatメソッドを使用し、それ以外の場合は+を使用する必要があります。ただし、パフォーマンスの違いはごくわずかで、おそらくこれについて心配する必要はありません。


concat infactはそれを行いません。concatメソッドの逆コンパイルを使用して投稿を編集しました
shsteimer

10
実際にはそうです。連結コードの最初の行を見てください。concatの問題は、常に新しいString()を生成することです
Marcio Aguiar

2
@MarcioAguiar:多分あなたは+が常に新しいを生成することを意味しますString-あなたが言うようにconcat、空を連結するとき1つの例外がありますString
Blaisorblade 2014

45

@marcioと同様のテストを実行しましたが、代わりに次のループを使用しました。

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
}

ちょうどいいので、私も投げ入れましStringBuilder.append()た。各テストは10回実行され、実行ごとに10万回が実行されました。結果は次のとおりです。

  • StringBuilder勝ちます。クロック時間の結果は、ほとんどの実行で0で、最長の時間が16ミリ秒かかりました。
  • a += b 実行ごとに約40000ミリ秒(40秒)かかります。
  • concat 実行あたり10000ms(10s)しか必要ありません。

内部を確認したり、プロファイラーで実行したりするためにクラスを逆コンパイルしていませんが、のa += b新しいオブジェクトを作成StringBuilderしてからに変換することに多くの時間を費やしていると思いますString


4
オブジェクトの作成時間は本当に重要です。これが、多くの状況で、+の後ろにあるStringBuilderを利用するのではなく、直接StringBuilderを使用する理由です。
coolcfan

1
@coolcfan:+が2つの文字列に使用されている場合、使用するStringBuilder方が良い場合はありString.valueOf(s1).concat(s2)ますか?コンパイラが後者を使用しない理由は何かありますか(または、nullでないことがわかっているvalueOf場合は呼び出しを省略しs1ます)。
スーパーキャット2015

1
@supercatごめんなさい。たぶんこの砂糖の後ろにいる人がこれに答えるのに最適な人です。
coolcfan 2015

25

ここでのほとんどの回答は2008年のものです。時間の経過とともに状況が変化したようです。JMHで作成した私の最新のベンチマークは、Java 8の方+がの約2倍速いことを示していますconcat

私のベンチマーク:

@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))));
    }
}

結果:

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

なぜJava Stringは、の要素を連結して文字列を形成する静的関数を含めなかったのでしょうかString[]。この+ような関数を使用して8つの文字列を連結するためにを使用すると、を作成して後で放棄String[8]する必要がありますが、を使用して作成して放棄する必要があるオブジェクトは、を使用するStringBuilderと、StringBuilderインスタンスと少なくとも 1つのchar[]バッキングストアを作成して放棄する必要があります。
スーパーキャット

@supercat一部の静的String.join()メソッドは、java.util.StringJoinerクラスのクイック構文ラッパーとしてJava 8に追加されました。
Ti Strga

@TiStrga:+そのような関数を使用するように処理が変更されましたか?
スーパーキャット

@supercatバイナリの下位互換性が損なわれるため、違います。それは単に「なぜStringが静的関数を含まなかったのか」というコメントへの返答でした。今でそのような関数があります。+悲しいことに、残りの提案(それを使用するためのリファクタリング)では、Java開発者が喜んで変更する以上のものを必要とします。
Ti Strga

@TiStrga:Javaバイトコードファイルが「関数Xが使用可能な場合はそれを呼び出す、それ以外の場合は何かを行う」ことを、クラスのロードプロセスで解決できる方法で示す方法はありますか?Javaの静的メソッドにチェーンできる静的メソッドでコードを生成するか、利用できない場合は文字列ビルダーを使用するのが最適なソリューションのようです。
スーパーキャット

22

トムは、+演算子の機能を正確に説明しています。一時的なを作成しStringBuilder、パーツを追加して、で終了しtoString()ます。

ただし、これまでの回答はすべて、HotSpotランタイムの最適化の影響を無視しています。具体的には、これらの一時的な操作は一般的なパターンとして認識され、実行時により効率的なマシンコードに置き換えられます。

@marcio:マイクロベンチマークを作成しました。最新のJVMでは、これはコードをプロファイルする有効な方法ではありません。

ランタイム最適化が重要である理由は、HotSpotが実行されると、コード内のこれらの違いの多く(オブジェクト作成を含む)が完全に異なるためです。確実に知る唯一の方法は、その場でコードプロファイリングすることです。

最後に、これらの方法はすべて実際には信じられないほど高速です。これは時期尚早の最適化の場合かもしれません。文字列をたくさん連結するコードがある場合、最大速度を得る方法は、選択する演算子ではなく、使用しているアルゴリズムとは関係ありません。


「これらの一時的な操作」とは、エスケープ分析を使用して、スタックに「ヒープ」オブジェクトを割り当て、証明可能であることが正しいことを意味していると思います。エスケープ分析は、(いくつかの同期を除去するのに有用な)ホットスポットに存在しているが、私はそれを信じていない、書いている時点である、U
トムホーティン- tackline

21

簡単なテストはどうですか?以下のコードを使用しました:

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);
  • "a + b"で実行バージョン2500ms
  • a.concat(b)で実行1200ms

数回テストした。concat()バージョンの実行は、平均時間の半分を取りました。

concat()メソッドが常に新しい文字列を作成するため、この結果は私を驚かせました( " new String(result)"を返します。それはよく知られています:

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

コンパイラーが「a + b」コードで文字列の作成を最適化できなかったのはなぜですか?新しい文字列の作成を回避できます。上記の説明を信じない場合は、自分自身をテストしてください。


私はあなたのコードをjava jdk1.8.0_241でテストしました。私にとって「a + b」コードは最適化された結果を与えています。concat()の場合:203ミリ秒、「+」の場合:113ミリ秒。以前のリリースでは、最適化されていなかったと思います。
Akki

6

基本的に、+とconcatメソッドの間には2つの重要な違いがあります。

  1. concatメソッドを使用している場合、連結できるのは文字列のみですが、+演算子の場合は、文字列を任意のデータ型と連結することもできます。

    例えば:

    String s = 10 + "Hello";

    この場合、出力は10Helloになります。

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

    上記の場合、2つの文字列を必須にする必要があります。

  2. +concatの2番目と主な違いは次のとおりです。

    ケース1:この方法 でconcat演算子を使用して同じ文字列を連結するとします

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

    この場合、プールで作成されたオブジェクトの総数は次のように7です。

    I
    am
    good
    boy
    Iam
    Iamgood
    Iamgoodboy

    ケース2:

    次に、+演算子を使用して同じ文字列を連結します

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

    上記の場合、作成されるオブジェクトの総数は5のみです。

    実際、+演算子を使用して文字列を連結すると、次のように同じタスクを実行するためにStringBufferクラスが維持されます。

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

    この方法では、5つのオブジェクトのみが作成されます。

つまり、これらは+concatメソッドの基本的な違いです。楽しい :)


親愛なる皆さん、文字列リテラル自体が文字列プールに格納される文字列オブジェクト自体として扱われることはご存じでしょう。この場合、4つの文字列リテラルがあるので、少なくとも4つのオブジェクトをプールに作成する必要があります。
Deepak Sharma

1
私はそうは思いません:String s="I"+"am"+"good"+"boy"; String s2 = "go".concat("od"); System.out.println(s2 == s2.intern());prints true、つまり"good"呼び出す前に、文字列プールになかったintern()
ファビアン・

この行についてのみ話しています。文字列s = "I" + "am" + "good" + "boy"; この場合、4つの文字列リテラルはすべてプールに保持されます。したがって、4つのオブジェクトをプールに作成する必要があります。
Deepak Sharma 14

4

完全を期すために、「+」演算子の定義がJLS SE8 15.18.1にあることを付け加えておきます。

一方のオペランド式のみが文字列型である場合、実行時に文字列を生成するために、もう一方のオペランドに対して文字列変換(5.1.11)が実行されます。

文字列連結の結果は、2つのオペランド文字列を連結したStringオブジェクトへの参照です。新しく作成された文字列では、左側のオペランドの文字が右側のオペランドの文字の前にあります。

Stringオブジェクトは、式が定数式(§15.28)でない限り、新しく作成されます(§12.5)。

実装について、JLSは次のように述べています。

実装は、中間のStringオブジェクトの作成と破棄を回避するために、変換と連結を1つのステップで実行することを選択できます。繰り返し文字列連結のパフォーマンスを向上させるために、JavaコンパイラはStringBufferクラスまたは同様の手法を使用して、式の評価によって作成される中間のStringオブジェクトの数を減らすことができます。

プリミティブ型の場合、実装は、プリミティブ型から文字列に直接変換することにより、ラッパーオブジェクトの作成を最適化することもできます。

したがって、「JavaコンパイラーはStringBufferクラスまたは同様の手法を使用して削減する可能性がある」と判断すると、コンパイラーによってバイトコードが異なる可能性があります。


2

+演算子、文字列と文字列、文字、整数、ダブルまたは浮動データ型の値との間で動作することができます。連結する前に値を文字列表現に変換するだけです。

連結演算子文字列のみにしてで行うことができます。データ型の互換性をチェックし、一致しない場合はエラーをスローします。

これを除いて、あなたが提供したコードは同じことをします。


2

私はそうは思いません。

a.concat(b)Stringで実装されており、初期のJavaマシン以降、実装はほとんど変わっていないと思います。+操作の実装は、Javaのバージョンやコンパイラに依存します。現在、操作を可能な限り高速にするために+を使用StringBufferして実装されています。将来的には、これは変わるでしょう。以前のバージョンのJava +では、文字列に対する操作は中間結果を生成するため、はるかに低速でした。

私はそれ+=を使用して実装され+、同様に最適化されていると思います。


7
「現在+はStringBufferを使用して実装されています」FalseこれはStringBuilderです。StringBufferは、StringBuilderのスレッドセーフな実装です。
フレデリックモーリン、

1
StringBuilderが最初に導入されたときのバージョンだったため、Java 1.5より前のバージョンはStringBufferでした。
ccpizza

0

+を使用すると、文字列の長さが増加するにつれて速度は低下しますが、concatを使用すると速度はより安定します。最適なオプションは、速度を安定させるStringBuilderクラスを使用することです。

理由はわかると思います。しかし、長い文字列を作成するための完全に最良の方法は、StringBuilder()とappend()を使用することです。どちらの速度でも許容できません。


1
+演算子を使用することは、StringBuilder(docs.oracle.com/javase/specs/jls/se8/html/…)を使用することと同等です
ihebiheb
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.