73

The SCJP Tip Line の著者である Corey McGlone による javaranch サイトの記事を読んだ後、私はすべて混乱しています。名前付き文字列、文字通り、および Kathy Sierra (javaranch の共同創設者) と Bert Bates による SCJP Java 6 プログラマー ガイド。

Corey 氏と Kathy Sierra 氏が String Literal Pool について引用したことを引用しようと思います。

1. Corey McGlone氏によると:

  • 文字列リテラル プールは、文字列オブジェクトを指す参照のコレクションです。

  • String s = "Hello"; (「Hello」という名前のオブジェクトがヒープ上にないと仮定します)、"Hello"ヒープ上に文字列オブジェクトを作成し、このオブジェクトへの参照を文字列リテラル プール (定数テーブル) に配置します。

  • String a = new String("Bye");(「Bye」という名前のオブジェクトがヒープ上にないと仮定すると、newオペレーターは JVM にヒープ上にオブジェクトを作成するよう義務付けます。

この記事では、文字列を作成するための演算子の説明 "new"とその参照が少しわかりにくいので、記事自体のコードと説明を以下にそのまま掲載します。

public class ImmutableStrings
{
    public static void main(String[] args)
    {
        String one = "someString";
        String two = new String("someString");

        System.out.println(one.equals(two));
        System.out.println(one == two);
    }
}

この場合、キーワードが原因で実際​​にはわずかに異なる動作になります"new." 。このような場合、2 つの文字列リテラルへの参照は引き続き定数テーブル (文字列リテラル プール) に入れられますが、キーワードに到達すると、"new,"JVM は、定数テーブルのオブジェクトを使用するのではなく、実行時に新しい String オブジェクトを作成する必要があります。

ここにそれを説明する図があります..

ここに画像の説明を入力

つまり、 String Literal Pool もこの Object への参照を持っているということですか?

Corey McGlone の記事へのリンクは次のとおりです。

http://www.javaranch.com/journal/200409/Journal200409.jsp#a1

2. SCJP book の Kathy Sierra と Bert Bates によると:

  • Java のメモリ効率を高めるために、JVM は「文字列定数プール」と呼ばれるメモリの特別な領域を確保します。コンパイラが文字列リテラルを検出すると、プールをチェックして、同一の文字列が既に存在するかどうかを確認します。そうでない場合は、新しい文字列リテラル オブジェクトを作成します。

  • String s = "abc"; // 1 つの String オブジェクトと 1 つの参照変数を作成します....

    それは問題ありませんが、次のステートメントに混乱しました。

  • String s = new String("abc")// 2 つのオブジェクトと 1 つの参照変数を作成します。

    それは本の中で....通常の(非プール)メモリ内の新しい文字列オブジェクトであり、「s」はそれを参照すると述べています...追加のリテラル「abc」がプールに配置されます。

    本の上記の行は、Corey McGlone による記事の行と衝突します。

    • Corey McGlone が言及したように String Literal Pool が String オブジェクトへの参照のコレクションである場合、リテラル オブジェクト「abc」がプールに配置されるのはなぜですか (本で言及されているように)。

    • そして、この文字列リテラル プールはどこにあるのでしょうか?

コードを書いている間はあまり問題になりませんが、メモリ管理の観点からは非常に重要であり、それが私がこのファンダをクリアしたい理由です.

4

1 に答える 1

112

Stringここで理解すべき主なポイントは、 Java オブジェクトとその内容 (プライベートフィールドchar[]の下) の違いだと思います。は基本的に配列のラッパーであり、それをカプセル化して変更できないようにするため、不変のままにすることができます。また、クラスは、この配列のどの部分が実際に使用されるかを記憶しています (以下を参照)。これはすべて、同じ を指す2 つの異なるオブジェクト (非常に軽量) を持つことができることを意味します。valueStringchar[]StringStringStringchar[]

hashCode()それぞれのフィールドStringhashCode()内部フィールド (文字列と区別するためにテキスト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効果的に理解するのに役立ちます。Stringchar[]

クラス内の 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()テストに合格することを意味します。しかし、さらにonetwoまったく同じリファレンスです! そうone == twoです。明らかに、と が同じオブジェクトonetwo指している場合、one.valuetwo.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されます。Strings は不変であるため、決して変更されないことがわかっているデータ構造をコピーする必要はありません。

これが問題の手がかりだと思います.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);

明らかにonetwoは 2 つの異なるオブジェクトであり、2 つの異なるテキストを指しています。しかし、なぜ出力は両方が同じ配列を指していることを示唆しているのchar[]でしょうか?!?

23583040
23583040
11108810
8918249

答えはお任せします。substring()どのように機能するか、そのようなアプローチの利点は何か、そしていつ大きな問題につながる可能性があるかを教えてくれます。

于 2012-07-28T12:27:32.883 に答える