【面试突击】Java中String类为什么要设计成final?

talk is cheap, show me the code.

什么是不可变?

String a = "abcd";
a = "abcdef";

大多数人看了上面这两句代码,都认为aabcd变成了abcdef,而且aString类型的,这句String不可变不攻自破啊?那么真的是这样吗?
这个理解是错误的。大多数人对String不可变这句话的理解都容易陷入上面这种思想。
而这两句代码的真正含义是
首先将String类型的变量a赋值为abcd,再将变量a赋值为abcdef
进行第二次赋值时不是在原内存地址上进行修改数据,而是在堆中建了一个新的String对象,并将栈中的引用指向了这个新对象,新地址。
所以abcd这个字符串对象从创建出来后,始终都没有被改变。

String为什么不可变?

翻开JDK源码,java.lang.String类起手前三行,是这样写的:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /** String本质是个char数组. 而且用final关键字修饰.*/
    private final char value[];
	...
	...
}

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

有的人以为故事就这样完了,其实没有。
因为虽然value是不可变,也只是value这个引用地址不可变。
挡不住Array数组是可变的事实。

Array的数据结构看下图

在这里插入图片描述
也就是说value只是在栈中存了这个数组的引用地址,数组的本体结构在堆。
同理,String类里的valuefinal修饰,只是value在栈中存的这个数组的引用地址不可变,但是可以改变堆中的这个数组的内容。

看下面这个例子

final int[] value={1,2,3}
int[] another={4,5,6};
value = another;    //编译器报错,final不可变

valuefinal修饰,编译器不允许我把value指向堆区另一个地址。
但如果我直接对数组元素动手,分分钟搞定。如:

final int[] value={1,2,3};
value[2]=100;  //这时候数组里已经是{1,2,100}

或者更粗暴的反射直接改,也是可以的。如:

final int[] array={1,2,3};
Array.set(array,2,100); //数组也被改成{1,2,100}

所以String是不可变,关键是因为SUN公司的工程师,在后面所有String的方法里很小心的没有去动Array里的元素,没有暴露内部成员字段。

private final char value[]这一句里,使用private修饰value,保证外部不可见;使用final修饰value,保证内部不改变value的引用。
而且设计师还很小心地把整个String设成final禁止继承,避免被其他人继承后破坏。
所以String是不可变的关键都在底层的实现,而不单单是一个final的功劳。
考验的是工程师构造数据类型,封装数据的功力。

不可变有什么好处?

最简单的原因,就是为了安全

示例1

String a, b, c;
a = "test";
b = a;
c = b;

a += "A";
System.out.println(a);
System.out.println(b);
System.out.println(c);
System.out.println();


b += "B";
System.out.println(a);
System.out.println(b);
System.out.println(c);
System.out.println();

c += "C";
System.out.println(a);
System.out.println(b);
System.out.println(c);
System.out.println();

控制台输出

testA
test
test

testA
testB
test

testA
testB
testC

示例一的输出可以看出String为不可变的时候,b、c是通过引用传递的方式进行赋值,
虽然一开始三个变量都指向了同一个地址,但是改变了a的值,并没有影响后面b、c的使用。

如果String是可变的,就可能如下例,我们使用StringBuffer来模拟String是可变的:

StringBuffera, b, c;
a = new StringBuffer("test");
b = a;
c = b;

a.append("A");
System.out.println(a.toString());
System.out.println(b.toString());
System.out.println(c.toString());
System.out.println();

b.append("B");
System.out.println(a.toString());
System.out.println(b.toString());
System.out.println(c.toString());
System.out.println();

c.append("C");
System.out.println(a.toString());
System.out.println(b.toString());
System.out.println(c.toString());
System.out.println();

控制台输出

testA
testA
testA
testAB
testAB
testAB
testABC
testABC
testABC

我么的本意是希望a、b、c是不变的,是相互独立的,结果却并不是我们期望的那样。
所以String不可变的安全性就体现在这里。
实际上StringBuffer的作用就是起到了String的可变配套类角色。

示例2

再看下面这个HashSetStringBuilder做元素的场景,问题就更严重了,而且更隐蔽。

HashSet<StringBuilder> hs=new HashSet<>();
StringBuilder sb1 = new StringBuilder("aaa");
StringBuilder sb2 = new StringBuilder("aaabbb");
hs.add(sb1);
hs.add(sb2);    // 这时候HashSet里是{"aaa","aaabbb"}

StringBuilder sb3 = sb1;
sb3.append("bbb");  // 这时候HashSet里是{"aaabbb","aaabbb"}
System.out.println(hs);

控制台输出

[aaabbb, aaabbb]

StringBuilder型变量sb1sb2分别指向了堆内的字面量aaaaaabbb,并把它们插入到HashSet。
后面将sb1赋值给sb3,再改变sb3的值,因为StringBuilder没有不可变性的保护,
sb3直接在原先aaa的地址上改,导致sb1的值也变了。
这时候,HashSet上就出现了两个内容相等的字符串aaabbb。破坏了HashSet元素的唯一性。
所以千万不要用可变类型做HashMapHashSet

不可变性支持线程安全

在并发场景下,多个线程同时对一个资源进行写操作,会出现线程安全的问题。
不可变对象不能被写,所以是线程安全的。

总结

Q:Java中String类为什么要设计成final?
A:.安全性、效率

  • 安全性:final类型的类不能被继承,并且String类中的final方法可以防止其内部的方法被重写,乱改。
  • 效率:final类型的类被JVM当作内联函数,提高了性能。

参考

在java中String类为什么要设计成final?

猜你喜欢

转载自blog.csdn.net/AV_woaijava/article/details/106934661