String为什么是不可变的

Posted 写Bug的渣渣高

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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);
    

以上是关于String为什么是不可变的的主要内容,如果未能解决你的问题,请参考以下文章

Java中String为什么是不可变

为什么String是不可变的?StringStringBufferStringBuilder

为什么String是不可变的?StringStringBufferStringBuilder

Java中的String为什么是不可变的?

Java 中的 String 为什么是不可变的?(转)

java 集合类