首頁  >  文章  >  Java  >  Java String原始碼分析

Java String原始碼分析

高洛峰
高洛峰原創
2017-02-27 15:25:321355瀏覽

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只是一個引用,它指向了一個具體的對象,當s=“123456”; 這句代碼執行過之後,又創建了一個新的對象“123456”, 而引用s重新指向了這個心的對象,原來的物件「ABCabc」還在記憶體中存在,並沒有改變。記憶體結構如下圖:

Java String源码分析

Java和C++的一個不同點是, 在Java中不可能直接操作物件本身,所有的對象都由一個引用指向,必須透過這個引用才能存取物件本身,包括取得成員變數的值,改變物件的成員變量,呼叫物件的方法等。而在C++中存在引用,物件和指標三個東西,這三個東西都可以存取物件。其實,Java中的引用和C++中的指標在概念上是相似的,他們都是存放的物件在記憶體中的位址值,只是在Java中,引用喪失了部分彈性,例如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類別做了一些改動,主要是改變了substring方法執行時的行為,這和本文的主題不相關。 JDK1.7中String類別的主要成員變數就剩下了兩個:

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在這個value數組中的起始位置,count是String所佔的字元的個數。在JDK7中,只有一個value變量,也就是value中的所有字元都是屬於String這個物件的。這個改變不影響本文的討論。 除此之外還有一個hash成員變量,是該String物件的雜湊值的緩存,這個成員變數也和本文的討論無關。在Java中,陣列也是物件(可以參考我之前的文章java中陣列的特性)。 所以value也只是一個引用,它指向一個真正的陣列物件。其實執行了String s = “ABCabc”; 這句程式碼之後,真正的記憶體佈局應該是這樣的:


Java String源码分析

# #value,offset和count這三個變數都是private的,沒有提供setValue, setOffset和setCount等公共方法來修改這些值,所以在String類別的外部無法修改String。也就是說一旦初始化就不能修改, 且在String類別的外部不能存取這三個成員。另外,value,offset和count這三個變數都是final的, 也就是說在String類別內部,一旦這三個值初始化了, 也不能改變。所以可以認為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