[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!

                
            
        
浙公网安备 33010602011771号