リフレクションと組み合わされた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"
マリオはどうなりましたか?
マリオはどうなりましたか?
基本的に変更しました。はい、リフレクションを使用すると、文字列の不変性に違反する可能性があります...文字列のインターンにより、「マリオ」の使用(コンパイル時に解決されるはずの大きな文字列定数式以外)が終了することを意味しますプログラムの残りの部分では「Luigi」になります。
このようなことが、リフレクションにセキュリティ権限が必要な理由です...
str + " " + "Mario"
の左結合性により、式+
はnotを実行し、コンパイル時の連結を実行することに注意してください。これは事実上(str + " ") + "Mario"
であるため、Luigi Luigi
が表示されたままです。コードを次のように変更した場合:
System.out.println(str + (" " + "Mario"));
...その後、コンパイラはLuigi Mario
を別の文字列に" Mario"
にインターンしているので、"Mario"
が表示されます。
ルイージに設定されました。 Javaは不変です。したがって、コンパイラは"Mario"
のすべての言及を同じString定数プール項目への参照として解釈できます(大まかに言うと、 "メモリの場所")。その項目を変更するには、コード内のすべての"Mario"
が"Luigi"
を記述したかのようになります。
文字列リテラルは文字列プールに格納され、それらの標準値が使用されます。どちらも "Mario"
リテラルは、同じ値を持つ単なる文字列ではなく、同じオブジェクトです。それらの1つを(リフレクションを使用して)操作すると、同じオブジェクトへの2つの参照であるため、「両方」が変更されます。
String constant poolString
のMario
をLuigi
は複数のString
sによって参照されたため、すべての参照literalMario
はLuigi
になりました。
_Field stringValue = String.class.getDeclaredField("value");
_
クラスvalue
から_char[]
_という名前のString
フィールドを取得しました
_stringValue.setAccessible(true);
_
アクセス可能にします。
_stringValue.set(original, "Luigi".toCharArray());
_
original
String
フィールドをLuigi
に変更しました。しかし、元はMario
the String
literalであり、リテラルは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
プールのマリオを変更しました。リテラルの代わりにString
Object
(つまりnew String("Mario")
)を作成すると、2つの異なるMario
sがあるため、この動作に直面することはありません。
他の回答は、何が起こっているかを適切に説明しています。 セキュリティマネージャ がインストールされていない場合にのみ機能するという点を付け加えたかっただけです。デフォルトでコマンドラインからコードを実行するときは存在せず、次のようなことができます。ただし、本番環境のアプリケーションサーバーやブラウザのアプレットサンドボックスなど、信頼できるコードと信頼できないコードが混在する環境では、通常セキュリティマネージャーが存在するため、これらの種類のシェナンガンは許可されません。これは、見た目ほどひどいセキュリティホールではありません。
別の関連する点: 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()
を適切に使用した文字列。