String为什么是不可变的

本文概述:
本文讲了一些其他博主的说的不太准确的地方. 然后会进行一些简单的知识点联系
并且会谈及 hashCode 方法
并且会使用一个小实验去模拟 String 如果可变会变得怎么样

本文思路:
先介绍一下 String 的不可变性, 然后拓展到为什么需要不可变性 (这很重要, 真的, 熟练一个知识不是知道怎么做, 而是知道为什么这么做,本文不会详细的跟你讲清楚所有的铺垫, 但是如果你稍微懂一些, 你会发现这个文章很有意思)

不变性

关于 String 的不变性, 可能都听过很多遍了. 但是实际上有些博主说的并不准确.

可能大家都知道了,String 是被 final 修饰的类, 同时底层存储字符的 char[] 数组, 也是被 final 修饰的.

final 修饰有什么作用?

  • 修饰类, 类不可以继承
  • 修饰变量, 变量的引用不能改变
  • 修饰方法, 方法不可用被子类重写

final 为什么会让 String 具有不可变性呢?

网上很多博主说, 是因为 final 具有不可变性, 我们来看看代码

    final char[] value = {'a','b'};
    /**
     * 测试 final char[] 是否具有不可变性
     *
     * @return
     * @author ggzx
     * @create 2023/3/10 9:16
     */
    @Test
    public void testString(){
        value[0] = 'c';
        for (char c : value) {
            System.out.println(c);
        }
    }

结果:
c
b

可以看出来, 我们可以直接更改 value 中的值. 所以不能说 String 的不可变是因为 final 修饰了 char[]. 至少不能只说是 char[]导致的

实际原因分析:

如果一个 final 数组是 char[] 类型的, 我们实际上是可以去修改的.
但是如果这个数组在类中并且是 private 修饰, 我们外部无法去修改.==并且 String 没有提供一个直接修改 char[] 内部值的方法, 也没有暴露内部的 char[] ==. 所以说, 我们无法获得 char[] value ,就无修改其值. 并且 private 属性导致我们无法通过继承来获得父类的属性.

但是我们也可以通过反射来获取 char[] 的对象, 然后直接修改;

    /**
     * 测试通过反射来修改 String 的值
     * 
     * @return 
     * @author ggzx
     * @create 2023/3/10 18:50
     */
    @Test
    public void testStringReflection() throws NoSuchFieldException, IllegalAccessException {
        System.out.println("-------testStringReflection()------");
        String a = "abc";
        Class<? extends String> strClass = a.getClass();
        Field value = strClass.getDeclaredField("value");
        // 既可以破坏,private的私有性
        value.setAccessible(true);
        // 也可以破坏final的不可修改性
        value.set(a, new char[]{'a', 'b', 'c', 'd'});
        System.out.println(" 修改后的值: " + a);
    }

拓展: 不可变性的好处

  • 不存在线程安全问题:

线程安全问题建立在一个共享资源问题之上, 而 String 无法修改, 何来的线程安全问题.
如果你有点懵,String 对象虽然可以是线程共享的, 但是呢, 我们"无法去修改他", 修改它, 实际上只是改变了其 char[] 的引用, 而不是改变对象本身.

![[Pasted image 20230310190200.png]]

  • 不需要重新计算 hash 值:

这一点可能很多博主不一定讲过.
Sting 中缓存了 hashCode 属性, 大家应该知道 hashCode 属性是由对象的地址通过某些计算方法映射出来的, 映射到 int 范围内. 但是我们经常都会去重写这个方法, 比如说 String 就有其自己的生成 hashCode 方法. 其根据的是数组中的值来计算, 假如 String 是可变的, 那么每次更改都要重新计算哈希.

    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

很有趣的知识

注: 这里不谈及如果让 String 变成不可变的, 我们该如何操作, 是否具有可行性, 如果有方案, 是否比较完善, 仅仅是作为一个延申问题思考.

欸欸欸, 重新计算哈希, 不知道你有没有什么印象, 这个不是 hashMap 中的吗. 你想想一个 String 作为 key, 如果对象可变, 在外面修改 String, 那 Key 不也跟着变化了吗.
思考思考, 我们如何去根据 Key 来判断键值对存储在 hashMap 中的位置的?
首先 hashCode ^ hashCode >>> 16
然后 index = (n - 1) & hash ;
如果 String 具有可变性,如果 hashCode 变了, 那么必然就需要改变其作为 key 在 HashMap 中的位置啊, 我们在外面不经意的一改, 就让 HashMap 变得不靠谱了, 这合适嘛.

我在写到这里的时候, 倒是有一个小想法.
能不能模拟一下,String 如果具有不可变性, 那么会变得怎么样, 然后存放到 HashMap 中作为 Key 会怎么样

我猜测一下结果:
如果 Key 是可变的, 并且 hashCode 方法基于其 value 的内容改变而改变. 那么我如果加入一个对象, 然后在外部修改对象, 那么内部的 hashCode 方法也会改变.

如何实验:
创建一个类, 然后使用 String 的 hashCode 方法, 然后模拟一个 final 的 char[] .并且让该数组能通过 get ()方法在外部获取到并且修改.

理论建立, 实验开始
(先带你看看我实验的时候出错的地方, 其实挺有意思的)

下面这个是我模拟的可变的 “String”, 我现在就提前告诉你, 我直接粘贴 String. hashCode 方法导致实验出错了, 不知道你能不能看出来错误.

// 模拟
public class Component {
    final char[] value;
	// 一定要注意这个属性,这个属性 String 里面也有
	// 是为了缓存 hasCode方法的,也就是说很多时候我们不会
	// 直接使用hashCode方法,而是把他缓存下来
    int hash;

    public Component(char[] value) {
        this.value = value;
    }

    // 直接粘贴 String 的 hashCode 方法
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0)
            char[] val = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

    public char[] getValue() {
        return value;
    }
}

    /**
     * 这里测试 HashMap 中,如果在外部修改 key ,是否会影响 HashMap
     *
     * @return
     * @author ggzx
     * @create 2023/3/10 19:28
     */
    @Test
    public void testHashMapKey(){
        HashMap<Component,String> hashMap = new HashMap<>();
        Component component = new Component(new char[]{'a','b','c'});
        // 把对象加入
        hashMap.put(component,"ggzx");
        System.out.println(component.hashCode());
        System.out.println(hashMap.get(component));
        // 然后测试如果在外部修改,是否还能成功获取
        char[] value = component.getValue();
        // 这里修改其中的一个值即可
        value[0] = 'p';
        System.out.println(component.value);
        System.out.println(component.hashCode());
        System.out.println(hashMap.get(component));
    }

先来回顾一下我的推测, 自定义对象作为 Hash Map 的 Key, 如果 hashCode 和对象的内容有关, 我们在外部改变内容.

ggzx
pbc
96354
ggzx

大眼一瞪, 发现不对, 我们修改了内容, hashCode () 应该改变了, 但是却能正常获取.
问题在于:

我们平常可能只了解到,String 的 hashCode 和其的 value 关系, 但是却不知道 String 的 hashCode 方法只能执行一次. 这是因为
if (h == 0 && value.length > 0)
这一行, 只有第一次才会执行 hashCode 方法去计算.
不过还有一个原因, 是我后面看源码才恍然大悟, tab[i] = newNode (hash, key, value, null);
![[Pasted image 20230310204344.png]]
也就是说, 外部的对象存放进来之后,Node 中的 key 指向的是外部引用的

到了这里, 你可能觉得我说这么多, 那不是废话吗. 你说的这个实验有啥用, 学习知识不要当作去记住他的特性, 但却不去尝试.
我们的出发点是对的, 我们实际上是测试一个自定义对象, 其 hashCode 是根据其内容生成的, 我们模拟的就是 String ,我们想看加入一个对象, 在外部修改内容, 是否能成功从 hashMap 中取出来 (因为我们的惯性思维认为 String 的 hashCode 方法和 value 有关系).
但是结果可能不尽人意, 但是呢, 就没有别的发现了吗?

思维再次提升, 判断 == 0 有啥用呢?

  • 如果不加上这个条件, 那么 hashCode 方法就只会调用一次, 而
  • 即使通过反射来修改内容, String 内部存储的 hash 值不改变, 也不会影响到该 String 当作 Key, 因为 hashCode 方法只调用了一次, value 改变, hashCode 也不改变.

再想想再想想 , 这个对我们构建一个自定义的对象充当 Hash Map 的 Key 有没有什么参考价值呢?
起码, 我们了解到, 如果使用自定义对象作为 Key, 那么就要注意, 是否存储在外部能直接修改内部的值并且会导致 hashCode 改变的任何操作.

比如说我们回顾一下 String 能不能在外部修改

因为不可变性, 无法修改

比如说,Integer 方法

Integer 方法无法直接修改. 直接修改, 也是一个新的对象, 不会影响到存入到 HashMap 中的Key

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

渣渣高不会写Java

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

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

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

打赏作者

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

抵扣说明:

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

余额充值