查看原文
其他

由一篇博客引发的对Java String类的思考

GQB1226 该帐号已冻结 2022-09-09

引子

今天偶尔看到一篇博客说Java中String为什么是不可变的(immutable),貌似分析的头头是道,还拿出来源码振振有词的说的,看,就是因为它在声明类的时候是被final修饰的,然后就长篇大论final的东西

对于这类的博客实在不知道说啥好,一瓶水不满,半瓶水晃荡。

那么问题来了,String类为什么要被设计成不可变的,是怎么样设计为不可变的?

问题一,为什么要设计成不可变的,也就是不可变的好处,这个很明确,安全性和效率是两大方面,可以参照stackoverflow上这个问题为什么String类要被声明为final

今天我想重点说的是问题二:

String是怎么设计为不可变的

1. 不可变的是什么?

       String str = "hello word";
       str = "ni hao";
       System.out.println(str);
       //out:ni hao

              

       String str = "hello word";
       System.out.println(str);
//这里使用hashcode替代内存地址(不严谨)
       System.out.println("str的地址:"+ str.hashCode());
       str = "ni hao";
       System.out.println("str的地址:"+str.hashCode());
       System.out.println(str);
        //out:
        //hello word
       //str的地址:-1604693608
       //str的地址:-1047734607
       //ni hao

我们知道当我们创建一个String对象str时(String str = "hello word";),其在内存中的状态是这样的:

再执行str = "ni hao"时,就变成了这样的:

我们可以看到str实际上指向了一个新的地址,而不是在原内存地址上修改数据,这就是我们所说的不可变。

2. 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是怎么保证不变的。

  • String类是个final类,这就意味着String类不可被继承,不可被继承就不能带来熊孩子,在根源上防止熊孩子带来的破坏!

  • String类的主要字段char value数组也是被final修饰的

很多博客的分析就到这戛然而止了,但是这就完了吗?

很明显不是,value虽然是被final修饰了,但是这只是修饰了value,但是value是一个数组,这个数组是可变的如果这个数组改变了,那么string也就变了。数组的数据结构如下:

这可以看出value这个变量只不过是在栈上的一个引用,数组实际的内容是在堆上的,使用final修饰value只能限定栈里面这个value引用地址不可以变了,不能再指向其他地址,但是不能限制堆上的内容不可以改变。见下面这个例子:

       final char[] value = {'a','b','c'};
       //out:abc
       System.out.println(value);
       
       //error:Array initializer is not allowed here
       value = {'1','2', '3'};

       value[0] = '1';
       //out:1bc
       System.out.println(value);

上述代码在ide中直接会提示第3行错误,编译不会通过的,原因就是这里定义的value是final类型的,一旦初始化之后就不可以再次指向别的引用;但是也可以看到value指向的char数组是可以改变内容的,所以说String的不可变并不完全是由final修饰的value数组决定。

那是由什么决定的呢?

当然是由这个类代码的作者决定的,通读String类的源码的话,会发现后面所有String的方法都很小心的没有去修改这个数组的内容,也没有对外暴露成员,设置为private类型,再加上String类不可以被继承,避免被其他继承类修改,这样就完美实现了String的不可变。

好了,这里留下一个悬念,也是我当初在阅读String源码时候不解了好几天的地方,String构造方法中是这样的:

   /**
    * Initializes a newly created {@code String} object so that it represents
    * the same sequence of characters as the argument; in other words, the
    * newly created string is a copy of the argument string. Unless an
    * explicit copy of {@code original} is needed, use of this constructor is
    * unnecessary since Strings are immutable.
    *
    * @param original
    *         A {@code String}
    */
   public String(String original) {
       this.value = original.value;
       this.hash = original.hash;
  }


问题是String类中的value属性是被定义为private类型,那么这里为什么可以访问到original的value值的呢?

我当时可是郁闷了好一阵。。。。


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存