[Java基础]String 为什么是不可变的?

为什么String要设计成不可变的

  • 线程安全:不可变对象天生就是线程安全的:因为不可变对象不能被改变,所以他们可以自由地在多个线程之间共享。不需要任何同步处理。

  • hashmap需要:

    • 加快字符串处理速度由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。String重写了hashcode方法,所以如果string是可变的,那就要频繁重新计算hashcode
  • 字符串池:在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。假设现在我们有两个字符串指向了常量池中的同一个字符串常量,如果字符串是可以更改的,那么其中一个对字符串进行了更改,另一个字符串也会变化,这就违背了常量池的概念。

  • 避免安全问题:在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。

String 如何实现不可变

关于这个问题,网上有人说,是因为String类被写成final或者String中的成员变量value数组被写成final,但其实并不是,下面做一个实验

public final class MyString {
    public final char[] value = {'z'};
}

首先我们定义了一个类Mystring,并且类和成员变量都被设置成final

public class Main {
    public static void main(String[] args) {
        MyString myString = new MyString();
        System.out.println(myString.value);
        myString.value[0] = 'a';
        System.out.println(myString.value);
        myString.value[0] = 'b';
        System.out.println(myString.value);
    }
}

执行结果

z
a
b

这说明仅仅把类和成员变量设置成final无法实现不可变,为了实现不可变我们还需要把这个成员变量设置成private不可见。

继续我们的实验

public final class MyString {
    private final char[] value = {'z'};
}

这时再去执行上面的main函数,有如下结果,

E:\MIT6.830\Test_7\src\Main.java:12:36
java: value 在 MyString 中是 private 访问控制

这时候我们无法直接修改value的值,但是这就可以了吗?当然还没有

为了保证string的不可变我们还要继续做到以下几点

  • 首先设置内部成员变量的访问修饰符为private,这样就无法在类的外部直接访问到这个成员变量
  • 其次我们必须保证String类不提供成员方法去修改value,String的成员方法不是直接修改value而是通过新建一个String,并且把旧的string中的值复制到新的string对象中去。
  • 在value上加final保证这个数组的引用不会被修改而指向另一个数组
  • 在String上加final保证String没有子类,因为子类可能提供方法去修改value,子类可以赋值给父类引用进而破坏String的不可变特性

保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

让我们来看看string提供的一些修改方法的源码

public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
}


public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }

public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);
            }
        }
        return this;
    }

可以看到这些方法都不是直接操value数组,而是新生成了一个string,把新的内容赋给这个新的string。

String真就不可改变吗

答案是否定的。

我们可以利用反射机制改变value

public static void main(String[] args) {
        String str = "aaa";
        System.out.println("str = " + str + "\t" + "hashcode=" + str.hashCode());
        try {
            Field valueField = String.class.getDeclaredField("value");
            //暴力反射!
            valueField.setAccessible(true);
            byte[] valueCharArr = (byte[]) valueField.get(str);
            //改变!
            valueCharArr[0] = 'b';
            System.out.println("str = " + str + "\t" + "hashcode=" + str.hashCode());
        } catch (NoSuchFieldException | SecurityException
                | IllegalArgumentException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

结果:我们改变了value!

作者:Esofar

出处:https://www.cnblogs.com/DCFV/p/18331239

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   Duancf  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
点击右上角即可分享
微信分享提示