不変オブジェクトとは何ですか?
String オブジェクトは不変ですが、それはパブリック メソッドを呼び出して値を変更できないことを意味します。
ご存知のとおり、Java では String クラスは不変です。では、不変オブジェクトとは正確には何でしょうか? 次のように考えることができます。オブジェクトの作成後にその状態を変更できない場合、そのオブジェクトは不変です。状態は変更できません。つまり、基本データ型の値を含むオブジェクト内のメンバー変数は変更できません。また、参照型が指すオブジェクトの状態も変更できません。変えられる。
オブジェクトとオブジェクト参照を区別する
Java 初心者にとって、String が不変オブジェクトであることについては常に疑問があります。以下のコードを見てください。
文字列 s = "ABCabc";System.out.println("s = " + s);s = "123456";System.out.println("s = " + s);印刷結果は次のとおりです。
s = ABCabc s = 123456
まず String オブジェクト s を作成し、次に s の値を「ABCabc」、次に s の値を「123456」とします。 印刷結果からわかるように、s の値は実際に変化しています。では、なぜ String オブジェクトは不変だとまだ言えるのでしょうか? 実際、ここには誤解があります。s は String オブジェクトへの単なる参照であり、オブジェクト自体ではありません。オブジェクトはメモリ内のメモリ領域であり、メンバ変数が増えるほど、このメモリ領域が占めるスペースも大きくなります。参照は、参照先のオブジェクトのアドレスを格納する 4 バイトのデータであり、このアドレスを通じてオブジェクトにアクセスできます。
つまり、s は特定のオブジェクトを指す単なる参照であり、このコードが実行された後、新しいオブジェクト "123456" が作成され、参照 s は再びこのオブジェクトを指します。 、元のオブジェクト「ABCabc」はメモリ内にまだ存在しており、変更されていません。メモリ構造を次の図に示します。
Java と C++ の違いの 1 つは、Java ではオブジェクト自体を直接操作できないことです。メンバー変数の値の取得や変更など、オブジェクト自体にアクセスするには、この参照を使用する必要があります。オブジェクトのメンバー変数、オブジェクトのメソッドなどを呼び出します。 C++ には、参照、オブジェクト、ポインターの 3 つがあり、これらはすべてオブジェクトにアクセスできます。実際、Java の参照と C++ のポインタは概念的に似ています。ただし、Java では、参照は加算や減算のように使用することができません。 C++ のポインターのように実行されます。
なぜ String オブジェクトは不変なのでしょうか?
String の不変性を理解するには、まず String クラスのメンバー変数を見てください。 JDK1.6では、Stringのメンバー変数には次のものが含まれます。
public Final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence{ /** 値は文字ストレージに使用されます */ private Final char value[]; /** オフセットはストレージの最初のインデックスです。 */ private last int offset; /** カウントは文字列の文字数です。 */ private int count; /** 文字列のハッシュ コードをキャッシュします。デフォルトは0JDK1.7 では、String クラスにいくつかの変更が加えられています。主に部分文字列メソッドの実行時の動作が変更されていますが、これはこの記事のトピックとは関係ありません。 JDK1.7 の String クラスの主要なメンバー変数は 2 つだけです。
public Final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence { /** 値は文字の保存に使用されます */ private Final char value[]; /** 文字列のハッシュ コードをキャッシュします */プライベート int ハッシュ // デフォルトは 0上記のコードからわかるように、Java の String クラスは実際には文字配列をカプセル化したものです。 JDK6 では、value は String によってカプセル化された配列、offset は値配列内の String の開始位置、count は String が占める文字数です。 JDK7 では、値変数は 1 つだけです。つまり、value 内のすべての文字は String オブジェクトに属します。この変更は、この記事の説明には影響しません。 さらに、String オブジェクトのハッシュ値のキャッシュであるハッシュ メンバー変数もありますが、このメンバー変数もこの記事の説明とは無関係です。 Java では、配列もオブジェクトです (以前の記事「Java における配列の特性」を参照してください)。 したがって、value は単なる参照であり、実際の配列オブジェクトを指します。実際、コード String s = “ABCabc”; を実行すると、実際のメモリ レイアウトは次のようになります。
3 つの変数 value、offset、count はすべてプライベートであり、これらの値を変更するための setValue、setOffset、setCount などのパブリック メソッドは提供されていないため、String クラスの外部で String を変更することはできません。つまり、一度初期化すると変更することはできず、これら 3 つのメンバーには String クラスの外部からアクセスすることはできません。さらに、3 つの変数 value、offset、count はすべて最終的な値です。つまり、String クラス内では、これら 3 つの値が初期化されると変更できません。したがって、String オブジェクトは不変であると考えることができます。
したがって、String には明らかにいくつかのメソッドがあり、それらを呼び出すと変更された値を取得できます。これらのメソッドには、substring、replace、replaceAll、toLowerCase などが含まれます。たとえば、次のコード:
文字列 a = "ABCabc"; System.out.println("a = " + a); a = a.replace('A', 'a');印刷結果は次のとおりです。
a = ABCabca = aBCabc
するとaの値が変わったように見えますが、実は同じ誤解です。繰り返しますが、 a は単なる参照であり、実際の文字列オブジェクトではありません。 a.replace('A', 'a') を呼び出すと、メソッドは内部で新しい String オブジェクトを作成し、その新しいオブジェクトを Cited a に再割り当てします。 String の replace メソッドのソース コードで問題を説明できます。
リーダーは他のメソッドを自分でチェックでき、メソッド内で新しい String オブジェクトを再作成し、この新しいオブジェクトを返します。元のオブジェクトは変更されません。これが、replace、substring、toLowerCase などのメソッドがすべて戻り値を持つ理由です。これは、次のように呼び出してもオブジェクトの値が変更されない理由でもあります。
String ss = "123456";System.out.println("ss = " + ss);ss.replace('1', '0');System.out.println("ss = " + ss);結果を出力します。
ss = 123456 ss = 123456
String オブジェクトは本当に不変なのでしょうか?
上記からわかるように、String のメンバー変数はプライベートな最終変数です。つまり、初期化後に変更することはできません。これらのメンバーのうち、value は実際のオブジェクトではなく参照変数であるため、特別です。 value は、final によって変更されます。つまり、final は他の配列オブジェクトを指すことができなくなります。そのため、value が指す配列を変更できますか? たとえば、配列内の特定の位置にある文字をアンダースコア「_」に変更します。 少なくとも、私たちが自分で書く通常のコードではそれを行うことはできません。なぜなら、この参照を通じて配列を変更することはおろか、この値参照にまったくアクセスできないからです。
では、どうすればプライベートメンバーにアクセスできるのでしょうか? そうです、リフレクションを使用すると、String オブジェクトの value 属性を反映し、取得した値の参照を通じて配列の構造を変更できます。コード例は次のとおりです。
public static void testReflection() throws Exception { //文字列「Hello World」を作成し、それを参照に割り当てます。 s String s = "Hello World"; //Hello World //Stringクラスの値フィールドを取得 Field valueFieldOfString = String.class.getDeclaredField("value") //値属性のアクセス権限を変更 valueFieldOfString.setAccessible(true); //s オブジェクトの value 属性の値を取得します char[] value = (char[]) valueFieldOfString.get(s); //value によって参照される配列の 5 番目の文字を変更します value[5] = '_' ; System.out.println("s = " + s);印刷結果は次のとおりです。
s = Hello World s = Hello_World
このプロセスでは、 s は常に同じ String オブジェクトを参照しますが、リフレクションの前後で String オブジェクトは変化します。つまり、いわゆる「不変」オブジェクトはリフレクションを通じて変更できます。しかし、一般的にはそんなことはしません。このリフレクションの例は、問題を説明することもできます。オブジェクトおよびそれを構成する他のオブジェクトの状態が変化する可能性がある場合、このオブジェクトはおそらく不変オブジェクトではありません。たとえば、Car オブジェクトは Wheel オブジェクトと結合されますが、Wheel オブジェクトはプライベート Final として宣言されていますが、Wheel オブジェクトの内部状態は変更される可能性があるため、Car オブジェクトが不変であることは保証できません。