为什么Java String类的对象不可变
前言
学习 Java 的朋友对 Java 的 字符串都不陌生,字符串广泛应用在 Java 编程中,在 Java 中字符串属于对象,Java 提供了 String 类来创建和操作字符串。而且,Java 还规定了字符串是不可变的,也就是说,一旦一个字符串对象在 Java 中被创建,那么在死亡之前,它都至始至终保持着同一个状态。
需要理解
- 什么是不可变?
- 不可变即状态不可变。
- String 对象是什么数据类型?
- String 对象,也就是字符串,属于引用数据类型
- 引用数据类型和基本数据类型有什么区别?
- 引用数据类型中存的是被引用的数据的内存地址,也就是索引数据;基本数据类型,其值是在内存中开辟出来的空间所存放在其中的值(可以认为空间名即为变量名)。
- final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。final 修饰引用型数据,只保证这个引用变量所引用的地址不会改变,即一直引用同一个对象,但这个对象本身可以发生改变。例如某个指向数组的 final 引用,它必须从此至终指向初始化时指向的数组,但是这个数组的内容完全可以改变。
String 对象的创建:使用new关键字时,将在堆中开辟一个新的空间,此时字符串对象中存放的就是这个空间的地址。直接初始化的 String 对象,则是在方法区的字符串池中。
内存图解
首先我们来看一下String类的两个主要成员变量,其中value指向的是一个字符串数组,字符串中的字符就是用这个value变量存储起来的,并且用final修饰,private也禁止了外界对它的访问。也就是说value一旦赋予初始值之后,value指向的地址就不能再改变了。虽然value指向的数组是可以改变的,但是String也没有提供相应的方法让我们去修改value指向的数组的元素。同时,private使得value的地址值不会被外界访问到,自然也就无从下手来修改该对象内存放的内容啦!
但是,新手可能会被产生的一些障眼法蒙蔽:
//创建字符串对象
String s1 = "str";
System.out.println(s1);
//s1指向的内存地址
System.out.println("s1的内存地址:"+System.identityHashCode(s1));
String s2 = s1;
s1 = "str2";
System.out.println(s2);
//s2的内存地址和s1的相同
System.out.println("s2的内存地址:"+System.identityHashCode(s2));
//s1的内存地址已经改变了
System.out.println("s1的内存地址:"+System.identityHashCode(s1));
System.out.println(s1);
String s3 = new String();
s3 = "str3";
System.out.println("s3的内存地址:"+System.identityHashCode(s3));
System.out.println(s3);
[
s1被赋值"str2"后,原来字符常量池中的"str"状态并没有发生变化,而是重新创建了一个"str2"对象,此时,s1所指向的内存引用变量也不在是0X001,而是新的字符串对象的内存变量。
为什么 String 要设计得使其对象不可变
因为 String 对象缓存在 String 池中,由于缓存的字符串在多个操作者之间共享,因此始终存在风险,其中一个操作者的操作会影响所有其他操作者。
例如,如果一段代码将 “Hello” 的值更改为 “hello”,则所有其他操作者操作的String值将不会是自己想要的值,因此通过使 String 类不可变来避免这种风险。
同时,String 是 final 的,因此没有人可以通过扩展和覆盖行为来破坏 String 类的不变性、缓存、散列值的计算等。String 类不可变的另一个原因可能是由于 HashMap
。
对于键值来说,不可变性是非常的重要,保证用它们检索存储在 HashMap 中的值对象的准确性。由于 HashMap 的工作原理是散列,因此需要具有相同的key才能正常运行。如果在插入后修改了 String “key” 的内容,那么在插入和检索时就会生成两个不同的哈希码,造成 Map 中的值对象丢失。
String 真的真的真的 "不可变 " 吗?
虽然value是final修饰的,只是说明value不能再重新指向其他的引用。但是value指向的数组可以改变,所以我们如果真的想要改变value数组的内容党的话,可以通过反射得到String对象中的value属性,并且通过获得的value引用改变数组的结构。
public static void main(String[] args) throws Exception {
String str = "Hello, World!";
System.out.println("修改前的str:" + str);
System.out.println("修改前的str内存地址:" + System.identityHashCode(str));
// 获取String类中value字段
Field valueField = String.class.getDeclaredField("value");
// 禁用Java语言访问检查,以便我们可以修改value字段
valueField.setAccessible(true);
// 获取str对象中value字段的值
char[] value = (char[]) valueField.get(str);
// 更改字符串中的内容
value[7] = 'J';
value[8] = 'a';
value[9] = 'v';
value[10] = 'a';
value[11] = '!';
value[12] = '\0';
System.out.println("修改后的str:" + str);
System.out.println("修改前的str内存地址:" + System.identityHashCode(str));
}
运行结果:
修改前的str:Hello, World!
修改前的str内存地址:460141958
修改后的str:Hello, Java!
修改前的str内存地址:460141958