不可变对象(Immutable Objects)in Java
前言
本文主要阐述以下观点:
- 值类型与引用类型的定义与区别;
- 不可变对象与可变对象的定义与区别;
- 不可变对象的优点与缺点;
- String类为什么是不可变的;
- 怎样声明不可变对象(包括变量和类);
是什么?
在了解不可变对象(Immutable Objects)及可变对象(Mutable Objects)之前,我们需要知识内存中两种数据类型:值类型(value types)和引用类型(reference types)——
值类型数据存放在栈(stack)内,其值即代表数据本身,存储在栈中分配的内存空间。
引用类型数据存放在堆(heap)内,其值代表的是所指向的地址,指向所要存储的值而不直接存储。
例如:假设Person是一种值类型,House是一种引用类型,有以下变量:
Person 老王 = someone;
Person 小王 = 老王; //小王是老王“克隆”出来的,一模一样;
House 老王的家 = new House();
House 小王的家 = 老王的家; //小王和老王住在同一个家里;
那么基于此,假如有一天老王因某事败露而被打残,小王的安危并不会受此影响,即使他们有相同的样貌,因为他们是相互独立的个体;而老王家里置办了新的电视或惨遭洗劫,“小王的家”自然也会接受相同的改变,因为他们家的地址指向了同一条街道的同一个房间。
在Java中,只有基本类型(Primitive Data Types)(即byte, short, int, long, float, double, char, boolean)是值类型,其他的数据类型都是引用类型,包括String,即对象Objects.
而引用类型又分为不可变对象(Immutable Objects)和可变对象(Mutable Objects),可变对象即为上述提到的普通引用类型数据,如果两个变量指向同一地址时,其中一个的值改变了另一个值也同样改变,下面详细说一下对不可变对象的理解。
Immutable Objects
An object is considered immutable if its state cannot change after it is constructed.[1]
即:不可变对象在构造(声明并且初始化后)之后,其状态不可再改变。其中String类和基本类型的Wrapper Class (Integer, Double, etc.)属于典型的不可变类。这里需要注意:状态不可变是指对象实例的值不可变而不是指向该实例的引用的不可变,例如:
String king= "John Snow";
king = "others";
上述所示好像String 的值发生了改变,说好的String 一生不变到白头,它却偷偷焗了油;其实,此处的不可变是指”John Snow”这个内存中的值没有发生变化,只是king不再指向它了,而换成了”others”,那么”John Snow”去哪儿了?长城之外还是龙妈闺房?很可惜,都不是,他被留在了内存里,等待GC(Garbage Collection,垃圾收集,垃圾回收)来回收他。
那这样做有什么好处?白白制造出了内存垃圾,king到头来还是易主了?
为什么?
首先,我们知道String是不可变类,而StringBuilder是可变的,所以先看代码:
public static void main(String args[]) {
String John = "John";
StringBuilder John2 = new StringBuilder("John");
System.out.println("What's your name?\t" + John);
System.out.println("Your full name please.\t" + getFullName(John));
System.out.println("What's your first name?\t" + John);
System.out.println();
System.out.println("What's your name?\t" + John2);
System.out.println("Your full name please.\t" + getFullName(John2));
System.out.println("What's your first name?\t" + John2);
}
private static String getFullName(String fstName) {
fstName += " Snow";
return fstName;
}
private static StringBuilder getFullName(StringBuilder fstName) {
fstName.append(" Snow");
return fstName;
}
执行结果如下:
// What's your name? John
// Your full name please. John Snow
// What's your first name? John //Value has NOT changed.
// What's your name? John
// Your full name please. John Snow
// What's your first name? John Snow //Value has changed.
上述所示,可变类StringBuilder的值改变了,而这常常不是程序员主动要做的,所以不可变类的一大优点是保证了线程安全,不会出现同步问题和隐私泄漏(Privacy Leaks)。当然,这种隐患还可以通过保护性拷贝(Defensive Copy)或深度复制(Deep Copy)来规避,不可变类同样可靠,但代码简单。
除此之外,不可变类还提高了拷贝的效率,因为复制时不再需要复制该对象的值,只需要复制其地址(指针)即可,而这只需要很小的内存空间,同时,对其他引用该对象的变量不造成影响。[2]
由开始时”John Snow”被“雪藏”的例子可以看出,过多的不可变类会造成很多内存垃圾,一定程度上增加了程序的运行成本,但也有不同看法认为:
程序员往往不愿使用不可变对象,因为他们担心创建一个新的对象要比更新对象的成本要高。实际上这种开销常常被过分高估,而且使用不可变对象所带来的一些效率提升也抵消了这种开销。例如:使用不可变对象降低了垃圾回收所产生的额外开销,也减少了用来确保使用可变对象不出现并发错误的一些额外代码。[3]
所以一般更多地建议在代码中合理运用不可变类。
怎么做?
局部变量、成员变量和类都可以声明为不可变对象。局部变量、成品变量操作较为简单,归为一类,不可变类单说。
不可变变量
形式:
(vis) final type var = value;
例如:
局部变量:final String king = "John Snow";
成员变量:private final String king = "John Snow";
特殊地,常量以以下形式声明:
private static final String KING = "John Snow";
特别需要注意数组(Array)等类型的不可变性:
final String[] week = new String[]{"Sunday", "Monday", "Tuesday", "Wednesday"...};
//Week = new String[]{"Sunday", "Sunday", "Sunday", "Sunday"...}; //invalid
for (int i = 0; i < week.length; i++) {
week[i] = "Sunday";
}
System.out.println(Arrays.toString(week));
执行结果如下:
//[Sunday, Sunday, Sunday, Sunday, Sunday, Sunday, Sunday]
上述代码中,将数组变量week置为不可变,那么当我们要将一周的每一天都赋值为星期天时,它如约提示”Cannot assign a value to final variable” 让我们断了念想,但转身一个for 循环就实现了梦想,在使用中要注意这种情况。
不可变类
不可变类的声明要麻烦很多,不单单是声明为”final”就可以的,而且,”final”在不可变类中的作用是使该类不可被继承,而和使之不可变没有必然联系。主要步骤如下:
- 保证所有成员变量必须私有,并且加上final修饰
- 不提供改变成员变量的方法,包括setter
- 通过构造器初始化所有成员,进行深拷贝(deep copy),特别是对非基本类型的成员,即引用类型
- 类添加final修饰符,保证类不被继承
看一下Java自己源代码中不可变类(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
...
}
String 类中基本符合以上声明的主要步骤;
可以看到String其实是用char[]存储数据的,但它是final 并且private的,所以避免了像week数组那样被改写的风险;
有个hash变量没有final,这里利用了其他的机制来保证其不可变,这里不做解释,可以参考:
Java中同样有没有将类声明为final的不可变类(BigDecimal和BigInteger),如下代码:
public class BigDecimal extends Number implements Comparable<BigDecimal> {
/**
* The unscaled value of this BigDecimal, as returned by {@link
* #unscaledValue}.
*
* @serial
* @see #unscaledValue
*/
private final BigInteger intVal;
但同样采取了其他机制来保证其不可变性。
总结
本文简单介绍了Immutable Objects(不可变对象)和Mutable Objects(可变对象)的一些特点,合理运用不可变对象会使代码整洁而高效,特别是在事件、多线程等数据安全性要求高的情景下,在实践中可以摸索出适合自己的代码风格。
参考文献
1.https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html
2. http://zhiheng.me/124#comment-195
3. https://waylau.gitbooks.io/essential-java/docs/concurrency-Immutable%20Objects.html#