日々常々

ふつうのプログラマがあたりまえにしたいこと。

文字列連結から見るシステム内で扱う型について

Stringの連結はそう簡単なものではない - じゅんいち☆かとうの技術日誌

いまさらなのか、いまだになのか、文字列結合ネタからの派生です。

実際、業務アプリケーションで文字列結合が致命的なパフォーマンス劣化を引き起こすようなことは無いと思います。ですが、Javaを使うなら知っていて当然になっている事でもありまして。こういうのは普段からどの程度クラスの性質や扱い方を意識しているかのリトマス試験紙的なものになるのかなと思ったりします。理解した上で可読性重視の選択をするのも当然有りで、選択した結果であれば十二分以上の理由になると思います。ある程度フォーマット等で可読性の改善は出来るものの、StringBuilder.append とか並べるのって見難いですし。

それよりも、最近 String を扱う際に「文字列である必要があるの?」と疑問を持つことが多いです。「このデータは、この瞬間、String であることが本当に適切なのか?」と。確かに人の目に触れる瞬間は常に文字列です。それこそ数値であれ、日付であれ、文字列となってから表示されます。ですが、システム内で文字列として扱われている多くのデータが、文字列である必要がある場面って実際使われているより少ないのではないかと感じています。*1


大層なタイトル付けた気がするけど、ムズカシイ話は以上で終わり。
で、せっかくなので遊んでみました。元ネタに書かれているコードを殆ど書き写した感じですけれど、System.nanoTime() も使ってみたかったのです。

System.nanoTime() は昨日知りました。このブログ見て。 → Javaパフォーマンス計測 そんなタイマーで大丈夫か? - プログラマーの脳みそ

package test;

public class StringTest {
	public static void main(String[] args) {
		final int loop = 100000;

		// +演算子
		EditStopwatch plus = new EditStopwatch() {
			String editString() {
				String value = "";
				for (int i = 0; i < loop; i++) value += "a";
				return value;
			}
		};

		// String.concat
		EditStopwatch concat = new EditStopwatch() {
			String editString() {
				String value = "";
				for (int i = 0; i < loop; i++) value = value.concat("a");
				return value;
			}
		};

		// StringBuffer.append
		EditStopwatch stringbuffer = new EditStopwatch() {
			String editString() {
				StringBuffer value = new StringBuffer();
				for (int i = 0; i < loop; i++) value.append("a");
				return value.toString();
			}
		};

		// StringBuilder.append
		EditStopwatch stringbuilder = new EditStopwatch() {
			String editString() {
				StringBuilder value = new StringBuilder();
				for (int i = 0; i < loop; i++) value.append("a");
				return value.toString();
			}
		};

		// オマケ
		EditStopwatch array = new EditStopwatch() {
			String editString() {
				char[] arr = new char[loop];
				for (int i = 0; i < loop; i++) arr[i] = 'a';
				return new String(arr);
			}
		};

		// 実行
		plus.execute();
		concat.execute();
		stringbuffer.execute();
		stringbuilder.execute();
		array.execute();

		// 結果
		System.out.println("plus          :" + plus.time);
		System.out.println("concat        :" + concat.time);
		System.out.println("stringbuffer  :" + stringbuffer.time);
		System.out.println("stringbuilder :" + stringbuilder.time);
		System.out.println("array         :" + array.time);
	}
}

abstract class EditStopwatch {
	long time;
	String value;

	abstract String editString();

	void execute() {
		long begin = System.nanoTime();
		value = editString();
		long end = System.nanoTime();
		time += (end - begin);
	}
}

結果

・1回目
plus          :6012479095
concat        :3287798932
stringbuffer  :8282611
stringbuilder :5590036
array         :2138235

・2回目
plus          :5944232670
concat        :2999206984
stringbuffer  :6346267
stringbuilder :5552395
array         :2133530

・3回目
plus          :5899566279
concat        :2962490857
stringbuffer  :9840413
stringbuilder :5610139
array         :2441069

流石に配列は反則ですかね。くっつける文字も String じゃなく char にしちゃってますし。でも「String で扱う必要なくね?」って感覚はこの辺から来てたりもします。処理前にサイズが判って、それから文字を足していくーならば配列でいいじゃん、とか言い出すともっと、こう、色々方法はあるでしょうけれども。でも途中で文字列である必要は全く無くて、適した形で扱うのがいいと思います。*2
リソースは殆ど無限に近いと感じるでしょうが、実際は有限です。出来るところは節約した方がいいんじゃないの、とか思ったりもします。


(追記1)ループ数変えても面白いですよ。

・10,000回ループ
plus          :87346818
concat        :31026002
stringbuffer  :1725901
stringbuilder :807131
array         :281448

・1,000ループ
plus          :2619005
concat        :1092001
stringbuffer  :384532
stringbuilder :383676
array         :27375


(追記2)
Javaパフォーマンス計測 文字列操作編 - プログラマーの脳みそ

*1:日付を文字列に変換してから比較するとかばっかり見ているせいかも知れませんが…。

*2:数が可変だと配列は途端に不適切になりますしね。