String类设计成final的原因

转发自:https://blog.csdn.net/u013905744/article/details/52414111

大神链接:在java中String类为什么要设计成final? - 程序员 - 知乎

我进行了重新排版,并且更换了其中的一个例子,让我们更好理解。

String很多实用的特性,比如说不可变性,是工程师精心设计的艺术品!艺术品易碎!用final就是拒绝继承,防止世界被熊孩子破坏,维护世界和平!

1. 什么是不可变?

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


2. String为什么不可变?

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

  1. public final class String implements java.io.Serializable, Comparable<String>, CharSequence {  
  2.     /** String本质是个char数组. 而且用final关键字修饰.*/  
  3.     private final char value[];  
  4.     …  
  5.     …  
  6. }  
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的数据结构看下图

也就是说Array变量只是stack上的一个引用,数组的本体结构在heap堆。String类里的valuefinal修饰,只是说stack里的这个叫value的引用地址不可变。没有说堆里array本身数据不可变。看下面这个例子,

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

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

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

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

  1. final int[] array={1,2,3};  
  2. Array.set(array,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的私有访问权限的作用都比final大。而且设计师还很小心地把整个String设成final禁止继承,避免被其他人继承后破坏。所以String是不可变的关键都在底层的实现,而不是一个final考验的是工程师构造数据类型,封装数据的功力。


3. 不可变有什么好处?

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

示例1

  1. package _12_01字符串;  
  2.   
  3. public class 为什么String要设计成不可变类你 {  
  4.   
  5.     public static void main(String[] args) {  
  6.   
  7.         String a, b, c;  
  8.         a = ”test”;  
  9.         b = a;  
  10.         c = b;  
  11.         String processA = processA(a);  
  12.         String processB = processB(b);  
  13.         String processC = processC(c);  
  14.         System.out.println(processA);  
  15.         System.out.println(processB);  
  16.         System.out.println(processC);  
  17.     }  
  18.       
  19.     static String processA(String str){  
  20.         return str + “A”;  
  21.     }  
  22.       
  23.     static String processB(String str){  
  24.         return str + “B”;  
  25.     }  
  26.       
  27.     static String processC(String str){  
  28.         return str + “C”;  
  29.     }  
  30.   
  31. }  
  32. //OUTPUT  
  33. // testA  
  34. //testB  
  35. //testC  
package _12_01字符串;

public class 为什么String要设计成不可变类你 {

    public static void main(String[] args) {

        String a, b, c;
        a = "test";
        b = a;
        c = b;
        String processA = processA(a);
        String processB = processB(b);
        String processC = processC(c);
        System.out.println(processA);
        System.out.println(processB);
        System.out.println(processC);
    }

    static String processA(String str){
        return str + "A";
    }

    static String processB(String str){
        return str + "B";
    }

    static String processC(String str){
        return str + "C";
    }

}
//OUTPUT
// testA
//testB
//testC

String支持非可变性的时候,它们的值很好确定,不管调用哪个方法,都互不影响。

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

  1. package _12_01字符串;  
  2.   
  3. public class 为什么String要设计成不可变类2 {  
  4.   
  5.     public static void main(String[] args) {  
  6.   
  7.         StringBuffer a, b, c;  
  8.         a = new StringBuffer(“test”);  
  9.         b = a;  
  10.         c = b;  
  11.         String processA = processA(a);  
  12.         String processB = processB(b);  
  13.         String processC = processC(c);  
  14.         System.out.println(processA);  
  15.         System.out.println(processB);  
  16.         System.out.println(processC);  
  17.     }  
  18.       
  19.     static String processA(StringBuffer str){  
  20.         return str.append(“A”).toString();  
  21.     }  
  22.       
  23.     static String processB(StringBuffer str){  
  24.         return str.append(“B”).toString();  
  25.     }  
  26.       
  27.     static String processC(StringBuffer str){  
  28.         return str.append(“C”).toString();  
  29.     }  
  30.   
  31. }  
  32. //OUTPUT  
  33. // testA  
  34. //testAB  
  35. //testABC  
package _12_01字符串;

public class 为什么String要设计成不可变类2 {

    public static void main(String[] args) {

        StringBuffer a, b, c;
        a = new StringBuffer("test");
        b = a;
        c = b;
        String processA = processA(a);
        String processB = processB(b);
        String processC = processC(c);
        System.out.println(processA);
        System.out.println(processB);
        System.out.println(processC);
    }

    static String processA(StringBuffer str){
        return str.append("A").toString();
    }

    static String processB(StringBuffer str){
        return str.append("B").toString();
    }

    static String processC(StringBuffer str){
        return str.append("C").toString();
    }

}
//OUTPUT
// testA
//testAB
//testABC

能看出b=a,c=b;程序员的本意是希望变量是不变的。所以String不可变的安全性就体现在这里。实际上StringBuffer的作用就是起到了String的可变配套类角色。

示例2

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

  1. class Test{  
  2.     public static void main(String[] args){  
  3.         HashSet<StringBuilder> hs=new HashSet<StringBuilder>();  
  4.         StringBuilder sb1=new StringBuilder(“aaa”);  
  5.         StringBuilder sb2=new StringBuilder(“aaabbb”);  
  6.         hs.add(sb1);  
  7.         hs.add(sb2);    //这时候HashSet里是{“aaa”,”aaabbb”}  
  8.   
  9.         StringBuilder sb3=sb1;  
  10.         sb3.append(”bbb”);  //这时候HashSet里是{“aaabbb”,”aaabbb”}  
  11.         System.out.println(hs);  
  12.     }  
  13. }  
  14. //Output:  
  15. //[aaabbb, aaabbb]  
class Test{
    public static void main(String[] args){
        HashSet<StringBuilder> hs=new HashSet<StringBuilder>();
        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);
    }
}
//Output:
//[aaabbb, aaabbb]
StringBuilder型变量sb1sb2分别指向了堆内的字面量“aaa”“aaabbb”。把他们都插入一个HashSet。到这一步没问题。但如果后面我把变量sb3也指向sb1的地址,再改变sb3的值,因为StringBuilder没有不可变性的保护,sb3直接在原先“aaa”的地址上改。导致sb1的值也变了。这时候,HashSet上就出现了两个相等的键值“aaabbb”破坏了HashSet键值的唯一性。所以千万不要用可变类型做HashMapHashSet键值。

不可变性支持线程安全

还有一个大家都知道,就是在并发场景下,多个线程同时读一个资源,是不会引发竟态条件的。只有对资源做写操作才有危险。不可变对象不能被写,所以线程安全。

不可变性支持字符串常量池

最后别忘了String另外一个字符串常量池的属性。像下面这样字符串onetwo都用字面量“something”赋值。它们其实都指向同一个内存地址。

  1. String one = “someString”;  
  2. String two = ”someString”;  
String one = "someString";
String two = "someString";


这样在大量使用字符串的情况下,可以节省内存空间,提高效率。但之所以能实现这个特性,String的不可变性是最基本的一个必要条件。要是内存里字符串内容能改来改去,这么做就完全没有意义了。






总的来说:
1、由于String类不能被继承,所以就不会没修改,这就避免了因为继承引起的安全隐患,如果不设置为final,String类在Java中是经常用到的,如果能够重写、修改,那Java的优点之一,安全性将大打折扣。
2、String每次出现时都用final来修饰,这无疑会降低程序的执行效率,所以干脆直接将其设为final一提高效率。

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页