你真的知道String不可变的原因吗?答案在这


前言

以往我们对String不可变的认识大多都停留在内部有一个final修饰的数组,但是这个说法不太严谨,这篇文章来揭示一下String到底为什么不可变。


一、String为什么不可变

String 不可变的表现就是当我们试图对一个已有的对象 “abcd” 赋值为 “abcde”,String 会新创建一个对象。

public class Test {
    public static void main(String[] args) {
        String str="abcd";
        System.out.println(str.hashCode());
        str="abcde";
        System.out.println(str.hashCode());
    }
}

从下图我们看出str的哈希值发生了变化,即str指向了其他的地址。
在这里插入图片描述
即原来的"abcd"没有发生变化,这就是String不可变的体现。
在这里插入图片描述
为什么String不可变呢?下面给出一种通用的答案。
String内部用 final 修饰 char 数组,这个数组无法被修改。
在这里插入图片描述
这个说法确实说的通,但不太严谨。严谨的说法应该是什么呢?
答:严谨的说法应该是String的不可变不仅仅体现在final上面。

这个数组无法被修改仅仅是指引用地址不可被修改(也就是这个叫 value 的引用地址不可变,编译器不允许我们把 value 指向堆中的另一个地址),并非是value[]这个数组的内容不可修改。

下图尝试将final修饰的引用value指向其他的数组,编译器直接报错,也就是上面说的引用地址不可变。
在这里插入图片描述
但是如果我们改变value数组本身的内容,这样操作是可行的。

public class Test {
    public static void main(String[] args) {
        final int []value={1,2,3};
        value[0]=5;
        System.out.println(Arrays.toString(value)); //输出[5, 2, 3]
    }
}

那看到这里大家可能有点迷惑了,这value数组如果是可变的,那不是乱套了吗?

接下来我们再来回顾一下上面说的严谨的说法:String的不可变不仅仅体现在final上面。
1.value数组是用private修饰的,且String外界不提供修改这个数组的方法,所以初始化之后我们没法继续改变value数组的内容。
2.String 类被 final 修饰的,也就是不可继承,避免被他人继承后破坏。
3.Java 作者在 String 的所有方法里面,都很小心地避免去修改了 char 数组中的数据,涉及到对 char 数组中数据进行修改的操作全部都会重新创建一个 String 对象。你可以随便翻个源码看看来验证这个说法,比如 substring 方法:
在这里插入图片描述
总结:虽然理论上数组内容可变,但是你没有办法改变,这也是不可变的一种体现。


二、为什么要设计成不可变的

1.字符串常量池的需要
字符串常量池的定义:大量频繁的创建字符串,将会极大程度的影响程序的性能。为此,JVM 为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化:

1.为字符串开辟了一个字符串常量池 String Pool,可以理解为缓存区
2.创建字符串常量时,首先检查字符串常量池中是否存在该字符串
3.若字符串常量池中存在该字符串,则直接返回该引用实例,无需重新实例化;
若不存在,则实例化该字符串并放入池中。

如下面的代码所示,堆内存中只会创建一个 String 对象:

String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2) // true 

假设 String 允许被改变,那如果我们修改了 str2 的内容为world,那么 str1 也会被修改,这不是我们想要看见的结果。

2.网络连接地址URL,文件路径path通常情况下都是以String类型保存, 假若String不是固定不变的,将会引起各种安全隐患。

3.在Java程序中String类型是使用最多的,这就牵扯到大量的增删改查,每次增删改差之前其实jvm需要检查一下这个String对象的安全性,就是通过hashcode,当设计成不可变对象时候,就保证了每次增删改查的hashcode的唯一性,也就可以放心的操作。


三、如何让String可变

当然,如果你真的很想改变value数组的内容,你也不是不可以。value是私有属性,你只需要一种方法访问类的私有属性即可。

使用反射可以直接修改value数组中的内容,当然建议不要这样做,毕竟JAVA设计者都把String设计成不可变了,你还硬要改变它,这不是为难自己么!

public class Test {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        String str = "HelloWorld";
        //获取类的class对象
        Class strClass = str.getClass();
        //获取class对象的私有属性
        Field value = strClass.getDeclaredField("value");
        //设置可进入
        value.setAccessible(true);
        //得到value数组
        char[] change = (char[]) value.get(str);
        //改变第一个位置的元素值
        change[0] = 'I';
        System.out.println(str);
    }
}

费了半天力气,可以看到我们的value数组已经成功不情愿的被改变了。
在这里插入图片描述


总结

如果final修饰的是基本数据类型,则代表其值不可变(基本数据类型存储的就是值),如果final修饰的是引用类型,指的是引用的地址不可变(即必须指向该对象),但是该对象自身内容是可以变得。String不可变不仅仅只是因为final关键字,需要全方面来分析。


  • 14
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JinziH Never Give Up

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值