Sring 为什么要用final修饰

204 阅读2分钟

什么是不可变?

String不可变很简单,如下图,给一个已有"abcd"第二次赋值成"abcedl",不是在原内存地址上修改数据,而是重新指向一个新对象,新地址,在内存中会创建两个对象。

image.png

String为什么不可变?

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

首先String类是用final关键字修饰,这说明String不可继承。再看下面,String类的主力成员字段value是个char[]数组,而且是用final修饰的。final修饰的字段创建以后就不可改变。

image.png 也就是说Array变量只是stack上的一个引用,数组的本体结构在heap堆。String类里的value用final修饰,只是说stack里的这个叫value的引用地址不可变,没有说堆里array本身数据不可变。那既然我们说 String是不可变的,那显然仅仅靠 final 是远远不够的:

  1. 首先,char数组是private的,并且String类没有对外提供修改这个数组的方法,所以它初始化之后外界没有有效的手段去改变它;
  2. 其次,String 类被 final 修饰的,也就是不可继承,避免被他人继承后破坏;
  3. 最重要的!是因为 Java 作者在String的所有方法里面,都很小心地避免去修改了 char 数组中的数据,涉及到对 char 数组中数据进行修改的操作全部都会重新创建一个String对象。你可以随便翻个源码看看来验证这个说法,比如 substring 方法:
public String substring(int beginIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = value.length - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

为什么要设计成不可变的呢?

1)首先,字符串常量池的需要

我们来回顾一下字符串常量池的定义:大量频繁的创建字符串,将会极大程度的影响程序的性能。为此,JVM 为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化:

  • 为字符串开辟了一个字符串常量池 String Pool,可以理解为缓存区
  • 创建字符串常量时,首先检查字符串常量池中是否存在该字符串
  • 若字符串常量池中存在该字符串,则直接返回该引用实例,无需重新实例化;若不存在,则实例化该字符串并放入池中。

如下面的代码所示,堆内存中只会创建一个 String对象:

String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2) // true 

假设 String 允许被改变,那如果我们修改了 str2 的内容为 good,那么 str1 也会被修改,显然这不是我们想要看见的结果。

2)另外一点也比较容易想到,String被设计成不可变就是为了安全

作为最基础最常用的数据类型,String被许多 Java 类库用来作为参数,如果String不是固定不变的,将会引起各种安全隐患。

举个例子,我们来看看将可变的字符串StringBuilder存入HashSet的场景:

public static void main(String[] args) {
    Set<StringBuffer> set = new HashSet<>();
    StringBuffer s1 = new StringBuffer("aa");
    StringBuffer s2 = new StringBuffer("aabb");
    set.add(s1);
    set.add(s2);

    for (StringBuffer s : set) {
        System.out.println(s);
    }

    StringBuffer s3 = s1;
    s3.append("bb");
    set.add(s3);
    for (StringBuffer s : set) {
        System.out.println(s);
    }
}

我们把可变字符串 s3 指向了 s1 的地址,然后改变 s3 的值,由于StringBuilder没有像String那样设计成不可变的,所以 s3 就会直接在 s1 的地址上进行修改,导致 s1 的值也发生了改变。于是,糟糕的事情发生了,HashSet中出现了两个相等的元素,破坏了HashSet的不包含重复元素的原则。

另外,在多线程环境下,众所周知,多个线程同时想要修改同一个资源,是存在危险的,而String作为不可变对象,不能被修改,并且多个线程同时读同一个资源,是完全没有问题的,所以String是线程安全的。

String 真的不可变吗?

想要改变String无非就是改变 char 数组 value 的内容,而 value 是私有属性,那么在 Java 中有没有某种手段可以访问类的私有属性呢?

没错,就是反射,使用反射可以直接修改 char 数组中的内容,当然,一般来说我们不这么做。

看下面代码:

public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
    String s = "hello";
    System.out.println(s);

    Class<? extends String> cls = s.getClass();
    Field value = cls.getDeclaredField("value");
    value.setAccessible(true);
    char[] v = (char[]) value.get(s);
    v[0] = 'g';
    System.out.println(s);
}

总结

总结来说,并不是因为 char 数组是final才导致String的不可变,而是为了把String设计成不可变才把 char 数组设置为final的。下面是一些创建不可变对象的简单策略,当然,也并非所有不可变类都完全遵守这些规则:

  • 不要提供 setter 方法(包括修改字段的方法和修改字段引用对象的方法);
  • 将类的所有字段定义为 final、private 的;
  • 不允许子类重写方法。简单的办法是将类声明为 final,更好的方法是将构造函数声明为私有的,通过工厂方法创建对象;
  • 如果类的字段是对可变对象的引用,不允许修改被引用对象。