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
このプログラムはなぜこのように動作しますか? s1
とs2
の値が変更されても、s3
の値が変更されないのはなぜですか?
String
name__は不変*ですが、これはあなたがその公開APIを使って変更できないことを意味するだけです。
ここでやっていることはリフレクションを使って通常のAPIを迂回することです。同様に、enumの値を変更したり、Integerオートボクシングなどで使用されているルックアップテーブルを変更したりできます。
さて、s1
とs2
が値を変えるのは、どちらも同じ内部文字列を参照しているからです。コンパイラはこれを行います(他の回答で述べたとおり)。
s3
がしないしないという理由は、実際には少し驚くべきことでした。value
name__配列( を共有すると思いました) Javaの以前のバージョン 、Java 7u6より前)ただし、String
name__のソースコードを見ると、サブストリングのvalue
name__文字配列が実際にコピーされていることがわかります(Arrays.copyOfRange(..)
を使用)。これが変わらない理由です。
悪意のあるコードがそのようなことをするのを防ぐためにSecurityManager
name__をインストールすることができます。しかし、いくつかのライブラリはこれらの種類のリフレクショントリック(通常はORMツール、AOPライブラリなど)の使用に依存していることに注意してください。
*)私は最初にString
name__sは本当に不変ではなく、単に "有効不変"であると書いた。 String
name__配列が実際にprivate final
とマークされている現在のvalue
name__の実装では、これは誤解を招く可能性があります。ただし、Javaで配列を不変として宣言する方法はないため、適切なアクセス修飾子があっても、その配列をクラスの外部に公開しないように注意する必要があります。
このトピックは圧倒的に人気があるように思われるので、ここにさらに読むべき提案があります。 JavaZone 2009からのHeinz KabutzのReflection Madnessトーク 。これはOPの多くの問題と他の考察を含みます。狂気.
なぜこれが便利なのかを説明します。そして、なぜ、ほとんどの場合、あなたはそれを避けるべきです。 :-)
Javaでは、2つの文字列プリミティブ変数が同じリテラルに初期化されると、両方の変数に同じ参照が割り当てられます。
String Test1="Hello World";
String Test2="Hello World";
System.out.println(test1==test2); // true
それが、比較がtrueを返す理由です。 3番目の文字列は、同じ文字列を指すのではなく、新しい文字列を作成するsubstring()
を使って作成されます。
リフレクションを使用して文字列にアクセスすると、実際のポインタが取得されます。
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
したがって、これを変更すると、それを指すポインターを保持しているストリングが変更されますが、substring()
のためにs3
が新しいストリングで作成されても変更されません。
Stringの不変性を回避するためにリフレクションを使用しています - それは「攻撃」の一種です。
このように作成できる例はたくさんあります(たとえば Void
オブジェクトをインスタンス化することもできます )、しかしStringが「不変」ではないという意味ではありません。
GCの前の可能な限り早い時期にパスワードをメモリから消去する のように、このタイプのコードが有利に使用され、「優れたコーディング」になることがあるユースケースがあります。
セキュリティマネージャによっては、コードを実行できないことがあります。
文字列オブジェクトの「実装詳細」にアクセスするためにリフレクションを使っています。不変性は、オブジェクトのパブリックインタフェースの機能です。
可視性修飾子とfinal(すなわち不変性)は、Javaの悪意のあるコードに対する尺度ではありません。それらは、間違いから保護し、コードをより保守しやすくするためのツールです(システムの大きなセールスポイントの1つ)。リフレクションを介してString
sのバッキング文字配列のような内部実装の詳細にアクセスできるのはそのためです。
2番目の効果は、すべてのString
が、s1
だけを変更したように見える一方で、変更されることです。 Java Stringリテラルの特定の特性は、それらが自動的にインターンされる、つまりキャッシュされることです。同じ値を持つ2つの文字列リテラルは、実際には同じオブジェクトになります。 new
を使用して文字列を作成しても、自動的に埋められることはなく、この効果は見られません。
#substring
は最近まで(Java 7u6)同様の方法で働いていました。それはあなたの質問のオリジナル版の振る舞いを説明したでしょう。新しいバッキング文字配列を作成しませんでしたが、元の文字列からのものを再利用しました。その配列の一部だけを表示するためにオフセットと長さを使用した新しいStringオブジェクトを作成しただけです。これを回避しない限り、これは一般的に文字列は不変であるとして機能します。 #substring
のこのプロパティは、それから作成されたより短い部分文字列がまだ存在する場合、元の文字列全体をガベージコレクションできないことを意味します。
現在のJavaおよびあなたの現在のバージョンの質問の時点で、#substring
の奇妙な振る舞いはありません。
文字列の不変性はインタフェースの観点からです。リフレクションを使用してインタフェースをバイパスし、Stringインスタンスの内部を直接変更しています。
s1
とs2
は、どちらも同じ "intern" Stringインスタンスに割り当てられているため、どちらも変更されています。その部分についてもう少し詳しく知ることができます この記事 文字列の等価性と内部化について。サンプルコードでs1 == s2
がtrue
を返すことを知って驚かれるかもしれません。
どのバージョンのJavaを使用していますか? Java 1.7.0_06から、OracleはStringの内部表現、特に部分文字列を変更しました。
Oracle Tunes Javaの内部文字列表現 からの引用
新しいパラダイムでは、Stringのoffsetフィールドとcountフィールドが削除されたため、サブストリングは基礎となるchar []値を共有しなくなりました。
この変更により、それは反射なしに起こるかもしれません(???)。
ここには2つの質問があります。
1を指す:ROMを除いて、あなたのコンピュータには不変メモリはありません。今日でさえROMでも書き込み可能です。自分のメモリアドレスに書き込めるコードは、どこかに(カーネル環境でもネイティブコードでも管理対象環境を回避するために)常に存在します。ですから、「現実」では、そうではありません絶対に不変ではありません。
要点2:これは、substringがおそらく新しい文字列インスタンスを割り当てているためで、配列がコピーされている可能性があります。コピーをしないような方法で部分文字列を実装することは可能ですが、それはそれが意味するものではありません。関係するトレードオフがあります。
たとえば、reallyLargeString.substring(reallyLargeString.length - 2)
への参照を保持すると、大量のメモリが存続するようになるのでしょうか。
これは部分文字列の実装方法によって異なります。ディープコピーを実行すると、使用するメモリが少なくなりますが、実行速度は少し遅くなります。浅いコピーはより多くのメモリを存続させておくでしょう、しかしそれはより速くなります。ディープコピーを使用すると、2つの別々のヒープ割り当てとは対照的に、文字列オブジェクトとそのバッファを1つのブロックに割り当てることができるため、ヒープの断片化を減らすこともできます。
いずれにせよ、JVMはサブストリング呼び出しにディープコピーを使用することを選択したように見えます。
@ haraldKの答えに加えて - これはアプリに深刻な影響を与える可能性があるセキュリティハックです。
まず最初に、文字列プールに格納されている定数文字列を変更します。 stringがString s = "Hello World";
として宣言されているとき、それはさらなる潜在的な再利用のために特別なオブジェクトプールに置かれています。問題は、コンパイラがコンパイル時に変更されたバージョンへの参照を配置し、実行時にユーザーがこのプールに格納されている文字列を変更すると、コード内のすべての参照が変更されたバージョンを指すことです。これは次のようなバグになります。
System.out.println("Hello World");
印刷します。
Hello Java!
そのような危険な文字列に対して重い計算を実行していたときに私が経験した別の問題がありました。計算中に1000000回中1回の割合で発生するバグがあり、結果は決定的ではありませんでした。 JITをオフにすることで問題を見つけることができました - JITをオフにしても同じ結果が得られていました。その理由は、この文字列セキュリティハックがJIT最適化規約の一部を破ったためだと私は思います。
プールの概念によると、同じ値を含むすべてのString変数は同じメモリアドレスを指します。したがって、両方とも「Hello World」の同じ値を含むs1とs2は、同じメモリ位置を指します(たとえばM1)。
一方、s3には「World」が含まれているため、異なるメモリ割り当てを指すことになります(たとえばM2)。
だから今起こっているのはS1の値が(char []値を使って)変更されているということです。そのため、s1とs2の両方が指すメモリ位置M1の値が変更されています。
したがって、結果として、メモリ位置M1が修正され、それによってs1およびs2の値が変化する。
しかし、位置M2の値は変更されないままであり、したがってs3は同じ元の値を含む。
S3が実際には変更されないのは、Javaでは、サブストリングを実行するときにサブストリングの値文字配列が内部的にコピーされるためです(Arrays.copyOfRange()を使用)。
s1とs2は同じです。Javaでは、どちらも同じ内部文字列を参照するためです。これはJavaの仕様によるものです。
Stringは不変ですが、リフレクションを介してStringクラスを変更できます。 Stringクラスをリアルタイムで変更可能として再定義したところです。必要に応じて、メソッドをパブリック、プライベート、または静的に再定義することができます。
[免責事項これは故意に意見を述べた答えのスタイルであり、私はもっと "家庭ではこれをしないでください"という答えが保証されていると感じています。
罪はfield.setAccessible(true);
という行で、プライベートフィールドへのアクセスを許可することでパブリックAPIを侵害すると言っています。セキュリティマネージャを設定することでロックできる巨大なセキュリティホールがあります。
問題の現象はリフレクションを介してアクセス修飾子に違反するためにその危険なコード行を使用していないときには決して見られない実装の詳細です。明らかに2つの(通常)不変の文字列が同じchar配列を共有できます。サブストリングが同じ配列を共有するかどうかは、それが可能かどうか、および開発者がそれを共有すると考えたかどうかによって異なります。通常、これらは目に見えない実装の詳細であり、そのコード行でアクセス修飾子を頭から撮影しない限り、知る必要はないはずです。
リフレクションを使用してアクセス修飾子に違反することなく経験することができないような詳細に頼るのは、単に良い考えではありません。そのクラスの所有者は通常のパブリックAPIのみをサポートしており、将来的に実装を変更することができます。
あなたが銃を持っているときにコードの行が本当にあなたにそのような危険なことをすることを強いるあなたの頭を抱かせたとき、コードの行が本当に非常に役に立つとすべて言った。このバックドアを使用することは通常あなたが罪を犯す必要がないところでより良いライブラリコードにアップグレードする必要があることをコード臭いです。この危険なコード行のもう1つの一般的な用途は、 "voodooフレームワーク"(orm、injection containerなど)を書くことです。多くの人がそのようなフレームワークについて(彼らのためにも反対にも)信仰を持っているので、私はプログラマーの大多数以外は何もそこに行く必要はないと言って炎上戦争を招くことを避けます。
文字列はJVMヒープメモリの永続領域に作成されます。だから、はい、それは本当に不変であり、作成後に変更することはできません。 JVMには、3種類のヒープメモリがあるためです。1.若い世代2.古い世代3.永続的な世代。
オブジェクトが作成されると、それは若い世代のヒープ領域および文字列プール用に予約されているPermGen領域に入ります。
文字列は本質的に不変です。文字列オブジェクトを変更する方法はないためです。それが彼らが StringBuilder と StringBuffer クラスを導入した理由です