解放 - java 長い




Javaの文字列は本当に不変ですか? (10)

@ haraldKの回答に追加するには、これはセキュリティハックで、アプリに深刻な影響を与える可能性があります。

まず、文字列プールに格納されている定数文字列を変更します。 文字列が文字列として宣言されたときString s = "Hello World"; それは、潜在的な再利用のための特別なオブジェクトプールへの場所です。 問題は、コンパイル時にコンパイラがコンパイル時に参照を配置し、ユーザが実行時にこのプールに格納された文字列を変更すると、コード内のすべての参照が変更されたバージョンを指すことです。 これは、次のバグにつながります。

System.out.println("Hello World"); 

印刷されます:

Hello Java!

私がこのような危険な文字列に重い計算を実装しているときに私が経験した別の問題がありました。 計算中に1000000回のうち1回のように起こったバグがあり、その結果が非決定的になりました。 私はJITをオフにして問題を見つけることができました - 私はいつもJITをオフにして同じ結果を得ていました。 私の推測では、その理由はJIT最適化契約のいくつかを破ったこのストリングセキュリティのハッキングだということです。

我々はStringがJavaでは不変であることを知っていますが、次のコードをチェックしてください:

String s1 = "Hello World";  
String s2 = "Hello World";  
String s3 = s1.substring(6);  
System.out.println(s1); // Hello World  
System.out.println(s2); // Hello World  
System.out.println(s3); // World  

Field field = String.class.getDeclaredField("value");  
field.setAccessible(true);  
char[] value = (char[])field.get(s1);  
value[6] = 'J';  
value[7] = 'a';  
value[8] = 'v';  
value[9] = 'a';  
value[10] = '!';  

System.out.println(s1); // Hello Java!  
System.out.println(s2); // Hello Java!  
System.out.println(s3); // World  

なぜこのプログラムはこのように動作しますか? そして、なぜs1s2値は変わるのですか? s3はありません。


Javaでは、2つの文字列プリミティブ変数が同じリテラルに初期化されている場合、両方の変数に同じ参照が割り当てられます。

String Test1="Hello World";
String Test2="Hello World";
System.out.println(test1==test2); // true

これが比較結果を真に戻す理由です。 3番目の文字列は、同じ文字列を指す代わりに新しい文字列を作成するsubstring()を使用して作成されます。

リフレクションを使用して文字列にアクセスすると、実際のポインタが得られます。

Field field = String.class.getDeclaredField("value");
field.setAccessible(true);

したがって、これを変更すると、ポインタを保持する文字列が変更されますが、 substring()ためにs3が新しい文字列で作成されるため、変更されません。


Stringは不変ですが、リフレクションによってStringクラスを変更することができます。 Stringクラスをリアルタイムで変更可能なものとして再定義しました。 必要に応じてメソッドをパブリック、プライベート、静的に再定義することができます。


[免責事項]これは、私が「家庭でこれをやってはいけない」答えが正当だと感じるにつれて、

sinは行field.setAccessible(true); プライベートフィールドへのアクセスを許可することによってパブリックAPIに違反すると言われています。 これは、セキュリティマネージャを設定することでロックダウンできる巨大なセキュリティホールです。

問題の現象は、リフレクションを介してアクセス修飾子に違反するためにその危険なコード行を使用していないときには決して見ることのできない実装の詳細です。 明らかに2つの(通常は)不変の文字列は、同じchar配列を共有できます。 部分文字列が同じ配列を共有するかどうかは、それができるかどうか、および開発者がそれを共有するかどうかによって決まります。 通常、これらは目に見えないインプリメンテーションの詳細です。アクセス修飾子をそのコード行で先頭から取り出さない限り、知る必要はありません。

リフレクションを使用してアクセス修飾子に違反することなく経験できないような詳細に頼ることは、単に良い考えではありません。 そのクラスの所有者は通常の公開APIしかサポートしておらず、将来的に実装の変更を自由に行うことができます。

コードの行は、あなたが銃を持っているときにあなたがそのような危険なことをすることを強要したとき、本当に非常に有用であると言っています。 そのバックドアを使用することは、通常、あなたが罪を犯す必要のないより良い図書館コードにアップグレードする必要のあるコード臭です。 その危険なコード行のもう一つの一般的な使い方は、 "ブードーフレームワーク"(orm、注入コンテナなど)を書くことです。 多くの人々はそのような枠組み(それらのためのものとそうでないものの両方)について宗教的になるので、私は大多数のプログラマーがそこに行かなくてもいいと言って火炎戦争を招いてはいけません。


Stringは不変*ですが、これはpublic APIを使用して変更できないことを意味します。

あなたがここでやっていることは、リフレクションを使って通常のAPIを迂回していることです。 同様に、列挙型の値を変更したり、Integer autoboxingなどで使用されるルックアップテーブルを変更したりすることができます。

今、 s1s2が値を変更する理由は、両方とも同じinterned文字列を参照しているためです。 コンパイラはこれを行います(他の回答でも言及されています)。

私がそれがvalue配列を共有すると思ったので(なぜなら、 以前のバージョンのJavaで 、Java 7u6の前に行った)、 s3が実際には私に少し驚きでした 。 しかし、 Stringのソースコードを見ると、実際に部分文字列のvalue文字配列が( Arrays.copyOfRange(..)を使用してArrays.copyOfRange(..)コピーされていることがArrays.copyOfRange(..) 。 これが変わらない理由です。

SecurityManagerをインストールすると、悪質なコードがそのようなことをするのを防ぐことができます。 しかし、いくつかのライブラリは、これらの種類のリフレクショントリック(通常はORMツール、AOPライブラリなど)を使用することに依存していることに注意してください。

*)私は当初、 Stringは本当に不変ではなく、単に「効果的な不変」であると書いていました。 これはStringの現在の実装で誤解を招く可能性がありvalue配列は本当にprivate finalます。 しかし、Javaで配列を不変として宣言する方法がないことに注意する価値はあるので、適切なアクセス修飾子を使用していても、そのクラスの外部に公開しないよう注意しなければなりません。

このトピックは圧倒的に人気があるようですが、ここにいくつか示唆されている追加の読書があります: Heinz KabutzのReflection Madnessの話題 、JavaZone 2009にはOPの問題の多くが収録されています。

それはなぜこれが役に立つのかをカバーしています。 そして、なぜ、ほとんどの時間、あなたはそれを避けるべきですか? :-)


どのJavaのバージョンを使用していますか? Java 1.7.0_06以降、Oracleでは文字列、特に部分文字列の内部表現が変更されています。

Oracle Tunes Javaの内部文字列表現からの引用:

新しいパラダイムでは、文字列オフセットとカウントフィールドが削除されているため、部分文字列は基になるchar []値を共有しなくなりました。

この変更で、それは反射(???)なしで起こるかもしれません。


リフレクションを使用して、文字列オブジェクトの「実装の詳細」にアクセスしています。 不変性は、オブジェクトのパブリックインターフェイスの機能です。


可視性修飾子と最終的な(すなわち不変性)は、Javaの悪質なコードに対する測定値ではありません。 彼らは間違いから守り、コードをよりメンテナンス可能にするためのツールにすぎません(システムの大きなセールスポイントの1つです)。 そのため、リフレクションによってStringのバッキング文字配列のような内部実装の詳細にアクセスできます。

2番目の効果は、 s1変更するだけのように見えますが、すべてのStringが変更されることです。 Java Stringリテラルの特定のプロパティは、自動的にインターンされる、つまりキャッシュされます。 同じ値を持つ2つの文字列リテラルは、実際には同じオブジェクトになります。 newを使ってStringを作成すると、自動的に内部化されず、この効果は表示されません。

最近まで#substring (Java 7u6)も同様の方法で動作していました。これは、質問の元のバージョンの動作を説明していました。 新しいバッキング文字配列は作成されませんでしたが、元のStringから再利用されました。 オフセットと長さを使用してその配列の一部だけを表示する新しいStringオブジェクトを作成しました。 これは一般的に文字列として機能しますが、それを回避しない限りは変更できません。 #substringこのプロパティは、元のString全体が、そこから生成された短い部分文字列がまだ存在するときにガベージコレクションできないことも意味しました。

現在のJavaと質問の現在のバージョンでは、 #substringな動作はありません。


文字列の不変性は、インターフェイスの観点からです。 リフレクションを使用してインタフェースをバイパスし、Stringインスタンスの内部を直接変更しています。

s1s2は、両方とも同じ「intern」Stringインスタンスに割り当てられているため、両方とも変更されます。 文字列の平等とインターンに関するこの記事からその部分についてもう少し詳しく知ることができます。 あなたのサンプルコードでは、 s1 == s2true返すことに気付かれるかもしれません!


文字列は、JVMヒープメモリの永久領域に作成されます。 そう、本当に不変で、作成後に変更することはできません。 JVMには3種類のヒープメモリーがあるので、1.若い世代2.古い世代3.永続的な世代。

オブジェクトが作成されると、若い世代ヒープ領域とPermGen領域がStringプーリング用に予約されます。

より多くの情報を得ることができますガベージコレクションはJavaでどのように動作しますか?





immutability