扒开外衣仔细分析:String为什么不可变

4,450 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

说到Java源码,我相信小伙伴们或多或少都有接触,比如util包中常见的ArrayList、HashMap、LinkedHashMap,还有前面我们学过的多线程相关类,如:ExecutorsCountDownLatchCyclicBarrier,又或者是lang包中的ObjectIntegerStringThread等。

这些都是我们比较常见的类,不过对于他们的实现原理,我们有时候并不能说出个所以然来,甚至有些人写了四五年代码,连最最常见的String如何实现都没有看过,还总是抱怨每天总是搬砖、前途迷茫。说实话,学习源码并不仅仅是为了应对面试,更重要的是我们通过阅读源码的过程,学习它的设计思想,这种思想在我们项目中完全可以实践出来。

总结来说,阅读源码有以下几点好处:

  1. 学习优秀的设计思想;
  2. 增加面试实力,横扫一切;
  3. 代码日常评审时大秀肌肉。

二、String源码分析

首先看一个小例子:

String name = "huage";
name="huasao";

相信面试中我们也会遇到类似【这个过程中有几个对象】的问题,其实当name重新赋值后,并不会在原有内存地址中修改,而是会创建一个新对象,如下图所示。

image.png

2.1 不可继承原因一:类定义

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
...
}

有了上面这个例子,我们就要从代码中来分析,String为什么不可变,在String.java类定义中可以看到:String类由final关键字修饰,这也意为着String类不可被继承,创建后不能被修改。

String类同时实现了三个接口:

  • Serializable:实现序列化,标记接口,用于标识序列化,未实现该接口无法被序列化。
  • Comparable:对两个实例化对象比较大小
  • CharSequence:String本质是个char数组,该接口为只读的字符序列。

2.2 不可继承原因二:成员变量

/** The value is used for character storage. */
private final char value[];

String 中保存数据的是一个 char 的数组 value, value 同样也是被 final 修饰,这就意外着该数组无法被修改。

但是这时候有小伙伴就会反问道:被final修饰的变量只是引用不可变,堆内存中的值完全可以被修改啊。

final char value[] = {'h','u','a','g','e'};
value[4] = 's';
System.out.println(value); //最终输出:huags

这个时候我们就要看一下,value 的访问权限是 private ,说明外部访问不到该变量,并且String 也没有提供 value 的相关操作方法,所以 value 一旦生成就无法再被被修改。

2.3 拓展:常见方法

接下来看String类中几个常见的方法是如何实现的

  • equals
public boolean equals(Object anObject) {
    //内存地址是否相同
    if (this == anObject) {
        return true;
    }
    //目标对象是否为String类,不是的话直接返回false
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        //判断两者长度是否相等
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            //比较两者中每一个字符是否相等
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}
  • 截取字符串:substring
public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > value.length) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    int subLen = endIndex - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return ((beginIndex == 0) && (endIndex == value.length)) ? this
            : new String(value, beginIndex, subLen);
}

通过上述源码可以得出:对于满足条件的起始/终止下标,该方法会调用new String()构成器,而该构造器最终通过System.arraycopy生成一个新的字符串返回,这也就意为着该方法并不会在原有字符串基础上进行修改。

同样的,我们可以查看replace、concat等方法源码,他们都会不会修改原有字符串,而是会生成一个新的字符串对象返回,这也是String不可变的表现。

三、String不可变的好处

  • 节省内存

如果定义了多个对象,并且每个对象的值是相同的,那么在实际中,堆内存只会存储一份数据,从而节省内存开销

image.png

\