String为什么是不可变的?

我们最常见的面试题:String为什么是不可变的?

  • String在我们的开发过程中是最常见不过的了,但是你真正了解它么?

  • 上源码,源码中就能找到答案

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[]; //String的本质就是一个char[]
    
        /** Cache the hash code for the string */
        //在创建一个字符串的同时会生成这个字符串对应的hash值 之后的获取不需要再重新计算 提高效率!
        private int hash; // Default to 0 
        //................
    }
    
  • 首先,String类是用final修饰,这说明String不可被继承;

  • String类的关键底层实现成员value是一个char[],同样被final修饰,这说明value的引用不可被修改。

  • 注意:value的不可变,是指value的引用地址不可变value所指向的char[]数组本体是可以改变的。value只是数组本体在stack栈上的一个引用,数组本体存在于heap堆中。

  • 起作用的另外一个关键在于:private私有访问权限,不对外暴露内部成员字段。关键原因:private

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

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

为什么要设计成不可变?不可变有什么好处?

  • 安全

    • 看一点就可以了。HashMap,HashSet 如果String可变那不就GG了,破坏了HashMap,HashSet 键值唯一性。
  • 高效

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

      //它们其实都指向同一个内存地址。
      String str1 = "java";
      String str2 = "java";
      
    • 这样在大量使用字符串的情况下,可以节省内存空间,提高效率。但之所以能实现这个特性,String的不可变性是最基本的一个必要条件。试想,如果String没有不可变性保护,常量池中字符串内容能改来改去,那字符串常量池还有什么意义呢?

String、StringBuilder、StringBuffer对比

  • 共同点:

    • 都是final类,都不允许被继承
  • 不同点:

    • 它们三者的主要区别表现在运行速度线程安全这两方面

    • 运行速度

      • StringBuilder > StringBuffer > String

      • 原因:

        • StringBuilder ,StringBuffer 都提供了对字符串操作的append,insert方法(这也算是两者的共同点)

        • 而因为String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。

        • 举例

          在这里插入图片描述

          • 运行这段代码会发现先输出“123”,然后又输出“12345”,好像是str这个对象被更改了,其实,这只是一种假象罢了,JVM对于这几行代码是这样处理的,首先创建一个String对象str,并把“123”赋值给str,然后在第三行中,其实JVM又创建了一个新的对象也名为str,然后再把原来的str的值和“45”加起来再赋值给新的str,而原来的str就会被JVM的垃圾回收机制(GC)给回收掉了,所以,str实际上并没有被更改,也就是前面说的String对象一旦创建之后就不可更改了。所以,Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。而StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以速度相比较之下要比String快很多。
    • 线程安全

      • String天生安全,具体原因见上述

      • StringBuilder是不安全的,而StringBuffer是安全的。直接原因就是StringBuffer中很多方法带有synchronized(同步锁)关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder(因为没有synchronized同步锁,不用等待,所以比StringBuffer更快!)。

      • StringBuffer部分源码

        //===================省略代码================
        @Override
        public synchronized StringBuffer append(Object obj) {
            toStringCache = null;
            super.append(String.valueOf(obj));
            return this;
        }
        
        @Override
        public synchronized StringBuffer append(String str) {
            toStringCache = null;
            super.append(str);
            return this;
        }
        //===================省略代码================
        
    • 总结

      • String:适用于少量的字符串操作的情况
      • StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
      • StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况

版权声明:本文为weixin_46157585原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。