Java String ソースコード分析
不変オブジェクトとは何ですか?
ご存知のとおり、Java では String クラスは不変です。では、不変オブジェクトとは正確には何でしょうか? 次のように考えることができます。オブジェクトの作成後にその状態を変更できない場合、そのオブジェクトは不変です。状態は変更できません。つまり、基本データ型の値を含むオブジェクト内のメンバー変数は変更できません。また、参照型が指すオブジェクトの状態も変更できません。変えられる。
オブジェクトとオブジェクト参照を区別する
Java 初心者にとって、String が不変オブジェクトであることについては常に疑問があります。次のコードを見てください:
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 つがあり、これら 3 つすべてがオブジェクトにアクセスできます。実際、Java の参照と C++ のポインタは概念的に似ています。ただし、Java では、参照は加算や減算のように使用することができません。 C++ のポインターのように実行されます。
なぜ String オブジェクトは不変なのでしょうか?
String の不変性を理解するには、まず String クラスのメンバー変数を見てください。 JDK1.6 では、String のメンバー変数には次のものが含まれます。
public final class String implements java.io.Serializable, Comparable, CharSequence { /** The value is used for character storage. */ private final char value[]; /** The offset is the first index of the storage that is used. */ private final int offset; /** The count is the number of characters in the String. */ private final int count; /** Cache the hash code for the string */ private int hash; // Default to 0
JDK1.7 では、String クラスにいくつかの変更が加えられ、主に実行時の部分文字列メソッドの動作が変更されました。これはこれと一致しています。記事 トピックは関係ありません。 JDK1.7 の String クラスの主なメンバー変数は 2 つだけです:
public final class String implements java.io.Serializable, Comparable, CharSequence { /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0
上記のコードからわかるように、Java の String クラスは実際には文字配列のカプセル化です。 JDK6 では、value は String によってカプセル化された配列、offset は値配列内の String の開始位置、count は String が占める文字数です。 JDK7 では、値変数は 1 つだけです。つまり、value 内のすべての文字は String オブジェクトに属します。この変更は、この記事の説明には影響しません。 さらに、String オブジェクトのハッシュ値のキャッシュであるハッシュ メンバー変数もありますが、このメンバー変数もこの記事の説明とは無関係です。 Java では、配列もオブジェクトです (以前の記事「Java における配列の特性」を参照してください)。 したがって、value は単なる参照であり、実際の配列オブジェクトを指します。実際、コード String s = "ABCabc"; を実行した後、実際のメモリ レイアウトは次のようになります:
value、offset、count の 3 つの変数はすべてプライベートであり、パブリックには提供されません。これらの値を変更するには setValue、setOffset、setCount などのメソッドが使用されるため、String クラスの外部で String を変更することはできません。つまり、一度初期化すると変更することはできず、これら 3 つのメンバーには String クラスの外部からアクセスすることはできません。さらに、3 つの変数 value、offset、count はすべて最終的な値です。つまり、String クラス内では、これら 3 つの値が初期化されると変更できません。したがって、String オブジェクトは不変であると考えることができます。
つまり、String には明らかにいくつかのメソッドがあり、それらを呼び出すと変更された値を取得できます。これらのメソッドには、substring、replace、replaceAll、toLowerCase などが含まれます。たとえば、次のコード:
String a = "ABCabc"; System.out.println("a = " + a); a = a.replace('A', 'a'); System.out.println("a = " + a);
出力される結果は次のようになります:
a = ABCabc a = aBCabc
那么a的值看似改变了,其实也是同样的误区。再次说明, a只是一个引用, 不是真正的字符串对象,在调用a.replace('A', 'a')时, 方法内部创建了一个新的String对象,并把这个心的对象重新赋给了引用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的成员变量是private final 的,也就是初始化之后不可改变。那么在这几个成员中, value比较特殊,因为他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变value指向的数组吗? 比如将数组中的某个位置上的字符变为下划线“_”。 至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个value引用,更不能通过这个引用去修改数组。
那么用什么方式可以访问私有成员呢? 没错,用反射, 可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。下面是实例代码:
public static void testReflection() throws Exception { //创建字符串"Hello World", 并赋给引用s String s = "Hello World"; System.out.println("s = " + s); //Hello World //获取String类中的value字段 Field valueFieldOfString = String.class.getDeclaredField("value"); //改变value属性的访问权限 valueFieldOfString.setAccessible(true); //获取s对象上的value属性的值 char[] value = (char[]) valueFieldOfString.get(s); //改变value所引用的数组中的第5个字符 value[5] = '_'; System.out.println("s = " + s); //Hello_World }
打印结果为:
s = Hello World s = Hello_World
在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化, 也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Wheel对象,虽然这个Wheel对象声明成了private final 的,但是这个Wheel对象内部的状态可以改变, 那么就不能很好的保证Car对象不可变。
感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!
更多Java String源码分析相关文章请关注PHP中文网!