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

首先要理解final的用途,final修饰符可以修饰类、方法和变量。

  • final修饰类:被修饰类不可被继承。
  • final修饰方法:被修饰方法不可被重写。
  • final修饰变量:被修饰变量的引用不可被修改。

String为什么不可变?

翻开JDK源码,java.lang.String类如下所示:

public final class String
    implements java.io.Serializable, Comparable, CharSequence {
    // 用于存储字符串的值
    private final char value[];
    // 缓存字符串的 hash code
    private int hash; // Default to 0
    // ......其他内容
}
  • String类是用final修饰,这说明String不可被继承;

  • String类的关键底层实现成员value是一个char[],同样被final修饰,这说明value的引用不可被修改。
    注意:value的不可变,是指value的引用地址不可变,value所指向的char[]数组本体是可以改变的。

    final int[] value = {1, 2, 3, 4, 5};
    // value = {6, 7, 8, 9, 10};// 编译器报错,被final修饰的value,其引用地址不可变
    value[3] = 99;
    System.out.println(Arrays.toString(value));// [1, 2, 3, 99, 5],数组本体已被改变
    

    value只是数组本体在stack栈上的一个引用,数组本体存在于heap堆中。


  • 正因为数组本体仍然是可变的,故起作用的另外一个关键在于:private私有访问权限,不对外暴露内部成员字段。其作用比final还要大。

  • String类的另外一个成员hash,被设计用于缓存当前字符串的hash code,字符串初始化完成后,其hash code就被缓存下来,之后的获取不需要再重新计算。这就要求String类一定是不可变的,避免其被外界继承导致结构破坏的可能性。
    这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

所以,【String类被设计成final不可被继承,避免被外部继承破坏结构破坏】 + 【成员char[]被修饰为final不可被修改,同时被private修饰,不对外暴露】,这两部分共同保证了String的不可变。

不可变有什么好处?

  • 安全
    对比如下2段代码

      @Test
      public void test02() {
          HashSet set = new HashSet<>();
    
          String s1 = new String("aaa");
          String s2 = new String("aaabbb");
          set.add(s1);
          set.add(s2);// 此时HashSet内部为:{"aaa","aaabbb"}
    
          String s3 = s1;// 在此处变量s3的引用先指向s1
          s3 += "bbb";// 然后修改变量s3
    
          System.out.println(set);// 由于String的不可变性,HashSet内部仍为:{"aaa","aaabbb"}
      }
    
      @Test
      public void test03() {
          HashSet set = new HashSet<>();
    
          StringBuilder sb1 = new StringBuilder("aaa");
          StringBuilder sb2 = new StringBuilder("aaabbb");
          set.add(sb1);
          set.add(sb2);// 此时HashSet内部为:{"aaa","aaabbb"}
    
          StringBuilder sb3 = sb1;// 在此处变量sb3的引用先指向sb1
          sb3.append("bbb");// 然后修改变量sb3
    
          System.out.println(set);// 由于StringBuilder不具备不可变性的保护,sb3直接在sb1的引用对象上修改,导致HashSet内部变更为:{"aaabbb","aaabbb"},不仅如此,更严重的是还破坏了hashset键值的唯一性
      }
    

    StringBuilder类型变量sb1和sb2分别指向了堆内的字面量"aaa"和"aaabbb"。把他们都插入一个HashSet。到这一步没问题。但如果后面我把变量sb3也指向sb1的地址,再改变sb3的值,因为StringBuilder没有不可变性的保护,sb3直接在原先"aaa"的地址上改。导致sb1的值也变了。这时候,HashSet上就出现了两个相等的键值"aaabbb",破坏了HashSet键值的唯一性。
    所以:千万不要用可变类型做HashMap和HashSet键值

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

  • 高效
    String的不可变性决定其支持字符串常量池。字符串常量池可以缓存字符串,提高程序运行效率。

    像下面这样字符串s1和s2都用字面量"java"赋值。它们其实都指向同一个内存地址。

    String s1 = "java";
    String s2 = "java";
    

    这样在大量使用字符串的情况下,可以节省内存空间,提高效率。但之所以能实现这个特性,String的不可变性是最基本的一个必要条件。试想,若String没有不可变性保护,常量池中字符串内容能改来改去,则字符串常量池便失去任何意义。

你可能感兴趣的:(java中String类为什么要设计成final?)