一、背景
字元串的不可變性可以說是面試中的一個常見的“簡單的” 問題。
常見的回答如:
字元串建立後不可改變。
字元串的不可變性是指字元串的字元不可變。
String 的 value 字元數組聲明為 final 保證不可變。
真的是這樣嗎?
下面我們再思考兩個問題:
- 那麼字元串的不可變究竟是指什麼?
- 是如何保證的呢?
下面看一個奇怪的現象:在程式一段程式的最後執行下面的語句居然列印了 "aw" 為什麼?
// 前面代碼省略
System.out.println("ab");
建議大家先思考,然後再看下文。
二、案例
你認為下面的示例代碼的輸出結果是啥?
import java.lang.reflect.Field;
public class Test {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String str = "ab";
System.out.println("str=" + str);
Class stringClass = str.getClass();
Field field = stringClass.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(str);
value[1] = 'w';
System.out.println("str=" + str );
}
}
輸出結果為:
str=ab
str=aw
是不是和有些同學想的有些不一樣呢?
字元串的字元數組可以通過反射進行修改,導緻字元串的“内容”發生了變化。
我們再多列印一些:
import java.lang.reflect.Field;
public class Test {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String str = "ab";
System.out.println("str=" + str + "," + System.identityHashCode(str)+","+ str.hashCode());
Class stringClass = str.getClass();
Field field = stringClass.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(str);
value[1] = 'w';
System.out.println("str=" + str + "," + System.identityHashCode(str)+","+ str.hashCode());
}
}
輸出結果為:
str=ab,1638215613,3105
str=aw,1638215613,3105
通過這個例子我們可以看出,String 字元串對象的 value 數組的元素是可以被修改的。
簡單看下
java.lang.System#identityHashCode
的源碼:
/**
* Returns the same hash code for the given object as
* would be returned by the default method hashCode(),
* whether or not the given object's class overrides
* hashCode().
* The hash code for the null reference is zero.
*
* @param x object for which the hashCode is to be calculated
* @return the hashCode
* @since JDK1.1
*/
public static native int identityHashCode(Object x);
native 方法,該函數給出對象唯一的哈希值(不管是否重寫了 hashCode 方法)。
可知,對象沒有變。
那麼,我們知道 String 的哈希值是通過字元串的字元數組計算得來的(JDK8),那為啥兩次 hashCode 函數傳回值一樣呢?
我們再仔細看下
java.lang.String#hashCode
源碼:
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
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
//省略其他
}
發現在第一次調用 hashCode 函數之後,字元串對象内通過 hash 這個屬性緩存了 hashCode的計算結果(隻要緩存過了就不會再重新計算),是以第二次和第一次相同。
那麼如何保證不可變性的呢?
首先将 String 類聲明為 fianl 保證不可繼承。
然後,所有修改的方法都傳回新的字元串對象,保證修改時不會改變原始對象的引用。
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
其次,字元串字面量都會指向同一個對象。
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// 字元串字面量
String str = "ab";
System.out.println("str=" + str + "," + System.identityHashCode(str)+","+ str.hashCode());
Class stringClass = str.getClass();
Field field = stringClass.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(str);
value[1] = 'w';
System.out.println("str=" + str + "," + System.identityHashCode(str)+","+ str.hashCode());
// 字元串字面量
System.out.println("ab");
}
可以看到列印結果為:
str=ab,1638215613,3105
str=aw,1638215613,3105
aw
很多人不了解,為啥
System.out.println("ab");
列印 aw ?
是因為字元串字面量都指向字元串池中的同一個字元串對象(本質是池化的思想,通過複用來減少資源占用來提高性能)。
https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.10.5A string literal is a reference to an instance of class( §4.3.1 , §4.3.3
String
).
字元串字面量是指向字元串執行個體的一個引用。
Moreover, a string literal always refers to the same instance of class
. This is because string literals - or, more generally, strings that are the values of constant expressions ( §15.28 ) - are "interned" so as to share unique instances, using the method
String
String.intern
.
字元串字面量都指向同一個字元串執行個體。
因為字面量字元串都是常量表達式的值,都通過
共享唯一執行個體。
String.intern
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
對象池中存在,則直接指向對象池中的字元串對象,否則建立字元串對象放到對象池中并指向該對象。
是以可以看出,字元串的不可變性是指引用的不可變。
雖然 String 中的 value 字元數組聲明為 final,但是這個 final 僅僅是讓 value的引用不可變,而不是為了讓字元數組的字元不可替換。
由于開始的 ab 和最後的 ab 屬于字面量,指向同一個字元串池中的同一個對象,是以對象的屬性修改,兩個地方列印都會受到影響。
三、思考
很多簡單的問題并沒有看起來那麼簡單。
大家在看技術部落格,在讀源碼的時候,一定要有自己的思考,多問幾個為什麼,有機會多動手實踐。
大家在學習某個技術時要養成本質思維,即思考問題的本質是什麼。
面試的時候,簡單的問題要回答全面又有深度,不會的問題要回答出自己的思路,這樣才會有更多的機會。