平日はプロジェクトのロジック実装で忙しかったので、土曜日に少し時間があったので、本棚から分厚い英語版 Thinking In Java を取り出して、文字列オブジェクトの接合について読みました。本書を参考に翻訳し、自分の考えを加えて、記録としてこの記事を書きます。
不変の文字列オブジェクト
Java では、String オブジェクトは不変です。コードでは、String オブジェクトに対して複数のエイリアスを作成できます。ただし、これらのエイリアスはすべて同じものを指します。
たとえば、s1 と s2 は両方とも「droidyue.com」オブジェクトのエイリアスであり、エイリアスには実際のオブジェクトへの参照が保存されます。したがって、s1 = s2
次のようにコードをコピーします。
文字列 s1 = "droidyue.com";
文字列 s2 = s1;
System.out.println("s1 と s2 は同じ参照 =" + (s1 == s2));
Java で唯一のオーバーロードされた演算子
Java では、オーバーロードされる演算子は文字列の連結に関連するものだけです。 +、+=。さらに、Java デザイナーは他の演算子のオーバーロードを許可しません。
スプライシング解析
本当にパフォーマンスコストがかかるのでしょうか?
上記 2 つの点を理解すると、Sting オブジェクトは不変であるため、複数 (3 つ以上) の文字列を結合すると、必然的に冗長な中間 String オブジェクトが生成されます。
次のようにコードをコピーします。
文字列 userName = "アンディ";
文字列年齢 = "24";
文字列ジョブ = "開発者";
文字列情報 = ユーザー名 + 年齢 + 職業;
上記の情報を取得するには、userName と age を結合して一時的な String オブジェクト t1 を生成します。コンテンツは Andy24 です。次に、t1 と job を結合して、必要な最終的な情報オブジェクトを生成します。中間t1が生成され、t1が生成される その後、積極的なリサイクルが行われない場合、必然的にある程度のスペースを占有します。多数の文字列 (主にオブジェクトの toString への呼び出しで数百個を想定) を結合する場合、コストはさらに大きくなり、パフォーマンスが大幅に低下します。
コンパイラ最適化処理
上記のようなパフォーマンスのコストは本当にあるのでしょうか? 一般的に使用される文字列連結に対する特別な処理の最適化はありませんか? この最適化は、コンパイラが .java をバイトコードにコンパイルするときに実行されます。
Java プログラムを実行するには、コンパイル時と実行時の 2 つの期間を経る必要があります。コンパイル中に、Java コンパイラー (Compiler) は Java ファイルをバイトコードに変換します。実行時に、Java 仮想マシン (JVM) はコンパイル時に生成されたバイトコードを実行します。この 2 つの期間を通じて、Java はいわゆるコンパイルを 1 か所で実行し、どこでも実行できるようになりました。
コンパイル中にどのような最適化が行われたかを実験してみましょう。パフォーマンスに影響を与える可能性のあるコードを作成できます。
次のようにコードをコピーします。
パブリック クラスの連結 {
public static void main(String[] args) {
文字列 userName = "アンディ";
文字列年齢 = "24";
文字列ジョブ = "開発者";
文字列情報 = ユーザー名 + 年齢 + 職業;
System.out.println(info);
}
}
Concatenation.java をコンパイルします。 getConcatenation.class
次のようにコードをコピーします。
javacConcatenation.java
次に、javap を使用して、コンパイルされた Concatenation.class ファイルを逆コンパイルします。 javap -c 連結。 javap コマンドが見つからない場合は、javap が存在するディレクトリを環境変数に追加するか、javap へのフルパスを使用することを検討してください。
次のようにコードをコピーします。
17:22:04-androidyue~/workspace_adt/strings/src$ javap -c 連結
「Concatenation.java」からコンパイル
パブリック クラスの連結 {
public Concatenation();
コード:
0: aload_0
1: invokespecial #1 // メソッド java/lang/Object."<init>":()V
4: 戻る
public static void main(java.lang.String[]);
コード:
0: ldc #2 // 文字列アンディ
2:astore_1
3: ldc #3 // 文字列 24
5:astore_2
6: ldc #4 // 文字列開発者
8:astore_3
9: 新しい #5 // クラス java/lang/StringBuilder
12:ダップ
13: invokespecial #6 // メソッド java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // メソッド java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: ロード_2
21: invokevirtual #7 // メソッド java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: ロード_3
25: invokevirtual #7 // メソッド java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: invokevirtual #8 // メソッド java/lang/StringBuilder.toString:()Ljava/lang/String;
31: アストア4
33: getstatic #9 // フィールド java/lang/System.out:Ljava/io/PrintStream;
36:ロード4
38: invokevirtual #10 // メソッド java/io/PrintStream.println:(Ljava/lang/String;)V
41: 戻る
}
このうち、ldc、astoreなどは、アセンブリ命令と同様のJavaバイトコード命令です。次のコメントでは、説明のために Java 関連のコンテンツを使用します。 上記には多数の StringBuilder があることがわかりますが、Java コードではこれらを明示的に呼び出していません。これは、Java コンパイラーが文字列の結合に遭遇すると、StringBuilder オブジェクトと次のオブジェクトを作成します。スプライシングは実際には StringBuilder オブジェクトの append メソッドを呼び出します。そうすれば、上で心配したような問題はなくなります。
コンパイラの最適化だけですか?
コンパイラが最適化を行ってくれるので、コンパイラの最適化に依存するだけで十分でしょうか? もちろん、そうではありません。
以下では、パフォーマンスが低い、最適化されていないコードの一部を見ていきます。
次のようにコードをコピーします。
public void implicitUseStringBuilder(String[] 値) {
文字列結果 = "";
for (int i = 0; i < 値.length; i ++) {
結果 += 値[i];
}
System.out.println(結果);
}
コンパイルには javac を使用し、表示には javap を使用します
次のようにコードをコピーします。
public void implicitUseStringBuilder(java.lang.String[]);
コード:
0: ldc #11 // 文字列
2:astore_2
3: iconst_0
4:istore_3
5: iload_3
6: ロード_1
7:配列の長さ
8: if_icmpge 38
11: 新しい #5 // クラス java/lang/StringBuilder
14:ダップ
15: invokespecial #6 // メソッド java/lang/StringBuilder."<init>":()V
18: ロード_2
19: invokevirtual #7 // メソッド java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: aload_1
23: iload_3
24: ああロード
25: invokevirtual #7 // メソッド java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: invokevirtual #8 // メソッド java/lang/StringBuilder.toString:()Ljava/lang/String;
31: アストア_2
32: iinc 3、1
35: 5へ進む
38: getstatic #9 // フィールド java/lang/System.out:Ljava/io/PrintStream;
41: ロード_2
42: invokevirtual #10 // メソッド java/io/PrintStream.println:(Ljava/lang/String;)V
45: 戻る
このうち、8: if_icmpge 38 と 35: goto 5 がループを形成しています。 8: if_icmpge 38 は、JVM オペランド スタックの整数比較が以上 (i < value.length の反対の結果) の場合、行 38 (System.out) にジャンプすることを意味します。 35: goto 5 は、5 行目に直接ジャンプすることを意味します。
ただし、ここで非常に重要なことの 1 つは、StringBuilder オブジェクトの作成がループ間で行われるということです。これは、多くのループで作成される StringBuilder オブジェクトの数を意味しますが、これは明らかに良くありません。裸の低レベルコード。
少し最適化するだけでパフォーマンスが即座に向上します。
次のようにコードをコピーします。
public void明示的UseStringBuider(String[]values) {
StringBuilder の結果 = new StringBuilder();
for (int i = 0; i < 値.length; i ++) {
result.append(値[i]);
}
}
対応するまとめ情報
次のようにコードをコピーします。
public void明示的UseStringBuider(java.lang.String[]);
コード:
0: 新しい #5 // クラス java/lang/StringBuilder
3: ダップ
4: invokespecial #6 // メソッド java/lang/StringBuilder."<init>":()V
7:astore_2
8: iconst_0
9:istore_3
10: iload_3
11: ロード_1
12:配列長
13: if_icmpge 30
16: ロード_2
17: aload_1
18: iload_3
19: アーロード
20: invokevirtual #7 // メソッド java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23:ポップ
24: iinc 3、1
27: 10に進む
30: 戻る
上記からわかるように、13: if_icmpge 30 と 27: goto 10 はループループを形成し、0: new #5 はループの外にあるため、StringBuilder が複数回作成されることはありません。
一般に、ループ本体内で暗黙的または明示的に StringBuilder を作成しないように努める必要があります。そのため、コードがどのようにコンパイルされ、内部でどのように実行されるかを理解している人は、より高度なコードを作成できます。
上記の記事に誤りがある場合は、批判と修正をお願いします。