StringBuilder#appendとStringの連結(+演算子)
StringBuilderに対して複数のStringをappendする場合、その呼び出しパターンは以下のコードに示される4種類に整理できる。
public class Test { private String s1 = "a"; private String s2 = "b"; private final String S1 = "a"; private final String S2 = "b"; private StringBuilder sb = new StringBuilder(); public void test1() { sb.append(s1 + s2); } public void test2() { sb.append(s1).append(s2); } public void test3() { sb.append(S1 + S2); } public void test4() { sb.append(S1).append(S2); } private Test() { } }
観点は以下の2つである。
- appendする複数のStringについて、それらを+演算子で連結してからappendするか、それとも各Stringを順にappendするか
- appendするString(型の変数が指す文字列)がコンパイル時に確定するか否か
で、上記のコードをコンパイルし、"javap -c"してみると、以下のような結果が得られる。(Sunのjdk1.5.0_11を使用)
Compiled from "Test.java" public class y2009.m05.d11.t001.Test extends java.lang.Object{ public void test1(); Code: 0: aload_0 1: getfield #20; //Field sb:Ljava/lang/StringBuilder; 4: new #22; //class java/lang/StringBuilder 7: dup 8: aload_0 9: getfield #24; //Field s1:Ljava/lang/String; 12: invokestatic #26; //Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String; 15: invokespecial #32; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V 18: aload_0 19: getfield #36; //Field s2:Ljava/lang/String; 22: invokevirtual #38; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 25: invokevirtual #42; //Method java/lang/StringBuilder.toString:()Ljava/lang/String; 28: invokevirtual #38; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 31: pop 32: return public void test2(); Code: 0: aload_0 1: getfield #20; //Field sb:Ljava/lang/StringBuilder; 4: aload_0 5: getfield #24; //Field s1:Ljava/lang/String; 8: invokevirtual #38; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 11: aload_0 12: getfield #36; //Field s2:Ljava/lang/String; 15: invokevirtual #38; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 18: pop 19: return public void test3(); Code: 0: aload_0 1: getfield #20; //Field sb:Ljava/lang/StringBuilder; 4: ldc #52; //String ab 6: invokevirtual #38; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 9: pop 10: return public void test4(); Code: 0: aload_0 1: getfield #20; //Field sb:Ljava/lang/StringBuilder; 4: ldc #10; //String a 6: invokevirtual #38; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 9: ldc #13; //String b 11: invokevirtual #38; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 14: pop 15: return }
test1()とtest2()、test3()とtest4()の比較をする際の観点は前者の観点、test1()とtest3()、test2()とtest4()の比較をする際の観点は後者の観点である。
そもそもこんなことを調べ始めたのは、test1()(およびtest3())のケースでは文字列連結処理の際にStringBuilderオブジェクトが余計に生成されるのではないかと考えたから。しかしよく見ると、test3()のケースではappend()メソッドに渡すものがコンパイル時に確定するため、「"a" + "b"」が「"ab"」に最適化されているのが分かる。また、test1()のケースでは、確かに文字列連結(+演算子)のためのStringBuilderがnewされている。パッと見ると、処理コストは「test1() > test2()」、「test3() < test4()」という形になりそう。
ということで、まず計ってみる。
import java.text.DecimalFormat; import java.text.NumberFormat; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; public class TestStringBuilder { private static NumberFormat nf = new DecimalFormat("0.000"); private static int loopCount1 = 10000; private static int loopCount2 = 1000; private String s1 = "a"; private String s2 = "b"; private final String S1 = "a"; private final String S2 = "b"; private StringBuilder sb = new StringBuilder(loopCount2 * 2); private long start; private long end; private String name; @BeforeClass public static void setUpBeforeClass() throws Exception { } @AfterClass public static void tearDownAfterClass() throws Exception { } @Before public void setUp() throws Exception { start = System.currentTimeMillis(); } @After public void tearDown() throws Exception { end = System.currentTimeMillis(); System.out.printf("%-28s %7s %7s %24s\n", name, loopCount1, loopCount2, nf.format((end - start) / 1000.0) + " sec"); } @Test public void test1() { name = "test1:append(v + v)"; for (int i = 0; i < loopCount1; ++i) { sb.setLength(0); for (int j = 0; j < loopCount2; ++j) { sb.append(s1 + s2); } } } @Test public void test2() { name = "test2:append(v).append(v)"; for (int i = 0; i < loopCount1; ++i) { sb.setLength(0); for (int j = 0; j < loopCount2; ++j) { sb.append(s1).append(s2); } } } @Test public void test3() { name = "test3:append(c + c)"; for (int i = 0; i < loopCount1; ++i) { sb.setLength(0); for (int j = 0; j < loopCount2; ++j) { sb.append(S1 + S2); } } } @Test public void test4() { name = "test4:append(c).append(c)"; for (int i = 0; i < loopCount1; ++i) { sb.setLength(0); for (int j = 0; j < loopCount2; ++j) { sb.append(S1).append(S2); } } } }
結果。
test1:append(v + v) 10000 1000 4.784 sec test2:append(v).append(v) 10000 1000 1.687 sec test3:append(c + c) 10000 1000 0.766 sec test4:append(c).append(c) 10000 1000 1.487 sec
まぁ、やはり予想通り。("v"は"variable"、"c"は"constant"と思ってください。)
StringBuilder#append(s1 + s2) は StringBuilder#append(s1).append(s2) とした方がよさそうと考えていたが、コンパイル時にappendする文字列が確定するかしないかをちゃんと見極めないといけないということだな。
しかし、test1()とtest2()の処理速度が約3倍も違うとは‥コンパイルオプションで最適化するように指定すると違ってきたりするのかな?‥