Java文字列は本当に不変ですか?


399

StringJavaでは不変であることは誰もが知っていますが、次のコードを確認してください。

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か?


394
リフレクションであらゆる種類の愚かなトリックを行うことができます。ただし、基本的には、クラスの「削除された場合の保証の無効」ステッカーを、その瞬間に破ることになります。
cHao

16
@DarshanPatelはSecurityManagerを使用してリフレクションを無効にします
Sean Patrick Floyd

39
本当に物事を台無しにしたい場合(Integer)1+(Integer)2=42は、キャッシュされたオートボクシングを台無しにすることでそれを実現できます。(Disgruntled-Bomb-Java-Edition)(thedailywtf.com/Articles/Disgruntled-Bomb-Java-Edition.aspx
Richard Tingle

15
あなたは、私がほぼ5年前に書いたこの答えで面白がってされる可能性がありますstackoverflow.com/a/1232332/27423 -それは、C#での不変のリストについてですが、それは基本的には同じことだ:どのように私は私のデータを変更することからユーザーを停止することができますか?そして答えは、できません。反射はそれを非常に簡単にします。この問題がない主流の言語の1つはJavaScriptです。これは、クロージャー内のローカル変数にアクセスできるリフレクションシステムがないため、プライベートは実際にはプライベートを意味します(キーワードがない場合でも!)
Daniel Earwicker

49
誰かが最後まで質問を読んでいますか?質問を繰り返します。「なぜこのプログラムはこのように動作するのですか?s3のs1とs2の値が変更され、変更されないのはなぜですか?」問題は、なぜs1とs2が変更されるのかではありません!質問IS:なぜs3が変更されないのですか?
Roland Pihlakas、2014年

回答:


403

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

ここでは、リフレクションを使用して、通常のAPIを回避しています。同様に、列挙型の値を変更したり、整数オートボクシングで使用されるルックアップテーブルを変更したりできます。

理由s1s2変更値は、どちらも同じインターンされた文字列を参照しているためです。コンパイラがこれを行います(他の回答で述べられているように)。

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

をインストールしてSecurityManager、そのようなことを行う悪意のあるコードを回避できます。ただし、一部のライブラリは、この種のリフレクショントリック(通常はORMツール、AOPライブラリなど)の使用に依存していることに注意してください。

*)私は最初にそれを書きました String sは実際には不変ではなく、単に「効果的な不変」であると。これはStringvalue配列が実際にマークされているの現在の実装では誤解を招く可能性がありますprivate final。ただし、Javaで配列を不変として宣言する方法はないため、適切なアクセス修飾子を使用しても、クラスの外部に配列を公開しないように注意する必要があります。


このトピックが圧倒的に人気があるように見えるため、ここにいくつかの参考文献を示します。Javaゾーン2009からのHeinz KabutzのReflection Madness講演は、OPの多くの問題とその他の反射について...まあ...狂気です。

これがなぜ有用なのかについて説明します。そして、なぜ、ほとんどの場合、あなたはそれを避けるべきです。:-)


7
実際、StringインターンはJLSの一部です(「文字列リテラルは常にクラスStringの同じインスタンスを参照します」)。しかし、私は同意しますString。クラスの実装の詳細を当てにすることは良い習慣ではありません。
haraldK 2014年

3
たぶん理由 substring既存の配列の「セクション」を使用するのではなくコピーは、巨大な文字列がsありt、そこから呼び出された小さな部分文字列を取り出し、後で放棄したsが維持tした場合、巨大な配列は存続するだろう(ガベージコレクションは行われません)。では、各文字列の値に独自の関連付けられた配列があるほうが自然なのではないでしょうか。
Jeppe Stig Nielsen

10
文字列とその部分文字列の間で配列を共有することは、参照される配列と長さへのオフセットを記憶するために、すべての Stringインスタンスが変数を運ぶ必要があることも意味しいました。これは、アプリケーションでの文字列の総数と通常の文字列と部分文字列の一般的な比率を考慮すれば無視できないオーバーヘッドです。文字列操作ごとに評価される必要があったため、速度が低下しましたため、1つの操作(安価な部分文字列)のメリットのために、すべての文字列操作のました。
Holger

2
@Holger-はい、私の理解では、最近のJVMではオフセットフィールドが削除されています。そして、それが存在していても、それほど頻繁には使用されませんでした。
Hot Licks 2014年

2
@supercat:ネイティブコードの有無に関係なく、同じJVM内の文字列とサブ文字列の実装が異なるかbyte[]、ASCII文字列の文字列があるかなどchar[]は、すべての操作で前にどの種類の文字列かを確認する必要があることを意味しますオペレーティング。これは、呼び出し元のコンテキスト情報を使用してさらに最適化するための最初のステップである、文字列を使用するメソッドへのコードのインライン化を妨げます。これは大きな影響です。
Holger

93

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);

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

変化する


これはリテラルに対してのみ機能し、コンパイル時の最適化です。
SpacePrez 2014年

2
@ Zaphod42真実ではありません。intern非リテラル文字列を手動で呼び出して、メリットを享受することもできます。
Chris Hayes

ただし、注意して使用してくださいintern。すべてをインターンしてもそれほど利益は得られず、ミックスにリフレクションを追加するときに頭を引っかくような瞬間の元になることがあります。
cHao

Test1Test1と矛盾しているtest1==test2とJavaの命名規則に従っていません。
2019

50

リフレクションを使用して、文字列の不変性を回避しています。これは「攻撃」の一種です。

このように作成できる例はたくさんあります(たとえばVoidオブジェクトをインスタンス化することもできます)が、Stringが「不変」ではないという意味ではありません。

このタイプのコードが有利に使用され、「適切なコーディング」になる可能性のあるユースケースがあります。たとえば、可能な限り早い時期に(GCの前に)メモリからパスワードクリアします

セキュリティマネージャによっては、コードを実行できない場合があります。


30

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


24

可視性修飾子と最終(つまり不変性)は、Javaの悪意のあるコードに対する測定ではありません。これらは、ミスから保護し、コードを保守しやすくするためのツールにすぎません(システムの大きなセールスポイントの1つ)。そのためString、リフレクションを介してsのバッキング文字配列などの内部実装の詳細にアクセスできます。

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

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

現在のJavaおよび現在のバージョンの質問では、の奇妙な動作はありません#substring


2
実際、可視性修飾子悪意のあるコードに対する保護として意図されています(または少なくとも意図されていました)。ただし、保護を有効にするにはSecurityManager(System.setSecurityManager())を設定する必要があります。これが実際にどれほど安全であるかは別の問題です...
sleske 2014年

2
アクセス修飾子コードを「保護する」こと目的とていないことを強調するので、賛成票を投じる価値があります。これは、Javaと.NETの両方で広く誤解されているようです。前のコメントはそれと矛盾していますが; Javaについてはあまり知りませんが、.NETでは確かにそうです。どちらの言語でも、これによりコードがハックプルーフになるとユーザーが想定するべきではありません。
トムW

final反省しても契約違反はできません。また、別の回答で述べたように、Java 7u6以降#substringは配列を共有していません。
ntoskrnl 2014年

実際の動作は、finalハインツ・Iによる「リフレクションマッドネス」の話をよると、-Oは、他のスレッドに投稿:...時間をかけて変化しているfinalJDK 1.1、1.3および1.4で、最終的なものではなく、常に1.2を使用してリフレクションを使用して変更することができ、そしてほとんどの場合 1.5と6 ...
haraldK 2014年

1
finalフィールドnativeは、シリアル化されたインスタンスのフィールドを読み取るときにシリアライゼーションフレームワークによって行われるように、およびSystem.setOut(…)最終的なSystem.out変数を変更するコードによって変更できます。後者は、アクセスオーバーライドを使用したリフレクションではstatic finalフィールドを変更できないため、最も興味深い機能です。
Holger、2014年

11

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

s1そしてs2それらは両方とも同じ「インターン」Stringインスタンスに割り当てられているので、両方に変更されています。この部分については、ストリングの等価性とインターンに関するこの記事からもう少し詳しく知ることができます。サンプルコードでs1 == s2が返されることを知って驚くかもしれませんtrue


10

どのバージョンのJavaを使用していますか?Java 1.7.0_06から、Oracleは文字列、特にサブ文字列の内部表現を変更しました。

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

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

この変更により、リフレクションなしで発生する可能性があります(???)。


2
OPが古いSun / Oracle JREを使用している場合、最後のステートメントは「Java!」と出力します。(彼が誤って投稿したように)。これは、文字列とサブ文字列の間の値配列の共有にのみ影響します。それでも、リフレクションのようなトリックなしでは値を変更することはできません。
haraldK 2014年

7

ここには本当に2つの質問があります。

  1. 文字列は本当に不変ですか?
  2. なぜs3は変更されないのですか?

ポイント1:ROMを除いて、コンピュータには不変のメモリはありません。最近では、ROMでさえも書き込み可能です。メモリアドレスに書き込むことができるコードが常に存在します(それがカーネルであろうと、管理された環境を回避するネイティブコードであろうと)。したがって、「現実」では、それらは完全に不変ではありません。

ポイント2:これは、部分文字列がおそらく新しい文字列インスタンスを割り当てているためであり、配列をコピーしている可能性があります。コピーを行わない方法で部分文字列を実装することは可能ですが、それはそうすることを意味するものではありません。これにはトレードオフが伴います。

たとえば、reallyLargeString.substring(reallyLargeString.length - 2)大量のメモリを存続させるために参照を保持する必要がありますか、それとも数バイトのみですか?

これは、部分文字列の実装方法によって異なります。深いコピーは、より少ないメモリを維持しますが、わずかに遅く実行されます。浅いコピーはより多くのメモリを維持しますが、より高速になります。ディープコピーを使用すると、2つの個別のヒープ割り当てではなく、文字列オブジェクトとそのバッファーを1つのブロックに割り当てることができるため、ヒープの断片化を減らすこともできます。

いずれにせよ、JVMが部分文字列呼び出しにディープコピーを使用することを選択したようです。


3
実際のROMは、プラスチックに収められた写真プリントと同じように不変です。パターンは、ウェーハ(またはプリント)が化学的に現像されるときに永久に設定されます。RAMチップを含む電気的に変更可能なメモリは、それをインストールする回路に追加の電気的接続を追加しないと書き込みに必要な制御信号にエネルギーを供給できない場合、「真の」ROMとして動作できます。組み込みデバイスに、工場で設定され、バックアップバッテリーによって維持されるRAMが含まれることは珍しくありません。その内容は、バッテリーが故障した場合に工場でリロードする必要があります。
スーパーキャット2014年

3
@supercat:お使いのコンピューターは、組み込みシステムではありません。:)真のハードワイヤードROMは、PCでは10年か2年は一般的ではありませんでした。最近はすべてのEEPROMとフラッシュです。基本的に、メモリを参照するユーザーから見えるすべてのアドレスは、潜在的に書き込み可能なメモリを参照します。
cHao

@cHao:多くのフラッシュチップでは、部分を書き込み保護することができます。これを元に戻すことができる場合、通常の動作に必要な電圧とは異なる電圧を加える必要があります(マザーボードには装備されていません)。私はマザーボードがその機能を使うことを期待します。さらに、私は今日のコンピューターについては定かではありませんが、歴史的に一部のコンピューターには、ブートステージ中に書き込み保護されたRAMの領域があり、リセットによってのみ保護を解除できました(ROMから実行を強制的に開始します)。
スーパーキャット2014年

2
@supercatこのトピックの要点を逃していると思います。つまり、RAMに格納されている文字列は、真に不変ではありません。
Scott Wisniewski

5

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

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

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

印刷されます:

Hello Java!

そのような危険な文字列に対して重い計算を実装しているときに経験した別の問題がありました。計算中に1000000回に1回程度発生するバグがあり、結果が不確定になりました。JITをオフにすることで問題を見つけることができました。JITをオフにしても常に同じ結果が得られました。その理由は、このStringセキュリティハックがJIT最適化契約の一部を破ったためだと思います。


これは、JITを使用しないと実行時間が長くなり、同時実行性が低下するため、スレッドセーフの問題であった可能性があります。
テッドペニングス2014年

@TedPennings私の説明から、私は詳細にあまり入りたくありませんでした。私は実際にそれをローカライズするために数日のように過ごしました。これは、2つの異なる言語で書かれた2つのテキスト間の距離を計算するシングルスレッドアルゴリズムでした。私は問題の2つの可能な修正を見つけました-1つはJITをオフにすることであり、もう1つはString.format("")内部ループの1つに文字通り何もしないことを追加することでした。それがJIT失敗の別の問題である可能性がありますが、このno-opを追加した後にこの問題が再現されなかったため、JITであったと思います。
Andrey Chaschev、2014年

私はJDK〜7u9の初期バージョンでこれを行っていたので、それである可能性があります。
Andrey Chaschev 2014年

1
@Andrey Chaschev:「私はこの問題に対して2つの可能な修正を見つけました」…3番目の修正は、String内部をハッキングしないために、思い浮かばなかったのですか?
Holger

1
@Ted Pennings:スレッドの安全性の問題とJITの問題は、ほとんど同じです。JITはfinal、オブジェクトの構築後にデータを変更すると壊れるフィールドスレッドの安全性の保証に依存するコードを生成できます。したがって、JIT課題またはMT課題として好きなように見ることができます。本当の問題は、String不変であると予想されるデータをハッキングして変更することです。
Holger

5

プーリングの概念によれば、同じ値を含むすべての文字列変数は同じメモリアドレスを指します。したがって、s1とs2はどちらも「Hello World」の同じ値を含み、同じメモリ位置を指します(たとえばM1)。

一方、s3には「ワールド」が含まれているため、別のメモリ割り当てを指します(たとえばM2)。

つまり、S1の値が(char []値を使用して)変更されているということです。したがって、s1とs2の両方が指すメモリ位置M1の値が変更されました。

したがって、結果として、メモリ位置M1が変更され、s1とs2の値が変化します。

ただし、ロケーションM2の値は変更されないままであるため、s3には同じ元の値が含まれます。


5

s3が実際に変更されない理由は、Javaでは部分文字列を実行すると、部分文字列の値文字配列が内部的にコピーされるためです(Arrays.copyOfRange()を使用)。

s1とs2は同じです。Javaではどちらも同じインターンされた文字列を参照するためです。これは、Javaの仕様によるものです。


2
この答えはあなたの前の答えにどのように何かを追加しましたか?
グレイ:

これはまったく新しい動作であり、どの仕様でも保証されていないことにも注意してください。
–PaŭloEbermann、2014

の実装はString.substring(int, int)Java 7u6 で変更されました。7u6以前は、JVMは元Stringのへのポインタとchar[]インデックスおよび長さを保持するだけでした。7u6以降、部分文字列を新しいStringThere にコピーします。賛否両論があります。
Eric Jablow、2014年

2

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


2
フィールド/メソッドの可視性を変更する場合、それらはコンパイル時にプライベートであるため、役に立ちません
ボヘミアン

1
メソッドのアクセシビリティを変更することはできますが、パブリック/プライベートステータスを変更したり、静的にしたりすることはできません。
グレイ

1

[免責事項これは、「家庭の子供ではこれを行わない」という答えの方が当然だと思うので、意図的に意見が分かれた答えのスタイルです]

罪は、field.setAccessible(true);プライベートフィールドへのアクセスを許可することにより、パブリックAPIに違反することを示す行です。これは、セキュリティマネージャーを構成することによって封じ込めることができる巨大なセキュリティホールです。

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

リフレクションを使用してアクセス修飾子に違反しないと体験できないような詳細に依存することは、単に良い考えではありません。そのクラスの所有者は、通常のパブリックAPIのみをサポートし、将来、実装を自由に変更できます。

銃を持っていて、そのような危険なことをするように強いられている場合、コード行は本当に非常に便利であるとすべて述べました。そのバックドアを使用することは通常、コードのにおいであり、罪を犯す必要がないより良いライブラリコードにアップグレードする必要があります。この危険なコード行のもう1つの一般的な使用法は、「ブードゥーフレームワーク」(orm、注入コンテナーなど)を記述することです。多くの人々がそのようなフレームワークについて(彼らのために、そして反対して)信仰を持っているので、大多数のプログラマーがそこに行く必要がないこと以外は何も言わないことで炎上戦争を招くことを避けます。


1

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

オブジェクトが作成されると、オブジェクトは、若い世代のヒープ領域と文字列プーリング用に予約されているPermGen領域に入ります。

以下に詳細を示します。詳細は、Javaでのガベージコレクションの動作方法をご覧ください 。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.