Java String不可变特性研究发现的问题

今天在研究Java中String的不可变性的时候,发现一个有趣的现象,直接上代码:

String str = "Hello String";
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char [])field.get(str);
value[9] = 'o';
System.out.println(str); // Hello Strong
System.out.println(str == "Hello String"); //true
System.out.println(str == "Hello Strong"); //false

上面这段代码逻辑很简单,就是将str原本指向的内容“Hello String”中,字符数组的第10位将字符“i”换成了字符“o”,这时str的输出的内容其实是Hello Strong,但是我直接把str与常量Hello String进行==比较的时候发现返回的竟然是true。
我了解到的是Java中String存在一个常量池的概念,很明显Hello String作为字面量直接赋值其实就是在常量池中缓存了。
问题:我这里很明显将字符串内部的字符数组中的某一位换掉了,但是在常量池判断是,它仍是将修改的那段内容判断为Hello String常量了,即使str这时内容已经变成了Hello Strong,那么在常量池中是如何实现存储的,我的猜测是:是不是在字符串装入常量池的时候,有一个类似于HashMap的那种结构,key就是放入的常量,value就是指向的具体字符数组区域,从而出现了现在这种,即使我修改了字符数组,它的key仍然是之前的字符串常量这种效果,想不通,求哪位大佬解答,不胜感激~~

阅读 3k
5 个回答

写在正文之前的话,本来下面的回答2天前就写好准备提交。。。结果一提交。。。emmm。。。害。。。正碰上。。。o(╯□╰)o


题主,你好,我来分享哈我的看法,做个补充,希望对你有所帮助,

其实正如_TNT_所说,字符串常量池确实就是一个hash表,也确实key为字符串做hash之后得到的,而value就是String对象的地址

所以只要你把字符串常量池理解String对象的一个引用集合(collection)就可以了

(注:这里只是方便理解所以说是引用的集合,但实际上引用只会出现在栈上,而字符串常量池现在在堆里,里面只有地址,没有引用,所以下面第二张图画的是地址)

注意,这里是对象引用,不是字符串本身,或许很多人听着字符串常量池就会认为里面全是字符串,类似下面这个样子
image.png

但是其实是忘了String虽然有点特别,但是本质还是一个引用类型嘛,跟我们平常写的类都差不多,只是官方这里做了一个缓存罢了,所以其实字符串常量池是一个String对象的一个引用集合,类似下面这样
image.png

牢记字符串常量池里存的只是String的对象引用就行了


所以现在回过头看看题主的例子:

第一行String str = "Hello String";其实就是表明,变量str本身就是String对象("Hello String")的引用,并且这个引用已经放到了字符串常量池里,且和字符串"Hello String"做了hash绑定

先不用看接下来你怎么把"Hello String"变为"Hello Strong",因为你后面的判断是基于==的,这个是比较引用是否相等的,跟值没啥关系

因此

System.out.println(str == "Hello String"); //true

就很好理解了,右边直接使用了字面量"Hello String",那就会去字符串常量池里找,诶,找到了一个,当然这个也和str相等

System.out.println(str == "Hello Strong");//false

也很好理解了嘛,右边直接使用了字面量"Hello Strong",同样也会字符串常量池里找,找不到,那就新生成一个String对象,并且把其地址和"Hello Strong"hash绑定起来放到字符串常量池里,那两个地址肯定不一样,所以返回false

如果觉得我说明的还不太清楚,我再举个例子,比如下面这个代码

String str1 = new String("abc");

如果我画图的话,会这样表述
image.png


最后我们说说微凉提到的String类的intern方法

这个方法其实就是根据当前String对象的字符串值去字符串常量池里找,看能不能找到已经存在的String对象引用,有的话,就返回,没有就把该String对象的引用hash进字符串常量池里绑定起来

微凉觉得懵逼的3个true其实原理都差不多了,比如:

System.out.println("Hello Strong".intern() == "Hello String".intern()); //true

左边嘛"Hello Strong".intern()那这肯定返回的是字符串常量池里根据"Hello Strong"字符串算出的hash值其对应引用

右边呢?"Hello String".intern()不难道是根据"Hello String"字符串算出的hash值其对应引用么?显然不是

谁调用intern()方法?

  1. 那可不是字符串去调用啊,是对象调用的嘛
  2. 对象怎么体现?那肯定根据引用找到的对象
  3. 这里的引用是谁?那肯定是根据字面量"Hello String"找到的引用
  4. "Hello String"找到的引用肯定是从字符串常量池里找到的引用,这个引用其实跟上面的变量str是一样的,对吧

"Hello String".intern()其实就可以写成str.intern(),结合之前对intern方法的描述,str.intern()也就是表示,用String对象str的字符串值去字符串常量池里找,找得到就返回

诶~ 通了吧,这里str的字符串值可不再是"Hello String"了啊,而是被古灵精怪的题主改为"Hello Strong"了啊o(╯□╰)o,用"Hello Strong"在字符串常量池里找到的引用肯定跟左边的一样啊

以上仅是个人观点,仅供参考哈~<( ̄ˇ ̄)/

字符串常量池是一个哈希表,但是好像和代码中的现象没有关系…?

代码中所有的字符串字面量,以及编译器能拼出来的字符串,都会进常量池,官方文档。内容相同的字符串都是同一个对象,修改了其中一个,其他的也会一起被修改,用==判断返回true


至于另外一个回答(分明是问题)里3的个true的现象也是差不多的道理:

  • 全程只有两个字符串对象:"Hello String""Hello Strong"
  • 在修改之后,所有的"Hello String"的内容都已经变成了"Hello Strong"
  • intern方法使用字符串的实际内容去字符串常量池里找字符串常量,返回的都是同一个"Hello Strong"对象
  • 额外说一句,平时的"Hello".intern()intern是多余的,因为字面量本来就在常量池里,无需intern

就是你猜测的那样

        String str = "Hello String";
        Field field = String.class.getDeclaredField("value");
        field.setAccessible(true);
        char[] value = (char [])field.get(str);
        value[9] = 'o';
        System.out.println(str.intern()); // Hello Strong
        System.out.println("Hello Strong".intern() == "Hello String".intern()); //true
        System.out.println(str.intern() == "Hello String".intern()); //true
        System.out.println(str.intern() == "Hello Strong".intern()); //true

3个true,说实话,我也有点懵逼,等待解答

其实题主在你的测试代码最后再加一句就容易明白了:

System.out.println("Hello String");

结果输出的是"Hello Strong"。

为什么呢?因为题主你把"Hello String"所指向的实际对象的值改成"Hello Strong"了。

本来String是不可变对象,值是改不了的,但题主很有想法,很好的尝试。

推荐问题
宣传栏