继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

你真的理解Java 字符串的不可变性吗?

明明如月
关注TA
已关注
手记 59
粉丝 3858
获赞 1465

一、背景

字符串的不可变性可以说是面试中的一个常见的“简单的” 问题。

常见的回答如:

字符串创建后不可改变。

字符串的不可变性是指字符串的字符不可变。

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 ?

是因为字符串字面量都指向字符串池中的同一个字符串对象(本质是池化的思想,通过复用来减少资源占用来提高性能)。

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 属于字面量,指向同一个字符串池中的同一个对象,因此对象的属性修改,两个地方打印都会受到影响。

三、思考

很多简单的问题并没有看起来那么简单。

大家在看技术博客,在读源码的时候,一定要有自己的思考,多问几个为什么,有机会多动手实践。

大家在学习某个技术时要养成本质思维,即思考问题的本质是什么。

面试的时候,简单的问题要回答全面又有深度,不会的问题要回答出自己的思路,这样才会有更多的机会。


想了解更多开发和避坑技巧,经验,学习方法少走弯路,

欢迎关注本人的慕课专栏:

打开App,阅读手记
3人推荐
发表评论
随时随地看视频慕课网APP