zoukankan      html  css  js  c++  java
  • Java中String类为什么被设计为final?

    Java中String类为什么被设计为final

      首先,String是引用类型,也就是每个字符串都是一个String实例。通过源码可以看到String底层维护了一个byte数组:private final byte[] value;(JDK9中为byte数组,并非网上所说的char数组)。虽然该数组被修饰为final,但这并不能保证数组的数据不会变化,因此还需要声明为private防止被其他类修改数据。
      被final修饰的类不能被继承,也就是不能有子类。那么为什么要把String设计为不能被继承呢?简单来说有两点:安全效率

    安全

      要知道String是一个非常非常基础的类,用处超级广泛,各种各样的类基本都使用到了字符串。
      假设String类可以被继承,现在有一个方法method,该方法的参数为String类型,并且该方法利用到了字符串的长度特性:

    public int method(String s){
        //do something
        int a = s.length() + 1;
        
        return a;
    }
    

    我们设计出一个String的子类MyString,并重写了其长度方法:

    public class MyString{
        @Override
        public int length(){
            return 0;
        }
    }
    

      基于Java的多态特性,当我们把MyString的实例作为参数传入method()方法时,编译器是不会报错的。但是我们的运行结果则完全错误,这会造成非常严重的后果。

    MyString myString = new MyString();
    method(myString);//此时编译并不会报错,但是运行结果是完全错误的。
    

      相对于每次使用字符串的时候使用final修饰,直接把String类定义为final更为安全,效率也更高。并且,整个类声明为final之后,如果有一个String的引用,则它引用的一定是String对象,而不会是其他类的对象(泛型允许引用子类)。防止世界被熊孩子破坏2333

      除了由多态引起的安全问题,还有引用类型本身的问题。
      比如现在有两个方法,appendStr负责在不可变的String参数后添加“bbb”并返回,appendSb负责在可变的StringBuilder后添加“bbb”并返回。

    public static String appendStr(String s){
        s = s + "bbb";
        return s;
    }
    
    public static StringBuilder appendSb(StringBuilder sb){
        sb.append("bbb");
        return sb;
    }
    
    public static void main(String[] args) {
        //String做参数
        String str = new String("aaa");
        String newStr = appendStr(str);
        System.out.println("String aaa -> " + str.toString());
    
        //StringBuilder做参数
        StringBuilder sb = new StringBuilder("aaa");
        StringBuilder newSb = appendSb(sb);
        System.out.println("StringBuilder aaa -> " + newSb.toString());
    }
    

    但实际输出结果却是:

    String aaa -> aaa
    StringBuilder aaa -> aaabbb
    

      如果程序员不小心像上面例子里,直接在传进来的参数上加"bbb",因为Java对象参数传的是引用,所以可变的的StringBuffer参数就被改变了。可以看到变量sb在Test.appendSb(sb)操作之后,就变成了"aaabbb"。有的时候这可能不是程序员的本意。所以String不可变的安全性就体现在这里。
      再看下面这个HashSet用StringBuilder做元素的场景,问题就更严重了,而且更隐蔽。

    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);//输出:[aaabbb, aaabbb]
    }
    

      这就破坏了HashSet键的唯一性,因此千万不要使用可变类型做HashMap和HashSet的键值。(不可变的字符串则非常适合作为键)

      除了上述两种问题,不可变的字符串还可以保证多线程时的线程安全问题。多线程时,只有读操作一般不会引发线程安全问题,当读写同时存在时便容易引发安全问题。当字符串不可变时也就不能写,当然不会引发线程问题。

    效率

      基于字符串的不可变,才能有字符串常量池这一特性。字符串常量池的诞生是为了提升效率和减少内存分配。可以说我们编程有百分之八十的时间在处理字符串,而处理的字符串中有很大概率会出现重复的情况。正因为String的不可变性,常量池很容易被管理和优化。
      并且1.7之前,字符串常量池在方法区,1.7之后在堆内存中,并且不仅仅可以存储对象,还可以存储对象的引用:

    String s = new String("A") + new String("B");//此时常量池存在"A"、"B",但是不存在"AB";堆中存在"A"、"B"、"AB",并且s指向"AB"
    s.intern();//1.7之后这里加入的是对象s的引用,而非直接保存"AB"字符串
    //intern用来返回常量池中的某字符串,如果常量池中已经存在该字符串,则直接返回常量池中该对象的引用。否则,在常量池中加入该对象,然后 返回引用。
    

      对于什么时候会在常量池存储字符串对象:

    1. 显示调用String的intern方法的时候,例如上例。
    2. 直接声明字符串字面常量的时候,例如: String a = "aaa";
    3. 直接new String("A")方法的参数使用常量的时候
    4. 字符串直接常量相加的时候,例如: String c = "aa" + "bb"; 其中的aa/bb只要有任何一个不是字符串字面常量形式,都不会在常量池生成"aabb". 且此时jvm做了优化,不会同时生成"aa"和"bb"在字符串常量池中

    顺便说一句,Integer、Long、Double……这几个包装类也是final的~

    参考

  • 相关阅读:
    Bugly和dispatch_once Crash
    IQKeyboardManager
    Storyboard References
    Book
    Git管理
    iOS开发之RunLoop--转
    H264之PPS、SPS了解
    iOS之UI设置随记
    使用 github 本地项目上传到github上 步骤
    spring中自定义注解
  • 原文地址:https://www.cnblogs.com/lixin-link/p/11085029.html
Copyright © 2011-2022 走看看