クイズの解答です。
このクイズの趣旨は文字列連結演算子の+がどのように働くのかという知識を問うものでした。
まずは実行してみましょう。結果は以下のとおり。
A : 80026931
B : 143935333
C : 388877
D : 2152509
単位はns(ナノ秒)ですから、見やすくすると
メソッド | ナノ秒 |
A | 80,026,931 |
B | 143,935,333 |
C | 388,877 |
D | 2,152,509 |
となりますので、C,D,A,Bの順となりますね。
なぜ速度に差が出るのか?
Java言語仕様 第3版には「15.18.1 文字列連結演算子 +」という章があります。
今回のクイズは、このあたりの仕様が基準となります。
StringはいわゆるImmutableパターンという奴で、インスタンスが作られた際に値が決められ、
以後内部の値が変わることがありません。ですから、Stringの結合などは、
その都度新しいオブジェクトが作られ、古いオブジェクトは破棄されるわけです。
文字列連結の+はコンパイルの時点でStringBuilderのappend()による連結に置き換えられます。
そして、String型の変数に入るたびにStringオブジェクトを作り、古いStringオブジェクトが破棄されます。
String型はいわばchar型の配列ですから、結構生成にコストがかかります。
ここがパフォーマンスに大きな影響を及ぼすところですね。
さて、Stringのリテラル(つまるところダブルクォートに囲まれてソース中に書かれた文字列)同士の
+演算はコンパイル時点で最適化されて、あらかじめ結合済みの文字列となる場合があります。
問題編でのかつのりさんの回答の
A,CとB,Dの違いは、
"hogepiyo"か"hoge","piyo"の違いです。
というところが正にここにあたります。"hoge" + "piyo"がコンパイル時にあらかじめ"hogepiyo"になるのですね。
変数との+演算の場合はこの手抜きができませんから、
str += "hoge";
str += "piyo";
としてしまうと
str += "hoge" + "piyo";
よりも遅くなってしまう。
C,DではStringBuilderを用いてStringインスタンスの生成を行わずに文字列結合しているから早く、
Dが2回appendを実行するのに対し、Cは最適化によって"hogepiyo"を1回appendするだけで済むのでCが最も早いわけです。
最適化は実装依存という落とし穴
Java言語仕様の15.18.1.2に「文字列連結の最適化」という項があります。
一時的なStringオブジェクトの生成と破棄を回避するため、変換と連結を単一のステップで実行すること
が実装に対して許されている。また、連続して文字列連結を行う際におけるパフォーマンスの向上を目的とし
て、クラスStringBufferや同種の手法を採用し、式の評価中に生成される一時的なStringオブジェクト数
を削減することがJavaコンパイラに対して許されている。
プリミティブ型の場合、プリミティブ型を文字列に直接変換することによって、ラッパ・オブジェクトの生
成を回避するような最適化も実装に対して許されている。
強調は私によるものです。前者の強調部分はVMがコードを実行する際の話であり、
後者はコンパイラがコードを書き出す場合の話です。
じつはこの部分は実装依存なので、使用するコンパイラと計測に用いるVMによって結果が変わることがありえるのです。
「リテラルの文字列の+演算はどうせ連結してコンパイルされるから関係ない」というのは
あるJavaコンパイラでは真だとしても、ほかのコンパイラでは違うかもしれません。
実際、Eclipse3.3で以下のコードをコンパイルすると
int i = 1;
System.out.println(i + "hoge" + "piyo");
0 iconst_1
1 istore_1 [i]
2 getstatic java.lang.System.out : java.io.PrintStream [16]
5 new java.lang.StringBuilder [22]
8 dup
9 iload_1 [i]
10 invokestatic java.lang.String.valueOf(int) : java.lang.String [24]
13 invokespecial java.lang.StringBuilder(java.lang.String) [30]
16 ldc <String "hoge"> [33]
18 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [35]
21 ldc <String "piyo"> [39]
23 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [35]
26 invokevirtual java.lang.StringBuilder.toString() : java.lang.String [41]
29 invokevirtual java.io.PrintStream.println(java.lang.String) : void [45]
32 return
ってな感じのバイトコードが出力されます。
"hogepiyo"にならずに2回に分けて連結されていますね。
しかし、すべてのJavaコンパイラがこのようにコードを吐くとは限りません。
ちゃんと"hogepiyo"と最適化するコンパイラも存在するかもしれません。
結論
というわけで、AとB、CとDの順番は入れ替わる可能性があります。
このあたりについて考察があれば満点を差し上げたところなのですが…。
takaさん、かつのりさん、8点ということでお開きにしたいと思います。
投稿日時 : 2007年11月27日 23:05