web-dev-qa-db-ja.com

これはどのようにJavaコードスニペットが機能しますか?(文字列プールとリフレクション)

リフレクションと組み合わされたJava文字列プールは、Javaで想像を絶する結果をもたらす可能性があります。

import Java.lang.reflect.Field;

class MessingWithString {
    public static void main (String[] args) {
        String str = "Mario";
        toLuigi(str);
        System.out.println(str + " " + "Mario");
    }

    public static void toLuigi(String original) {
        try {
            Field stringValue = String.class.getDeclaredField("value");
            stringValue.setAccessible(true);
            stringValue.set(original, "Luigi".toCharArray());
        } catch (Exception ex) {
            // Ignore exceptions
        }
    }
}

上記のコードは印刷されます:

"Luigi Luigi" 

マリオはどうなりましたか?

82
macrog

マリオはどうなりましたか?

基本的に変更しました。はい、リフレクションを使用すると、文字列の不変性に違反する可能性があります...文字列のインターンにより、「マリオ」の使用(コンパイル時に解決されるはずの大きな文字列定数式以外)が終了することを意味しますプログラムの残りの部分では「Luigi」になります。

このようなことが、リフレクションにセキュリティ権限が必要な理由です...

str + " " + "Mario"の左結合性により、式+notを実行し、コンパイル時の連結を実行することに注意してください。これは事実上(str + " ") + "Mario"であるため、Luigi Luigiが表示されたままです。コードを次のように変更した場合:

System.out.println(str + (" " + "Mario"));

...その後、コンパイラはLuigi Marioを別の文字列に" Mario"にインターンしているので、"Mario"が表示されます。

95
Jon Skeet

ルイージに設定されました。 Javaは不変です。したがって、コンパイラは"Mario"のすべての言及を同じString定数プール項目への参照として解釈できます(大まかに言うと、 "メモリの場所")。その項目を変更するには、コード内のすべての"Mario""Luigi"を記述したかのようになります。

24
Amadan

既存の回答をもう少し説明するために、生成されたバイトコードを見てみましょう(ここではmain()メソッドのみ)。

Byte Code

これで、その場所のコンテンツの変更は、両方のreferencesに影響します(そして、あなたが与えた他のものも)。

16
Codebender

文字列リテラルは文字列プールに格納され、それらの標準値が使用されます。どちらも "Mario"リテラルは、同じ値を持つ単なる文字列ではなく、同じオブジェクトです。それらの1つを(リフレクションを使用して)操作すると、同じオブジェクトへの2つの参照であるため、「両方」が変更されます。

9
Mureinik

String constant poolStringMarioLuigiは複数のStringsによって参照されたため、すべての参照literalMarioLuigiになりました。

_Field stringValue = String.class.getDeclaredField("value");
_

クラスvalueから_char[]_という名前のStringフィールドを取得しました

_stringValue.setAccessible(true);
_

アクセス可能にします。

_stringValue.set(original, "Luigi".toCharArray());
_

originalStringフィールドをLuigiに変更しました。しかし、元はMario the Stringliteralであり、リテラルはStringプールに属し、すべてがinterned。つまり、同じ内容を持つすべてのリテラルは同じメモリアドレスを参照します。

_String a = "Mario";//Created in String pool
String b = "Mario";//Refers to the same Mario of String pool
a == b//TRUE
//You changed 'a' to Luigi and 'b' don't know that
//'a' has been internally changed and 
//'b' still refers to the same address.
_

基本的に、すべての参照フィールドでreflectedを取得したStringプールのマリオを変更しました。リテラルの代わりにStringObject(つまりnew String("Mario"))を作成すると、2つの異なるMariosがあるため、この動作に直面することはありません。

8
CoderCroc

他の回答は、何が起こっているかを適切に説明しています。 セキュリティマネージャ がインストールされていない場合にのみ機能するという点を付け加えたかっただけです。デフォルトでコマンドラインからコードを実行するときは存在せず、次のようなことができます。ただし、本番環境のアプリケーションサーバーやブラウザのアプレットサンドボックスなど、信頼できるコードと信頼できないコードが混在する環境では、通常セキュリティマネージャーが存在するため、これらの種類のシェナンガンは許可されません。これは、見た目ほどひどいセキュリティホールではありません。

5
Pepijn Schmitz

別の関連する点: String.intern() メソッドを使用することにより、定数プールを使用して、状況によっては文字列比較のパフォーマンスを向上させることができます。

このメソッドは、String定数プールから呼び出されたStringと同じ内容のStringのインスタンスを返し、まだ存在しない場合は追加します。言い換えると、intern()を使用した後、同じ内容のすべての文字列は、互いに同じ文字列インスタンスであり、それらの内容の文字列定数であることが保証されます。つまり、等しい演算子(_==_).

これは、それ自体ではあまり有用ではない単なる例ですが、ポイントを示しています:

_class Key {
    Key(String keyComponent) {
        this.keyComponent = keyComponent.intern();
    }

    public boolean equals(Object o) {
        // String comparison using the equals operator allowed due to the
        // intern() in the constructor, which guarantees that all values
        // of keyComponent with the same content will refer to the same
        // instance of String:
        return (o instanceof Key) && (keyComponent == ((Key) o).keyComponent);
    }

    public int hashCode() {
        return keyComponent.hashCode();
    }

    boolean isSpecialCase() {
        // String comparison using equals operator valid due to use of
        // intern() in constructor, which guarantees that any keyComponent
        // with the same contents as the SPECIAL_CASE constant will
        // refer to the same instance of String:
        return keyComponent == SPECIAL_CASE;
    }

    private final String keyComponent;

    private static final String SPECIAL_CASE = "SpecialCase";
}
_

この小さなトリックはコードを設計する価値はありませんが、パフォーマンスに敏感なコードから_==_演算子を使用することで速度が少し向上することに気付く日を念頭に置く価値がありますintern()を適切に使用した文字列。

3
Pepijn Schmitz