web-dev-qa-db-ja.com

JavaでStringが不変なのはなぜですか?

理由が分からなかった。私はいつも他の開発者のようにStringクラスを使用していますが、その値を変更すると、Stringの新しいインスタンスが作成されました。

JavaのStringクラスの不変性の理由は何でしょうか?

StringBufferやStringBuilderのようないくつかの代替手段があることを知っています。それは単なる好奇心です。

80
yfklon

並行性

Javaは、並行性を考慮して最初から定義されていました。しばしば言及されるように、共有された可変変数は問題があります。あるスレッドが別のスレッドの背後で別のスレッドを変更しても、そのスレッドが気付かない場合があります。

共有文字列が原因で発生したホストのマルチスレッドC++バグがあります。あるモジュールは、コード内の別のモジュールがポインターを保存し、同じままであると期待したときに変更しても安全だと考えていました。

これに対する「解決策」は、すべてのクラスがそれに渡される可変オブジェクトの防御コピーを作成することです。可変文字列の場合、これはコピーを作成するためのO(n)です。不変文字列の場合、コピーではないため、コピーを作成することはO(1)です。これは、変更できない同じオブジェクトです。

マルチスレッド環境では、不変オブジェクトは常に互いに安全に共有できます。これにより、メモリ使用量が全体的に削減され、メモリキャッシングが改善されます。

安全保障

文字列はコンストラクタの引数として何度も渡されます。ネットワーク接続とプロトコルが最も簡単に思い浮かぶ2つです。実行後の不確定な時間にこれを変更できると、セキュリティの問題が発生する可能性があります(関数は、あるマシンに接続していると考えていたが、別のマシンに転用されたが、オブジェクト内のすべてが最初のマシンに接続されているように見える...その同じ文字列)。

Javaではリフレクションを使用できます-そのためのパラメーターは文字列です。文字列を渡して、反映される別のメソッドへの途中で変更される危険性。これは非常に悪いです。

ハッシュの鍵

ハッシュテーブルは、最もよく使用されるデータ構造の1つです。多くの場合、データ構造のキーは文字列です。不変の文字列があることは、(上記のように)ハッシュテーブルが毎回ハッシュキーのコピーを作成する必要がないことを意味します。文字列が変更可能で、ハッシュテーブルがこれを行わなかった場合、何かが離れた場所でハッシュキーを変更する可能性があります。

Javaのオブジェクトが機能する方法は、すべてにハッシュキーがあることです(hashCode()メソッドを介してアクセス)。不変の文字列があることは、hashCodeをキャッシュできることを意味します。文字列がハッシュのキーとして使用される頻度を考慮すると、これによりパフォーマンスが大幅に向上します(毎回ハッシュコードを再計算する必要がなくなります)。

部分文字列

文字列を不変にすることで、データ構造を支える基本的な文字配列も不変になります。これにより、substringメソッドの特定の最適化を行うことができます(これらは行われません必要に応じて行われます)。メモリリークも)。

もしあなたがそうするなら:

String foo = "smiles";
String bar = foo.substring(1,5);

barの値は「マイル」です。ただし、foobarの両方を同じ文字配列でバックアップできるため、文字配列内の異なる開始点と終了点を使用するだけで、より多くの文字配列のインスタンス化またはコピーが削減されます。

 foo | | (0、6)
 v v 
笑顔
 ^ ^ 
バー| | (1、5)

さて、その欠点(メモリリーク)は、1kの長い文字列があり、最初と2番目の文字の部分文字列を受け取った場合、1kの長い文字配列によってもサポートされることです。文字配列全体の値を持つ元の文字列がガベージコレクションされた場合でも、この配列はメモリに残ります。

これは String from JDK 6b14 で確認できます(次のコードはGPL v2ソースからのものであり、例として使用されています)

   public String(char value[], int offset, int count) {
       if (offset < 0) {
           throw new StringIndexOutOfBoundsException(offset);
       }
       if (count < 0) {
           throw new StringIndexOutOfBoundsException(count);
       }
       // Note: offset or count might be near -1>>>1.
       if (offset > value.length - count) {
           throw new StringIndexOutOfBoundsException(offset + count);
       }
       this.offset = 0;
       this.count = count;
       this.value = Arrays.copyOfRange(value, offset, offset+count);
   }

   // Package private constructor which shares value array for speed.
   String(int offset, int count, char value[]) {
       this.value = value;
       this.offset = offset;
       this.count = count;
   }

   public String substring(int beginIndex, int endIndex) {
       if (beginIndex < 0) {
           throw new StringIndexOutOfBoundsException(beginIndex);
       }
       if (endIndex > count) {
           throw new StringIndexOutOfBoundsException(endIndex);
       }
       if (beginIndex > endIndex) {
           throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
       }
       return ((beginIndex == 0) && (endIndex == count)) ? this :
           new String(offset + beginIndex, endIndex - beginIndex, value);
   }

部分文字列が、配列のコピーを含まず、はるかに高速なパッケージレベルの文字列コンストラクターをどのように使用するかに注意してください(大きな配列を複製しない場合でも、いくつかの大きな配列を維持する可能性があります)。

上記のコードはJava 1.6用であることに注意してください。 Java 1.7.0_06で行われた文字列の内部表現の変更 に記載されているように、サブストリングコンストラクターの実装方法がJava 1.7で変更されました。上記の通り。 Javaは、多くの文字列操作を備えた言語とは見なされなかった可能性が高いため、部分文字列のパフォーマンス向上は良いことでした。現在、収集されない文字列に格納された巨大なXMLドキュメントでは、これは問題になります...したがって、Stringへの変更は、部分文字列と同じ基本配列を使用せず、より大きな文字配列がより迅速に収集されます。

スタックを乱用しないでください

1つcouldは、可変性の問題を回避するために、不変文字列への参照の代わりに文字列の値を渡します。ただし、大きな文字列の場合、これをスタックに渡すとシステムに悪用されます(xmlドキュメント全体をスタックに文字列として入れ、それを取り出したり、渡し続けたりします...)。

重複排除の可能性

確かに、これは文字列が不変である理由の最初の動機ではありませんでしたが、不変の文字列が良いものである理由の根拠を見るとき、これは確かに検討すべきことです。

Stringsを少し使ったことがある人なら誰でも、メモリを吸い尽くすことができることを知っています。これは、しばらくの間データベースに残っているデータをプルするような場合に特に当てはまります。これらの文字列では、何度も何度も同じ文字列になります(行ごとに1回)。

現在、多くの大規模なJavaアプリケーションがメモリでボトルネックになっています。測定により、これらのタイプのアプリケーションのJavaヒープライブデータセットの約25%がStringオブジェクトによって消費されることがわかっています。さらに、これらのStringオブジェクトの約半分は複製です。ここで、複製とはstring1.equals(string2)がtrueであることを意味します。ヒープ上にStringオブジェクトを複製することは、本質的に、メモリの無駄遣いです。 ...

Java 8 update 20では、これに対処するために JEP 192 (上記の動機)が実装されています。文字列の重複排除がどのように機能するかについて詳しく説明することなく、文字列自体が不変であることが不可欠です。 StringBuilderは変更される可能性があり、誰かがあなたの下から何かを変更したくないので、StringBuilderの重複排除はできません。不変文字列(その文字列プールに関連する)は、通過できることを意味し、同じ2つの文字列が見つかった場合、一方の文字列参照をもう一方に参照して、ガベージコレクターが新しく未使用の文字列を使用できるようにします。

他の言語

Objective C(Javaより前のバージョン)にはNSStringNSMutableStringがあります。

C#と.NETは、デフォルトの文字列が不変であるという同じ設計上の選択を行いました。

Lua 文字列も不変です。

Python も同様です。

歴史的に、LISP、Scheme、Smalltalkはすべて 文字列の内部 であり、したがって不変です。より最近の動的言語では、不変である必要がある何らかの方法で文字列を使用することがよくあります(Stringではないかもしれませんが、不変です)。

結論

これらの設計上の考慮事項は、多数の言語で何度も何度も作成されています。不変なすべての文字列について、不変な文字列は代替手段よりも優れており、全体としてコードの改善(バグの減少)と実行可能ファイルの高速化につながるというのが一般的なコンセンサスです。

105
user40980

私が思い出せる理由:

  1. 文字列プールの場合、1つの文字列オブジェクト/リテラル​​のため、文字列を不変にしない文字列プール機能はまったく不可能です。 「XYZ」は多くの参照変数によって参照されるため、それらのいずれか1つが値を変更すると、他の変数が自動的に影響を受けます。

  2. 文字列は、多くのJavaクラス(たとえば、ネットワーク接続を開くため、データベース接続を開くため、ファイルを開くため)のパラメータとして広く使用されています。文字列が不変でない場合、これは深刻なセキュリティ上の脅威につながります。

  3. 不変性により、Stringはハッシュコードをキャッシュできます。

  4. スレッドセーフにします。

21
NINCOMPOOP

1)文字列プール

Javaデザイナは、あらゆる種類のJavaアプリケーションでStringが最も使用されるデータ型になることを知っており、そのため、最初から最適化したかったのです。そのための重要なステップの1つは、Stringを格納するというアイデアでした。文字列プール内のリテラル。一時的な文字列オブジェクトを共有することでそれらを削減することが目標でした。共有するには、それらが不変クラスからのものである必要があります。相互に認識されていない2つのパーティと可変オブジェクトを共有することはできません。 2つの参照変数が同じStringオブジェクトを指している架空の例:

String s1 = "Java";
String s2 = "Java";

これで、s1がオブジェクトを「Java」から「C++」に変更すると、参照変数も値s2 = "C++"を取得しますが、それについても認識していません。文字列を不変にすることで、この文字列リテラルの共有が可能になりました。つまり、Javaで文字列をfinalまたは不変にしないと、文字列プールの重要なアイデアを実装できません。

2)セキュリティ

Javaには、あらゆるレベルのサービスで安全な環境を提供するという明確な目標があり、Stringはそれらのセキュリティ全般において重要です。文字列は、多くのJavaクラスのパラメータとして広く使用されています。たとえば、ネットワーク接続を開くために、Javaでファイルを読み取るために、ホストとポートを文字列として渡すことができます。ファイルとディレクトリのパスを文字列として渡し、データベース接続を開くためにデータベースのURLを文字列として渡すことができます。文字列が不変でない場合、ユーザーはシステム内の特定のファイルへのアクセスを許可している可能性がありますが、認証後にユーザーは何かへのPATHを指定すると、深刻なセキュリティ問題が発生する可能性があります。同様に、データベースやネットワーク内の他のマシンに接続しているときに、文字列値を変更するとセキュリティ上の脅威が発生する可能性があります。パラメータが文字列であるため、可変文字列もReflectionでセキュリティ問題を引き起こす可能性があります。

3)クラスローディングメカニズムでの文字列の使用

StringをfinalまたはImmutableにするもう1つの理由は、Stringがクラスロードメカニズムで頻繁に使用されていたためです。文字列は不変ではないため、攻撃者はこの事実を利用して、標準のJavaクラスをロードするリクエストなど、Java.io.Readerを悪意のあるクラスcom.unknown.DataStolenReaderに変更できます。 Stringを最終的な不変に保つことで、少なくともJVMが正しいクラスをロードしていることを確認できます。

4)マルチスレッドの利点

同時実行性とマルチスレッド化はJavaの主要な提供物だったので、Stringオブジェクトのスレッドセーフ性について考えることは非常に理にかなっています。 Stringが広く使用されることが予想されていたため、それをImmutableにすることは外部同期を行わないことを意味し、複数のスレッド間でのStringの共有を含むはるかにクリーンなコードを意味します。この単一の機能により、すでに複雑で混乱を招き、エラーが発生しやすい同時実行コーディングがはるかに容易になります。 Stringは不変であり、スレッド間で共有するだけなので、コードがより読みやすくなります。

5)最適化とパフォーマンス

これで、クラスを不変にすると、このクラスは一度作成すると変更されないことが事前にわかっています。これにより、多くのパフォーマンス最適化のためのオープンパスが保証されます。キャッシング。文字列自体は、私が変更することはないので、文字列はそのハッシュコードをキャッシュします。ハッシュコードを遅延計算し、作成したらキャッシュするだけです。単純な世界では、任意のStringオブジェクトのhashCode()メソッドを最初に呼び出すと、ハッシュコードが計算され、その後のすべてのhashCode()の呼び出しは、すでに計算されたキャッシュされた値を返します。これにより、文字列がハッシュベースのマップで頻繁に使用されている場合、パフォーマンスが向上します。 HashtableおよびHashMap。ハッシュコードのキャッシングは、文字列自体の内容に依存するため、不変で最終的なものにしなければ不可能でした。

7
saidesh kilaru

Java仮想マシンは、他の方法では実行できない文字列操作に関していくつかの最適化を実行します。たとえば、値「Mississippi」の文字列があり、「Mississippi」.substring(0 、4)別の文字列に、あなたが知る限り、「ミス」を作るために最初の4文字からコピーが作成されました。あなたが知らないのは、どちらも所有者である同じオリジナルの文字列「ミシシッピ」を共有していることです。もう1つは、位置0から4までのその文字列の参照です(所有者への参照は、所有者がスコープ外に出たときに、ガベージコレクターによって所有者が収集されないようにします)。

これは "Mississippi"ほどの小さな文字列の場合は簡単ですが、文字列が大きく、複数の操作があるため、文字列をコピーする必要がないため、時間を大幅に節約できます。文字列が変更可能である場合、元の文字列を変更すると部分文字列の「コピー」にも影響するため、これを行うことはできません。

また、Donalが述べているように、利点はその欠点によって大幅に圧迫されます。ライブラリに依存するプログラムを作成し、文字列を返す関数を使用するとします。その値が一定であることをどのように確認できますか?このようなことが起こらないようにするには、常にコピーを作成する必要があります。

同じ文字列を共有する2つのスレッドがある場合はどうなりますか?あなたは現在別のスレッドによって書き換えられている文字列を読みたくないでしょうか?したがって、文字列はスレッドセーフである必要があります。これは、一般的なクラスであるため、実質的にすべてのJavaプログラムがはるかに遅くなります。それ以外の場合は、すべてのコピーを作成する必要があります。その文字列を必要とするスレッド、またはその文字列を使用するコードを同期ブロックに配置する必要があります。どちらもプログラムの速度を低下させるだけです。

これらすべての理由から、それはC++と区別するためにJavaに対して行われた初期の決定の1つでした。

5
Neil

文字列が不変である理由は、言語の他のプリミティブ型との一貫性にあります。値42を含むintがあり、それに値1を追加しても、42は変更されません。新しい値43を取得します。これは、開始値とはまったく無関係です。文字列以外のプリミティブを変更しても、概念的な意味はありません。そのため、文字列を不変として扱うプログラムは、多くの場合、推論と理解が容易です。

さらに、Javaは、StringBuilderでわかるように、実際には可変文字列と不変文字列の両方を提供します。実際には、defaultのみが不変文字列です。すべての場所でStringBuilderへの参照を渡したい場合は、そうすることができます。 Javaは、その型システムでの可変性またはその欠如の表現をサポートしていないため、これらの概念に別々の型(StringおよびStringBuilder)を使用します。型システムで不変性をサポートしている言語(C++のconstなど)では、多くの場合、両方の目的を果たす単一の文字列型があります。

はい、文字列を不変にすることで、インターンなどの不変文字列に固有の最適化を実装し、スレッド間で同期せずに文字列参照を渡すことができます。しかし、これはメカニズムを言語の意図された目標と単純で一貫した型システムで混同します。私はこれを、ガベージコレクションを間違った方法で誰もが考える方法にたとえます。 ガベージコレクションは「未使用のメモリの回収」ではなく、「メモリが無制限のコンピュータのシミュレーション」 です。ここで説明するパフォーマンスの最適化は、不変文字列の目標を実際のマシンで適切に実行するために行われるものです。そもそもそのような文字列が不変である理由ではありません。

5
Billy ONeal

不変性とは、自分が所有していないクラスが保持する定数は変更できないことを意味します。所有していないクラスには、Javaの実装のコアにあるクラスが含まれ、変更してはならない文字列には、セキュリティトークン、サービスアドレスなどが含まれます。あなた本当にすべきではありませんこれらの種類のものを変更できる(サンドボックスモードで動作している場合、これは二重に適用されます)。

文字列が不変でなかった場合、文字列の内容が変更されないようにするコンテキストから文字列を取得するたびに、「念のため」コピーを作成する必要があります。それは非常に高くつく。

4
Donal Fellows

データを受け入れ、その正確性を確認してから(たとえば、DBに格納するために)渡すシステムを想像してみてください。

データがStringであり、少なくとも5文字の長さが必要であると仮定します。メソッドは次のようになります。

public void handle(String input) {
  if (input.length() < 5) {
    throw new IllegalArgumentException();
  }
  storeInDatabase(input);
}

ここで、storeInDatabaseがここで呼び出されると、inputが要件に適合することに同意できます。 しかしStringが変更可能である場合、callerinputオブジェクト(別のスレッドから)検証された直後で、データベースに格納される前。これには適切なタイミングが必要であり、おそらく毎回うまくいくとは限りませんが、時々、データベースに無効な値を保存するようにしてもらうことができます。

不変のデータ型は、この(および多くの関連する)問題の非常に単純な解決策です。値をチェックするときはいつでも、チェックされた条件がまだ真であることをdependすることができます。

2
Joachim Sauer

一般に、値の型および参照の型が発生します。値型では、それを表すオブジェクトについては気にせず、値について気にします。私があなたに価値を与えるなら、あなたはその価値が変わらないと期待します。あなたはそれが突然変わってほしくありません。数値5は値です。突然6に変わるとは思わないでしょう。文字列「Hello」は値です。突然「P ***オフ」になるとは思わない。

参照タイプを使用すると、オブジェクトが気になり、変更されることが期待されます。たとえば、配列が変更されることがよくあります。配列を渡して、それをそのまま維持したい場合は、変更しないように信頼するか、コピーを作成する必要があります。

Java文字列クラスを使用する場合、設計者は決定を行わなければなりませんでした。文字列が値型のように動作するか、参照型のように動作するほうが良いですか? Java文字列の場合、それらは値型であることが決定されました。つまり、これらはオブジェクトであるため、不変オブジェクトである必要があります。

反対の決定がなされたかもしれませんが、私の意見では、多くの頭痛の種を引き起こしたでしょう。他の場所で述べたように、多くの言語が同じ決定を行い、同じ結論に達しました。例外はC++で、これには1つの文字列クラスがあり、文字列は定数または非定数にすることができますが、C++では、Javaとは異なり、オブジェクトパラメータは参照としてではなく値として渡すことができます。

0
gnasher729

誰もこれを指摘しなかったので本当に驚いています。

回答:たとえ変更可能であったとしても、それはあなたにとって大きな利益にはなりません。それは追加のトラブルを引き起こすほどの利益にはなりません。突然変異の最も一般的な2つのケースを調べてみましょう。

文字列の1文字を変更する

Java=文字列の各文字は2バイトまたは4バイトをとるので、既存のコピーを変更できるとしたら、何かを得ますか?

2バイト文字を4バイト文字に(またはその逆に)置き換えるシナリオでは、文字列の残りの部分を2バイト左または右にシフトする必要があります。これは、計算の観点から文字列全体をコピーすることとそれほど変わりません。

これはまた、通常は望ましくない、非常に不規則な動作です。誰かが英語のテキストでアプリケーションをテストしていて、アプリケーションが中国などの海外で採用されると、全体が奇妙に動作し始めると想像してください。

既存の文字列(または文字)を既存の文字列に追加する

2つの任意の文字列がある場合、それらは2つの異なるメモリ位置にあります。最初の文字列を2番目の文字列を追加して変更したい場合は、最初の文字列の末尾に追加のメモリを要求することはできません。おそらくすでに使用されているためです。

連結された文字列をまったく新しい場所にコピーする必要があります。これは、両方の文字列が不変である場合とまったく同じです。

追加を効率的に行う場合は、StringBuilderを使用できます。これにより、文字列の末尾にかなりの量のスペースが確保されます。これは、将来の追加の可能性を目的としています。

0
Rok Kralj