天天看點

你真的了解Java 字元串的不可變性嗎?一、背景二、案例三、思考

一、背景

字元串的不可變性可以說是面試中的一個常見的“簡單的” 問題。

常見的回答如:

字元串建立後不可改變。

字元串的不可變性是指字元串的字元不可變。

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.5
A string literal is a reference to an instance of class

String

( §4.3.1 , §4.3.3

).

字元串字面量是指向字元串執行個體的一個引用。

Moreover, a string literal always refers to the same instance of class

String

. 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.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&trade; 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 屬于字面量,指向同一個字元串池中的同一個對象,是以對象的屬性修改,兩個地方列印都會受到影響。

三、思考

很多簡單的問題并沒有看起來那麼簡單。

大家在看技術部落格,在讀源碼的時候,一定要有自己的思考,多問幾個為什麼,有機會多動手實踐。

大家在學習某個技術時要養成本質思維,即思考問題的本質是什麼。

面試的時候,簡單的問題要回答全面又有深度,不會的問題要回答出自己的思路,這樣才會有更多的機會。