String
ここで理解すべき主なポイントは、 Java オブジェクトとその内容 (プライベートフィールドchar[]
の下) の違いだと思います。は基本的に配列のラッパーであり、それをカプセル化して変更できないようにするため、不変のままにすることができます。また、クラスは、この配列のどの部分が実際に使用されるかを記憶しています (以下を参照)。これはすべて、同じ を指す2 つの異なるオブジェクト (非常に軽量) を持つことができることを意味します。value
String
char[]
String
String
String
char[]
hashCode()
それぞれのフィールドString
とhashCode()
内部フィールド (文字列と区別するためにテキストchar[] value
と呼びます) と一緒に、いくつかの例を示します。最後に、テスト クラスの定数プールと共に出力を表示します。クラス定数プールと文字列リテラル プールを混同しないでください。それらはまったく同じではありません。定数プールの javap の出力についても参照してください。javap -c -verbose
前提条件
テストの目的で、String
カプセル化を破るユーティリティ メソッドを作成しました。
private int showInternalCharArrayHashCode(String s) {
final Field value = String.class.getDeclaredField("value");
value.setAccessible(true);
return value.get(s).hashCode();
}
が出力され、この特定のテキストが同じテキストを指しているかどうかhashCode()
をchar[] value
効果的に理解するのに役立ちます。String
char[]
クラス内の 2 つの文字列リテラル
最も単純な例から始めましょう。
Java コード
String one = "abc";
String two = "abc";
ところで、単純に と書く"ab" + "c"
と、Java コンパイラはコンパイル時に連結を実行し、生成されるコードはまったく同じになります。これは、コンパイル時にすべての文字列がわかっている場合にのみ機能します。
クラス定数プール
各クラスには独自の定数プールがあります。これは、ソース コードで複数回発生した場合に再利用できる定数値のリストです。一般的な文字列、数字、メソッド名などが含まれます。
上記の例の定数プールの内容は次のとおりです。
const #2 = String #38; // abc
//...
const #38 = Asciz abc;
注意すべき重要なことは、文字列が指すString
定数オブジェクト ( #2
) と Unicode でエンコードされたテキスト"abc"
( ) の違いです。#38
バイトコード
これが生成されたバイトコードです。one
と参照の両方に、文字列を指すtwo
同じ定数が割り当てられていることに注意してください。#2
"abc"
ldc #2; //String abc
astore_1 //one
ldc #2; //String abc
astore_2 //two
出力
各例について、次の値を出力しています。
System.out.println(showInternalCharArrayHashCode(one));
System.out.println(showInternalCharArrayHashCode(two));
System.out.println(System.identityHashCode(one));
System.out.println(System.identityHashCode(two));
両方のペアが等しいことは当然です。
23583040
23583040
8918249
8918249
これは、両方のオブジェクトが同じchar[]
(下の同じテキスト)を指しているだけでなく、equals()
テストに合格することを意味します。しかし、さらにone
、two
まったく同じリファレンスです! そうone == two
です。明らかに、と が同じオブジェクトone
をtwo
指している場合、one.value
とtwo.value
は等しくなければなりません。
リテラルとnew String()
Java コード
これで、私たち全員が待ち望んでいた例ができました。1 つの文字列リテラルとString
、同じリテラルを使用した 1 つの新しい例です。これはどのように機能しますか?
String one = "abc";
String two = new String("abc");
"abc"
ソースコードで定数が2回使用されているという事実は、いくつかのヒントを与えるはずです...
クラス定数プール
同上。
バイトコード
ldc #2; //String abc
astore_1 //one
new #3; //class java/lang/String
dup
ldc #2; //String abc
invokespecial #4; //Method java/lang/String."<init>":(Ljava/lang/String;)V
astore_2 //two
よく見る!最初のオブジェクトは上記と同じ方法で作成されますが、驚くことではありません。定数プールから既に作成されたString
( ) への定数参照を受け取るだけです。#2
ただし、2 番目のオブジェクトは通常のコンストラクター呼び出しによって作成されます。しかし!最初のString
ものは引数として渡されます。これは、次のように逆コンパイルできます。
String two = new String(one);
出力
出力は少し驚くべきものです。オブジェクトへの参照を表す 2 番目のペアString
は理解できます。2 つのString
オブジェクトを作成しました。1 つは定数プールに作成され、もう 1 つは手動で作成されましたtwo
。しかし、いったいなぜ、最初のペアが両方のString
オブジェクトが同じchar[] value
配列を指していることを示唆しているのでしょうか?!
41771
41771
8388097
16585653
String(String)
コンストラクターがどのように機能するかを見ると明らかになります(ここでは大幅に簡略化されています)。
public String(String original) {
this.offset = original.offset;
this.count = original.count;
this.value = original.value;
}
見る?String
既存のオブジェクトに基づいて新しいオブジェクトを作成する場合、再利用 char[] value
されます。String
s は不変であるため、決して変更されないことがわかっているデータ構造をコピーする必要はありません。
これが問題の手がかりだと思います.2つのString
オブジェクトがある場合でも、それらは同じコンテンツを指している可能性があります. ご覧のとおり、String
オブジェクト自体は非常に小さいです。
ランタイムの変更とintern()
Java コード
最初は 2 つの異なる文字列を使用していましたが、いくつかの変更を加えた後はすべて同じになったとします。
String one = "abc";
String two = "?abc".substring(1); //also two = "abc"
Javaコンパイラ(少なくとも私のもの)は、コンパイル時にそのような操作を実行するほど賢くありません。見てください:
クラス定数プール
突然、2 つの異なる定数テキストを指す 2 つの定数文字列が作成されました。
const #2 = String #44; // abc
const #3 = String #45; // ?abc
const #44 = Asciz abc;
const #45 = Asciz ?abc;
バイトコード
ldc #2; //String abc
astore_1 //one
ldc #3; //String ?abc
iconst_1
invokevirtual #4; //Method String.substring:(I)Ljava/lang/String;
astore_2 //two
拳の弦はいつものように作られています。2 つ目は、最初に定数"?abc"
文字列をロードしてから呼び出すsubstring(1)
ことによって作成されます。
出力
ここで当然のことですがchar[]
、メモリ内の 2 つの異なるテキストを指す 2 つの異なる文字列があります。
27379847
7615385
8388097
16585653
まあ、テキストは実際には違いはありませんが、equals()
メソッドはまだ生成されtrue
ます. 同じテキストの不要なコピーが 2 つあります。
ここで、2 つの演習を実行する必要があります。まず、実行してみてください:
two = two.intern();
ハッシュコードを印刷する前。one
と の両方が同じテキストをtwo
指すだけでなく、それらは同じ参照です!
11108810
11108810
15184449
15184449
これは、one.equals(two)
とone == two
テストの両方がパスすることを意味します。"abc"
また、テキストはメモリに 1 回しか表示されないため、メモリを節約できます (2 番目のコピーはガベージ コレクトされます)。
2 番目の演習は少し異なります。これを確認してください。
String one = "abc";
String two = "abc".substring(1);
明らかにone
とtwo
は 2 つの異なるオブジェクトであり、2 つの異なるテキストを指しています。しかし、なぜ出力は両方が同じ配列を指していることを示唆しているのchar[]
でしょうか?!?
23583040
23583040
11108810
8918249
答えはお任せします。substring()
どのように機能するか、そのようなアプローチの利点は何か、そしていつ大きな問題につながる可能性があるかを教えてくれます。