昨天面试的时候,面试官问我String的不可变性,我回答的有点糟糕,赶紧查资料总结一下以备忘!
一、原理
1、不变模式(不可变对象)
在并行软件开发过程中,同步操作似乎是必不可少的。当多线程对同一个对象进行读写操作时,为了保证对象数据的一致性和正确性,有必要对对象进行同步。而同步操作对系统性能是相当的损耗。为了能尽可能的去除这些同步操作,提高并行程序性能,可以使用一种不可改变的对象,依靠对象的不变性,可以确保其在没有同步操作的多线程环境中依然始终保持内部状态的一致性和正确性。这就是不变模式。
不变模式天生就是多线程友好的,它的核心思想是,一个对象一旦被创建,则它的内部状态将永远不会发生改变。所以,没有一个线程可以修改其内部状态和数据,同时其内部状态也绝不会自行发生改变。基于这些特性,对不变对象的多线程操作不需要进行同步控制。
同时还需要注意,不变模式和只读属性是有一定的区别的,不变模式是比读属性具有更强的一致性和不变性。对只读属性的对象而言,对象本身不能被其他线程修改,但是对象身状态却可能自行修改比如,一个对象的存活时间(对象创建时间和当前时间的时间差)是只读的,因为任何个第三方线程都不能修改这个属性,但是这是一个可变的属性,因为随着时间的推移,存活时司时刻都在发生变化。而不变模式则要求,无论出于什么原因,对象自创建后,其内部状态和数据保持绝对的稳定。
2、怎么实现不可变对象
在Java语言中,不变模式的实现很简单。为确保对象被创建后,不发生任何改变,并保证不变模式正常工作,只需要注意以下4点:
-
- 去除 setter方法以及所有修改自身属性的方法。
- 将所有属性设置为私有,并用final标记,确保其不可修改
- 确保没有子类可以重载修改它的行为。
- 有一个可以创建完整对象的构造函数。
是不是和final的功能很吻合。我们复习一下java中final的作用。
-
- final修饰类,表示该类不能被继承,俗称断子绝孙类,该类的所有方法自动地成为final方法
- final修饰方法,表示子类不可重写该方法
- final修饰基本数据类型变量,表示该变量为常量,值不能再修改
- final修饰引用类型变量,表示该引用在构造对象之后不能指向其他的对象,但该引用指向的对象的状态可以改变
这里需要说明的是:当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它保存的仅仅是一个引用,final只保证这个引用变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。例如某个指向数组的final引用,它必须从此至终指向初始化时指向的数组,但是这个数组的内容完全可以改变。
二、String源码分析
以下是jdk1.8中String类的部分源码。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0 /** use serialVersionUID from JDK 1.0.2 for interoperability */ private static final long serialVersionUID = -6849794470754667710L; /** ...
首先可以看到,String类使用了final修饰符,表明String类是不可继承的。然后,我们主要关注String类的成员变量value,value是char[]类型,因此String对象实际上是用这个字符数组进行封装的。再看value的修饰符,使用了private,也没有提供setter方法,所以在String类的外部不能修改value,同时value也使用了final进行修饰,那么在String类的内部也不能修改value,也就是说value一旦赋予初始值之后,value指向的地址就不能再改变了。但是上面final修饰引用类型变量的内容提到,这只能保证value不能指向其他的对象,但value指向的对象的状态是可以改变的。通过查看String类源码可以发现,String类不可变,关键是因为SUN公司的工程师,在后面所有String的方法里都很小心的没有去动字符数组里的元素。所以String类不可变的关键都在底层的实现,而不仅仅是一个final。
三、修改String使其“可变”
虽然value是final修饰的,只是说明value不能再重新指向其他的引用。但是value指向的数组可以改变,一般情况下我们是没有办法访问到这个value指向的数组的元素。But,反射,对,反射可以,牛逼吧。可以反射出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"); // 改变value属性的访问权限 valueField.setAccessible(true); // 获取str对象上value属性的值 char[] value = (char[]) valueField.get(str); // 改变value所引用的数组中的字符 value[3] = ‘?‘; System.out.println("修改后的str:" + str); System.out.println("修改前的str的内存地址" + System.identityHashCode(str)); } // 运行结果 // 可以看到str的字符串序列已经被改变了,但是str的内存地址还是没有改变。 修改前的str:Hello World 修改前的str的内存地址1922154895 修改后的str:Hel?o World 修改前的str的内存地址1922154895
四、String设计成不可变性的原因
在Java中,将String设计成不可变的是综合考虑到内存、同步、数据结构及安全等各种因素的结果,下文将为各种因素做一个小结。
1、运行时常量池的需要
比如执行 String s = "abc";执行上述代码时,JVM首先在运行时常量池中查看是否存在String对象“abc”,如果已存在该对象,则不用创建新的String对象“abc”,而是将引用s直接指向运行时常量池中已存在的String对象“abc”;如果不存在该对象,则先在运行时常量池中创建一个新的String对象“abc”,然后将引用s指向运行时常量池中创建的新String对象。
这样在运行时常量池中只会创建一个String对象"abc",这样就节省了内存空间。
2、同步
因为String对象是不可变的,所以是多线程安全的,同一个String实例可以被多个线程共享。这样就不用因为线程安全问题而使用同步。
3、允许String对象缓存hashcode
查看上文JDK1.8中String类源码,可以发现其中有一个字段hash,String类的不可变性保证了hashcode的唯一性,所以可以用hash字段对String对象的hashcode进行缓存,就不需要每次重新计算hashcode。所以Java中String对象经常被用来作为HashMap等容器的键。
4、安全性
如果String对象是可变的,那么会引起很严重的安全问题。比如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为String对象是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变String引用指向的对象的值,造成安全漏洞。
参考:
1、https://blog.csdn.net/dearKundy/article/details/82355019?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
2、https://blog.csdn.net/qq1404510094/article/details/80303334?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
原文地址:https://www.cnblogs.com/DDgougou/p/12588136.html