Java

[Java]文字列結合のappendチェーンは害悪[新人のなぜ?は正しい]

新人の頃に私は出会いまいました。

StringBuffer/StringBuilderでappendを何度もチェーンしているソースに。

その頃は、「なぜこんなに見づらい書き方をしてるのだろう?」と思いました。

「+でつなげばいいのに」と

ちなみにこんなソースです。

StringBuffer sb = new StringBuffer();
sb.append("ほげ").append("ふが")..................

そして最近、また長いappendチェーンに出会いました。

流石に「+でつなげばいい」とは思いませんでしたが、

その時の感想は、「なぜこんなに見づらい書き方をしているのだろう?」でした。

文字列結合を+でしない理由

前置きには書きましたが、そもそも「+」で結合しないのはなぜでしょうか?

Java屋にとっては当たり前のことと思われがちですが、
初心者だけじゃなく、案外ベテランでも知らないって場合もあったりします。

知らなかった人のために言うと、+での文字列結合が絶望的に遅いからです。

簡単な例を出します。10万回文字列結合を繰り返す処理を行います。
最後にミリ秒を表示します。(表示のための+結合は見やすくするためなので、許してください笑)

StringBufferで結合した場合

Test.java
======================
public class Test {
	public static void main(String[] args) {
		StringBuffer sb = new StringBuffer();
		long start = System.currentTimeMillis();
		for (int i = 0; i < 100000; i++) {
			sb.append("hoge");
		}
		long end = System.currentTimeMillis();
		System.out.println((end - start) + "ms");
	}
}

[結果]
7ms

ほうほう

StringBuilderで結合した場合

Test.java
======================
public class Test {
	public static void main(String[] args) {
		StringBuilder sb = new StringBuilder();
		long start = System.currentTimeMillis();
		for (int i = 0; i < 100000; i++) {
			sb.append("hoge");
		}
		long end = System.currentTimeMillis();
		System.out.println((end - start) + "ms");
	}
}

[結果]
5ms

StringBufferとだいたい一緒か

concatで結合した場合

Test.java
======================
public class Test {
	public static void main(String[] args) {
		String str = "";
		long start = System.currentTimeMillis();
		for (int i = 0; i < 100000; i++) {
			str.concat("hoge");
		}
		long end = System.currentTimeMillis();
		System.out.println((end - start) + "ms");
	}
}

[結果]
5ms

これも一緒

+演算子で結合した場合

Test.java
======================
public class Test {
	public static void main(String[] args) {
		String str = "";
		long start = System.currentTimeMillis();
		for (int i = 0; i < 100000; i++) {
			str += "hoge";
		}
		long end = System.currentTimeMillis();
		System.out.println((end - start) + "ms");
	}
}

[結果]
10045ms

!?

やばいほど違いますね。これを今知った人は今すぐ+結合は止めましょう。

appendチェーンが害悪な理由

さて本題に入ります。

そもそも前項を見ただけだと、「appendすれば速度上がるし最高じゃん!」ってなります。

そうです。最高なんです。ただ、そのソースを後から別の人が見たときになんて思うでしょうか?

サンプルです。

Test.java
======================
public class Test {
	public static void main(String[] args) {
		String str1 = "ほげ";
		String str2 = "ふが";
		String str3 = "ふー";
		String str4 = "ぴよ";

		System.out.println(new StringBuilder().append("昔").append(str1).append("さんが帰国された時に").append(str2).append(str2).append("何を言っているかよくわからなかったので、").append(str3).append("と溜息をついたら").append(str4).append("とひよこが鳴いた").toString());
	}
}

[結果]
昔ほげさんが帰国された時にふがふが何を言っているかよくわからなかったので、ふーと溜息をついたらぴよとひよこが鳴いた

ソース、見づらくないですか?笑 ※文章の内容については触れないでください

何が見づらくさせているかというと、文字と文字の間に入ってくるappendです。非常に読みづらい。

いくら速度が良いからと言って、こんなソース書かれたら初心者が「なぜ?」と思うのは当然でしょう。

appendチェーンは捨ててしまおう

「でもconcatだって同じようにチェーンになるでしょう?+を使うのは良くないのでは?」

はい。だから共通メソッドを作りましょう笑 当たり前っちゃ当たり前なんですが。。。

appendチェーンを捨てた実装 vol.1

Test.java
======================
public class Test {

	public static void main(String[] args) {
		String str1 = "ほげ";
		String str2 = "ふが";
		String str3 = "ふー";
		String str4 = "ぴよ";

		System.out.println(new StringBuilder().append("昔").append(str1).append("さんが帰国された時に").append(str2).append(str2).append("何を言っているかよくわからなかったので、").append(str3).append("と溜息をついたら").append(str4).append("とひよこが鳴いた").toString());

		System.out.println(build("昔", str1, "さんが帰国された時に", str2, str2, "何を言っているかよくわからなかったので、", str3, "と溜息をついたら", str4, "とひよこが鳴いた"));
	}

	private static String build(String... args) {
		StringBuilder sb = new StringBuilder();
		if (args != null) {
			for (int i = 0; i < args.length; i++) {
				sb.append(args[i]);
			}
		}
		return sb.toString();
	}
}

[結果]
昔ほげさんが帰国された時にふがふが何を言っているかよくわからなかったので、ふーと溜息をついたらぴよとひよこが鳴いた
昔ほげさんが帰国された時にふがふが何を言っているかよくわからなかったので、ふーと溜息をついたらぴよとひよこが鳴いた

だいぶ文字数減りましたね。
これで+で結合したときと変わらない見やすさだと思いませんか?

ただ、もっと使いやすくできます。拡張版がこちら。

appendチェーンを捨てた実装 vol.2

Test.java
======================
public class Test {

	public static void main(String[] args) {
		String str1 = "ほげ";
		int int1 = 1;
		String str2 = null;
		boolean boo1 = false;

		System.out.println(build(str1, int1, str2, boo1));
	}

	private static String build(Object... args) {
		StringBuilder sb = new StringBuilder();
		if (args != null) {
			for (int i = 0; i < args.length; i++) {
				sb.append(args[i] == null ? "" : args[i]);
			}
		}
		return sb.toString();
	}
}

[結果]
ほげ1false

引数になんでも受けられるようにしました。
nullの時にブランクにするのかnullの時のふるまい方は自由ですね。

ただ、これで文字列結合でシステムエラーになることはなくなりました。

appendチェーンを捨てた実装 おまけ

文字列結合はbuildのようなメソッドがあれば十分ですが、
区切り文字を入れたもの用のを作ります。

Test.java
======================
import java.util.StringJoiner;

public class Test {

	public static void main(String[] args) {
		String str1 = "ほげ";
		int int1 = 1;
		String str2 = null;
		boolean boo1 = false;

		System.out.println(build(str1, int1, str2, boo1));
		System.out.println("---------------");
		System.out.println(join("\r\n", str1, int1, str2, boo1));
	}

	// 以下をUtilクラスに作って放り込む(だからpublicにしてます)

	/** 文字列結合 */
	public static String build(Object... args) {
		StringBuilder sb = new StringBuilder();
		if (args != null) {
			for (int i = 0; i < args.length; i++) {
				sb.append(nullToBlank(args[i]));
			}
		}
		return sb.toString();
	}

	/** 文字列結合(区切りあり) */
	public static String join(CharSequence delimiter, Object... args) {
		StringJoiner sj = new StringJoiner(delimiter);

		if (args != null) {
			for (int i = 0; i < args.length; i++) {
				sj.add(nullToBlank(args[i]));
			}
		}
		return sj.toString();
	}

	/** nullをブランクに置き換える */
	public static String nullToBlank(Object value) {
		return value == null ? "" : value.toString();
	}
}

[結果]
ほげ1false
---------------
ほげ
1

false

コメントに記載の通り、最終的にUtilクラスにでも入れておけば
使う側はappendを全く意識することなく、見やすい文字列を結合ができます。

最後に

buildとjoinを作成しました。

これを使えば、ソース全体の見通しも大分付きやすくなると思いませんか?

正しく動くプログラムを前提としたときに、速度は何より大事ですが、可読性も次点くらいで重要な要素です。

後からソースを見られたときに、舌打ちされるようなソースはだけは書きたくないですね。

ご意見、ご感想等ございましたら、↓よりコメントお願いします。励みになります。

以上です。今後ともよろしくお願いします。

-Java

Copyright© 婿入りエンジニア、ブログ書く , 2019 All Rights Reserved.