ホームページ >Java >&#&チュートリアル >Java String ソースコード分析

Java String ソースコード分析

高洛峰
高洛峰オリジナル
2017-02-27 15:25:321430ブラウズ

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 String源码分析

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<String>, 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<String>, 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"; を実行した後、実際のメモリ レイアウトは次のようになります:


Java String源码分析

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(&#39;A&#39;, &#39;a&#39;); 
System.out.println("a = " + a);

出力される結果は次のようになります:

a = ABCabc
a = aBCabc

那么a的值看似改变了,其实也是同样的误区。再次说明, a只是一个引用, 不是真正的字符串对象,在调用a.replace('A', 'a')时, 方法内部创建了一个新的String对象,并把这个心的对象重新赋给了引用a。String中replace方法的源码可以说明问题:

Java String源码分析

读者可以自己查看其他方法,都是在方法内部重新创建新的String对象,并且返回这个新的对象,原来的对象是不会被改变的。这也是为什么像replace, substring,toLowerCase等方法都存在返回值的原因。也是为什么像下面这样调用不会改变对象的值:

String ss = "123456"; 
 
System.out.println("ss = " + ss); 
 
ss.replace(&#39;1&#39;, &#39;0&#39;); 
 
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] = &#39;_&#39;; 
   
  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中文网!


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。