一.什么是不可变对象
不可变对象(Immutable Object):对象一旦被创建后,对象所有的状态及属性在其生命周期内不会发生任何变化。
从不可变对象的定义来看,其实比较简单,就是一个对象在创建后,不能对该对象进行任何更改。比如下面这段代码:
public class ImmutableObject {
private int value;
public ImmutableObject(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
}
由于ImmutableObject不提供任何setter方法,并且成员变量value是基本数据类型,getter方法返回的是value的拷贝,所以一旦ImmutableObject实例被创建后,该实例的状态无法再进行更改,因此该类具备不可变性。
再比如我们平时用的最多的String:
public class Test {
public static void main(String[] args) {
String str = "I love java";
String str1 = str;
System.out.println("after replace str:" + str.replace("java", "Java"));
System.out.println("after replace str1:" + str1);
}
}
输出结果:
after replace str:I love Java
after replace str1:I love java
从输出结果可以看出,在对str进行了字符串替换替换之后,str1指向的字符串对象仍然没有发生变化。
二.不可变对象存在的意义
让并发编程变得更简单
说到并发编程,可能很多朋友都会觉得最苦恼的事情就是如何处理共享资源的互斥访问,可能稍不留神,就会导致代码上线后出现莫名其妙的问题,并且大部分并发问题都不是太容易进行定位和复现。所以即使是非常有经验的程序员,在进行并发编程时,也会非常的小心,内心如履薄冰。
大多数情况下,对于资源互斥访问的场景,都是采用加锁的方式来实现对资源的串行访问,来保证并发安全,如synchronize关键字,Lock锁等。但是这种方案最大的一个难点在于:在进行加锁和解锁时需要非常地慎重。如果加锁或者解锁时机稍有一点偏差,就可能会引发重大问题,然而这个问题Java编译器无法发现,在进行单元测试、集成测试时可能也发现不了,甚至程序上线后也能正常运行,但是可能突然在某一天,它就莫名其妙地出现了。
既然采用串行方式来访问共享资源这么容易出现问题,那么有没有其他办法来解决呢?
事实上,引起线程安全问题的根本原因在于:多个线程需要同时访问同一个共享资源。
假如没有共享资源,那么多线程安全问题就自然解决了,Java中提供的ThreadLocal机制就是采取的这种思想。
然而大多数时候,线程间是需要使用共享资源互通信息的,如果共享资源在创建之后就完全不再变更,如同一个常量,而多个线程间并发读取该共享资源是不会存在线上安全问题的,因为所有线程无论何时读取该共享资源,总是能获取到一致的、完整的资源状态。
不可变对象就是这样一种在创建之后就不再变更的对象,这种特性使得它们天生支持线程安全,让并发编程变得更简单。
消除副作用
很多时候一些很严重的bug是由于一个很小的副作用引起的,并且由于副作用通常不容易被察觉,所以很难在编写代码以及代码review过程中发现,并且即使发现了也可能会花费很大的精力才能定位出来。
减少容器使用过程出错的概率
我们在使用HashSet时,如果HashSet中元素对象的状态可变,就会出现元素丢失的情况。所以在Java中,对于String、包装器这些类,我们经常会用他们来作为HashMap的key,试想一下如果这些类是可变的,将会发生什么?后果不可预知,这将会大大增加Java代码编写的难度。
三.如何创建不可变对象
所有成员变量必须是private
最好同时用final修饰(非必须)
不提供能够修改原有对象状态的方法
-
最常见的方式是不提供setter方法
-
如果提供修改方法,需要新创建一个对象,并在新创建的对象上进行修改
通过构造器初始化所有成员变量,引用类型的成员变量必须进行深拷贝(deep copy)
getter方法不能对外泄露this引用以及成员变量的引用
最好不允许类被继承(非必须)
四.不可变对象真的"完全不可改变"吗?
不可变对象虽然具备不可变性,但是不是"完全不可变"的,这里打上引号是因为通过反射的手段是可以改变不可变对象的状态的。
大家看到这里可能有疑惑了,为什么既然能改变,为何还叫不可变对象?这里面大家不要误会不可变的本意,从不可变对象的意义分析能看出来对象的不可变性只是用来辅助帮助大家更简单地去编写代码,减少程序编写过程中出错的概率,这是不可变对象的初衷。如果真要靠通过反射来改变一个对象的状态,此时编写代码的人也应该会意识到此类在设计的时候就不希望其状态被更改,从而引起编写代码的人的注意。下面是通过反射方式改变不可变对象的例子:
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String s = "Hello World";
System.out.println("s = " + s);
Field valueFieldOfString = String.class.getDeclaredField("value");
valueFieldOfString.setAccessible(true);
char[] value = (char[]) valueFieldOfString.get(s);
value[5] = '_';
System.out.println("s = " + s);
}
输出结果:
s = Hello World
s = Hello_World