string・StringBuffer・StringBuilderの特徴&性能比較

# はじめに
Javaには文字列を扱うには`String`、`StringBuffer`、`StringBuilder`3つの参照型クラスがあります。

この3つは「文字列を扱う」という共通点がありますが、それぞれの特徴があります。

この記事では、`String`、`StringBuffer`、`StringBuilder`3つの参照型クラスの差を調べ、どんな状況にどれを使えばいいのかについて勉強していきたいと思います。

# StringBuffer・StringBuilderとは
StringBuffer・StringBuilderは文字列の演算（結合または変更）するとき、主に使われます。

もちろん、Stringも`+演算`や`concat()`メソッドを使って文字列を結合することができます。

しかし、Stringの+演算で文字列を結合すると、結合された文字列の新たなStringインスタンスが生成されます。つまり、＋演算をすればするほどメモリの無駄遣いや処理速度低下など性能が低下の原因になり得ます。
```java
String str1 = "aaa";
str1 += " bbb";
str1 += " ccc";
System.out.println(str1); // aaa bbb ccc
// 操作は簡単だが、処理速度が遅い
```
だから、Javaはこのような文字列演算に特化したクラスを提供しています。`StringBuffer`はクラス内部でバッファ(buffer)という空間があり、このbufferを使って文字列演算をすることで、性能を向上させています。
```java
StringBuffer sb = new StringBuffer("aaa");
sb.append(" bbb");
sb.append(" ccc");
System.out.println(sb.toString()); // aaa bbb ccc
// Stringの+演算よりは複雑そうだが、処理速度は速い
```

:::note warn
後で述べますが、StringBufferとStringBuilderの大きな差はSynchronized機能の有無です。
:::

StringBufferのbufferは基本的に16個の文字を保存できます。またbufferのサイズはコンストラクタで設定できます。

文字列演算で結合する文字列のサイズがbufferのサイズより大きいと自動的に文字列に併せてbufferのサイズが変更されます。

StringBufferのbufferサイズは`capacity()`メソッドで確認できます。
```java
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](https://docs.oracle.com/javase/jp/8/docs/api/java/lang/StringBuffer.html)で確認できます。

# Stringと(StringBuffer、StringBuilder)を比較
## 文字列型の不変と可変
### Stringは不変
JavaでStringオブジェクトの**データは変更できません**。

下記のコードを見ると"hello"のデータを持っているstr変数に" world"を結合します。
結合によって"hello world"というデータが作られ、新しいメモリ領域に保存されます。"hello"が保存されている領域は参照する変数がなくなるため、ガベージコレクションによって解放対象になります。
```java
String str = "hello";
str = str + " world";

System.out.println(str); // hello world
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/8a87191a-1ea2-062e-7706-00106320ee6a.png)
このようにStringは不変オブジェクトのため、データが変わらない文字列を頻繁に読み込む際に使います。

しかし、文字列の演算が頻繁に行われる場合Stringクラスを使うとメモリに開放対象の領域が多くなり、アプリケーションのパフォーマンスに悪影響を及ぼします。

:::note info
Stringオブジェクトの詳細は私の記事[【Java】Stringについて](https://qiita.com/alswnd/items/f7d559cdc4cd67564d68)を参考してください。
:::

### Stringbuffer・StringBuilderは可変
Stringbuffer・StringBuilderは文字列を扱うところはStringオブジェクトと同じですが、文字列演算で既存のオブジェクトサイズを超えると既存のbufferのサイズを増やせるので、可変的だという差があります。

そのため、Stringbuffer・StringBuilderは`append()`や`delete()`等のAPIを用いて同一オブジェクト内で文字列を変更することができます。したがって文字列の結合、変更、削除が頻繁に行われる場合使うとパフォーマンス向上ができます。
```java
StringBuffer sb= new StringBuffer("hello");
sb.append(" world");
```
![img1.daumcdn.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/7d9c2241-fcd6-862c-514f-3edd94a976ed.png)

## 文字列結合の性能比較
Javaで文字列を結合する方法は4つあります。

`+演算`、`String.concat()`メソッド、`StringBuffer`、`StringBuilder`でそれぞれの特徴について説明します。

### 文字列結合（＋演算）
Javaでプログラミングをするとき、文字列を扱う場合が多いです。例えば文字列を結合するときは"hello"+" world"のように＋演算で結合しがちです。

そこで、Javaの＋演算はどのように行われているのか調べてみました。

実際にJavaは文字列を＋演算で結合すると、コンパイル前に内部で`StringBuilder`クラスを宣言して、その文字列を返します。

つまり、`"hello"+" world"`の文字列結合があれば、これは`new StringBuilder("hello").append(" world").toString()`と同じわけです。

```java
String str1 = "hello" + " world";
String str2 = new StringBuilder("hello").append(" world").toString()
//2つのコードは同じです。
```
このように文字列演算が少ない場合、大きな差はないとみられます。

しかし、次のように文字列演算が多い場合、単純に＋演算で結合するとメモリと処理の性能が低下します。
```java
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で文字列を宣言した方がよさそうです。
```java
StringBuilder sb = "a";

for(int i = 0; i < 10000; i++) {
    sb = new StringBuilder(sb).append("a").toString();
}
```

### String.concat()

次のコードがString.concat()メソッドです。
```java
String str = "hello";
str = str.concat(" world");
```
concat()メソッドは連結する文字列をnew String()でインスタンスを生成して結合します。したがって文字列をconcat()メソッドで連続に結合すれば、アプリケーションの性能低下になり得ます。

### 文字列の結合性能比較結果
`+演算`、`String.concat()`メソッド、`StringBuffer`、`StringBuilder`4つの方法で文字列結合による性能は下記の図で見られます。
![img1.daumcdn.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/212e8e92-7bb8-6959-40ff-86dacca9397c.png)

この結果によって、「Javaで文字列を扱うときは、必ずStringBufferとStringBuilderを使うべきだ。」と考えるはずですが、それは文字列の使い方によると思います。

なぜかというと、StringBufferやStringBuilderを宣言するときはbufferのサイズを設定する必要があります。これはかなりのリソースがかかる作業です。加えて文字列の変更時にbufferのサイズの変更やデータの修正など色んな演算が必要なので、演算の回数が少ない場合はStringクラスを使うのが性能的に良いかと思います。

また、Stringクラスはサイズが決まっているので、単純にデータを読み込むだけなら処理速度は速いです。

つまり、<font color="red">**文字列の変更が少ない場合はString**</font>を<font color="blue">**文字列の結合や変更などの作業が多い場合はStringBuffer**</font>を使うのがいいと思います。


# StringBufferとStringBuilderの違い
## マルチスレッド環境での安全性(Thread Safe)
StringBufferとStringBuilderはどっちもbufferを持っている可変オブジェクトという特徴があり、提供しているメソッドも同じです。

で、この2つの違いは何でしょうか。
それは<font color="red">**マルチスレッド(Thread)環境で安全(Safe)かどうか**</font>それだけです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/af0f5010-f08d-6307-7fc2-9f618cb428fd.png)

実際にコードでテストしてみます。

次のコードはStringBufferとStringBuilderを宣言して、2つのマルチスレッドでStringBufferとStringBuilderのオブジェクトにそれぞれ"A"を１万回追加します。

2つのスレッドが"A"を１万回ずづ追加するので文字列のサイズは20000になるはずです。
```java
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して追加するロジックでテストしてみます。
```java
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の方が一番早いです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678234/8ed9b768-291d-f889-4b3d-f9e455525830.png)
このグラフをでも分かる通り処理にかかった時間はString＞StringBuffer＞StringBuilder順でした。

もしシングルスレッド環境ではStringBuilderを使うのが性能面で一番優れていますが、
実際、今日はマルチスレッド環境で動いてるサービスが多いだけ、安全なStringBufferでコードを書くのがいいと思います。（StringBufferとStringBuilderの処理速度の差はほぼなし）

# まとめ
|  | String | StringBuffer | StringBuilder |
|:-:|:-:|:-:|:-:|
| **不変、可変** | 不変  |  可変 | 可変  |
| **Thread Safe**  | O  | O  | X  |
| **処理速度**  | 遅い  | 早い  | 早い  |
| **使いどころ**  | 文字列演算が少ない、<br>マルチスレッド環境  | 文字列演算が多い、<br>マルチスレッド環境  | 文字列演算が多い、<br>シングルスレッド環境  |

表で分かる通り<font color="red">**読み込む作業が多い場合はString**</font>を<font color="blue">**そのほかはStringBuffer**</font>を使うようにします。

以上です。

次は、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