はじめに
Javaには文字列を扱うにはString
、StringBuffer
、StringBuilder
3つの参照型クラスがあります。
この3つは「文字列を扱う」という共通点がありますが、それぞれの特徴があります。
この記事では、String
、StringBuffer
、StringBuilder
3つの参照型クラスの差を調べ、どんな状況にどれを使えばいいのかについて勉強していきたいと思います。
StringBuffer・StringBuilderとは
StringBuffer・StringBuilderは文字列の演算(結合または変更)するとき、主に使われます。
もちろん、Stringも+演算
やconcat()
メソッドを使って文字列を結合することができます。
しかし、Stringの+演算で文字列を結合すると、結合された文字列の新たなStringインスタンスが生成されます。つまり、+演算をすればするほどメモリの無駄遣いや処理速度低下など性能が低下の原因になり得ます。
String str1 = "aaa";
str1 += " bbb";
str1 += " ccc";
System.out.println(str1); // aaa bbb ccc
// 操作は簡単だが、処理速度が遅い
だから、Javaはこのような文字列演算に特化したクラスを提供しています。StringBuffer
はクラス内部でバッファ(buffer)という空間があり、このbufferを使って文字列演算をすることで、性能を向上させています。
StringBuffer sb = new StringBuffer("aaa");
sb.append(" bbb");
sb.append(" ccc");
System.out.println(sb.toString()); // aaa bbb ccc
// Stringの+演算よりは複雑そうだが、処理速度は速い
後で述べますが、StringBufferとStringBuilderの大きな差はSynchronized機能の有無です。
StringBufferのbufferは基本的に16個の文字を保存できます。またbufferのサイズはコンストラクタで設定できます。
文字列演算で結合する文字列のサイズがbufferのサイズより大きいと自動的に文字列に併せてbufferのサイズが変更されます。
StringBufferのbufferサイズはcapacity()
メソッドで確認できます。
StringBuffer sb = new StringBuffer(); // bufferの基本サイズは16
System.out.println(sb.capacity());
sb.append("aaa bbb ccc ddd eee");
System.out.println(sb.capacity()); // 演算後bufferの基本サイズは34
Stringbufferの内部メソッド等の詳細はクラスBuffer_Docsで確認できます。
Stringと(StringBuffer、StringBuilder)を比較
文字列型の不変と可変
Stringは不変
JavaでStringオブジェクトのデータは変更できません。
下記のコードを見ると"hello"のデータを持っているstr変数に" world"を結合します。
結合によって"hello world"というデータが作られ、新しいメモリ領域に保存されます。"hello"が保存されている領域は参照する変数がなくなるため、ガベージコレクションによって解放対象になります。
String str = "hello";
str = str + " world";
System.out.println(str); // hello world
このようにStringは不変オブジェクトのため、データが変わらない文字列を頻繁に読み込む際に使います。
しかし、文字列の演算が頻繁に行われる場合Stringクラスを使うとメモリに開放対象の領域が多くなり、アプリケーションのパフォーマンスに悪影響を及ぼします。
Stringオブジェクトの詳細は私の記事【Java】Stringについてを参考してください。
Stringbuffer・StringBuilderは可変
Stringbuffer・StringBuilderは文字列を扱うところはStringオブジェクトと同じですが、文字列演算で既存のオブジェクトサイズを超えると既存のbufferのサイズを増やせるので、可変的だという差があります。
そのため、Stringbuffer・StringBuilderはappend()
やdelete()
等のAPIを用いて同一オブジェクト内で文字列を変更することができます。したがって文字列の結合、変更、削除が頻繁に行われる場合使うとパフォーマンス向上ができます。
StringBuffer sb= new StringBuffer("hello");
sb.append(" world");
文字列結合の性能比較
Javaで文字列を結合する方法は4つあります。
+演算
、String.concat()
メソッド、StringBuffer
、StringBuilder
でそれぞれの特徴について説明します。
文字列結合(+演算)
Javaでプログラミングをするとき、文字列を扱う場合が多いです。例えば文字列を結合するときは"hello"+" world"のように+演算で結合しがちです。
そこで、Javaの+演算はどのように行われているのか調べてみました。
実際にJavaは文字列を+演算で結合すると、コンパイル前に内部でStringBuilder
クラスを宣言して、その文字列を返します。
つまり、"hello"+" world"
の文字列結合があれば、これはnew StringBuilder("hello").append(" world").toString()
と同じわけです。
String str1 = "hello" + " world";
String str2 = new StringBuilder("hello").append(" world").toString()
//2つのコードは同じです。
このように文字列演算が少ない場合、大きな差はないとみられます。
しかし、次のように文字列演算が多い場合、単純に+演算で結合するとメモリと処理の性能が低下します。
String str1 = "a";
for(int i = 0; i < 10000; i++) {
str1 = str1 + "a";
}
// 上のコードは下のコードと同じです。
// つまり、毎回 new StringBuilder() オブジェクトメモリを生成し、また変数に代入することを一万回繰り返します。
String str2 = new String("a");
for (int i = 0; i < 10000; i++) {
str2 = new StringBuilder(str2).append("a").toString();
}
上記のように文字列演算が多いときは、最初からStringBuilderで文字列を宣言した方がよさそうです。
StringBuilder sb = "a";
for(int i = 0; i < 10000; i++) {
sb = new StringBuilder(sb).append("a").toString();
}
String.concat()
次のコードがString.concat()メソッドです。
String str = "hello";
str = str.concat(" world");
concat()メソッドは連結する文字列をnew String()でインスタンスを生成して結合します。したがって文字列をconcat()メソッドで連続に結合すれば、アプリケーションの性能低下になり得ます。
文字列の結合性能比較結果
+演算
、String.concat()
メソッド、StringBuffer
、StringBuilder
4つの方法で文字列結合による性能は下記の図で見られます。
この結果によって、「Javaで文字列を扱うときは、必ずStringBufferとStringBuilderを使うべきだ。」と考えるはずですが、それは文字列の使い方によると思います。
なぜかというと、StringBufferやStringBuilderを宣言するときはbufferのサイズを設定する必要があります。これはかなりのリソースがかかる作業です。加えて文字列の変更時にbufferのサイズの変更やデータの修正など色んな演算が必要なので、演算の回数が少ない場合はStringクラスを使うのが性能的に良いかと思います。
また、Stringクラスはサイズが決まっているので、単純にデータを読み込むだけなら処理速度は速いです。
つまり、文字列の変更が少ない場合はStringを文字列の結合や変更などの作業が多い場合はStringBufferを使うのがいいと思います。
StringBufferとStringBuilderの違い
マルチスレッド環境での安全性(Thread Safe)
StringBufferとStringBuilderはどっちもbufferを持っている可変オブジェクトという特徴があり、提供しているメソッドも同じです。
で、この2つの違いは何でしょうか。
それはマルチスレッド(Thread)環境で安全(Safe)かどうかそれだけです。
実際にコードでテストしてみます。
次のコードはStringBufferとStringBuilderを宣言して、2つのマルチスレッドでStringBufferとStringBuilderのオブジェクトにそれぞれ"A"を1万回追加します。
2つのスレッドが"A"を1万回ずづ追加するので文字列のサイズは20000になるはずです。
StringBuffer stringBuffer = new StringBuffer();
StringBuilder stringBuilder = new StringBuilder();
new Thread(() -> {
for(int i=0; i<10000; i++) {
stringBuffer.append("A");
stringBuilder.append("A");
}
}).start();
new Thread(() -> {
for(int i=0; i<10000; i++) {
stringBuffer.append("A");
stringBuilder.append("A");
}
}).start();
結果
StringBuffer.length : 20000
StringBuilder.length : 19888
しかし、上の結果ではサイズが違うのが分かります。
このようにStringBuilderはマルチスレッド環境でスレッドが同時に実行された場合、片方のスレッドで読み書きしているデータをもう一方のスレッドが読み書きしてしまう恐れがあります。
一方、StringBufferは同期化処理(Synchronized)によるThread Safeでサイズが20000で正しく出力されました。
処理速度比較
では、今まで演算方法について調べてみましたが、これらの中でどれが処理速度が速いのか確認します。
for文"A"
を100万回loopして追加するロジックでテストしてみます。
String str = "";
long plusStartTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
str = str + "a";
}
long plusEndTime = System.currentTimeMillis();
StringBuilder stringBuilder = new StringBuilder();
long builderStartTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
stringBuilder = stringBuilder.append("a");
}
long builderEndTime = System.currentTimeMillis();
StringBuffer stringBuffer = new StringBuffer();
long bufferStartTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
stringBuffer = stringBuffer.append("a");
}
long bufferEndTime = System.currentTimeMillis();
結果
「+演算 」: 78086(ms)
「StringBuffer」: 11(ms)
「StringBuilder」: 10(ms)
文字列の演算にかかった時間を見ると性能的にはThread Safeを採用しないStringBuilderの方が一番早いです。
このグラフをでも分かる通り処理にかかった時間はString>StringBuffer>StringBuilder順でした。
もしシングルスレッド環境ではStringBuilderを使うのが性能面で一番優れていますが、
実際、今日はマルチスレッド環境で動いてるサービスが多いだけ、安全なStringBufferでコードを書くのがいいと思います。(StringBufferとStringBuilderの処理速度の差はほぼなし)
まとめ
String | StringBuffer | StringBuilder | |
---|---|---|---|
不変、可変 | 不変 | 可変 | 可変 |
Thread Safe | O | O | X |
処理速度 | 遅い | 早い | 早い |
使いどころ | 文字列演算が少ない、 マルチスレッド環境 |
文字列演算が多い、 マルチスレッド環境 |
文字列演算が多い、 シングルスレッド環境 |
表で分かる通り読み込む作業が多い場合はStringをそのほかはStringBufferを使うようにします。
以上です。
次は、JavaのEnumについて記事を書こうと思います。
最後まで読んでいただき、ありがとうございました。
参考サイト
javaSilver 黒本
http://www.tcpschool.com/java/java_api_stringBuffer
https://ifuwanna.tistory.com/221
https://javacan.tistory.com/entry/41
https://gogomalibu.tistory.com/96
https://venishjoe.net/post/java-string-concatenation-and-performance/
https://madplay.github.io/post/difference-between-string-stringbuilder-and-stringbuffer-in-java
https://kotlinworld.com/36