不可变对象在并发程序中比较有用,由于其状态无法改变,因此无法被线程的干扰损坏或者被视为不一致状态。
基本概念
*不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,任何对它的改变都应该产生一个新的对象。
* 可变对象(Mutable Objects):相对于不可变类,可变类创建实例后可以改变其成员变量值,开发中创建的大部分类都属于可变类。
* 不可变对象的类即为不可变类(Immutable Class)。JAVA平台类库中包含许多不可变类,如String、基本类型的包装类、BigInteger和BigDecimal等。
编写不可变类
可以遵照以下几点来编写一个不可变类:
A. 确保类不能被继承:将类声明为final, 或者使用静态工厂并声明构造器为private。如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。
B. 使用private和final修饰符来修饰该类的属性
注:如果成员属性为可变对象属性,不要使这些对象改变:
1)不要提供更改可变对象的方法
2)不要共享对可变对象的引用,不要存储传给构造器的外部可变对象的引用。因为引用可变对象的成员变量和外部可变对象的引用指向同一块内存地址,用户可以在不可变类之外通过修改可变对象的值
public final class ImmutableDemo {
private final int[] myArray;
public ImmutableDemo(int[] array) {
this.myArray = array; // wrong
}
}
为了保证内部的值不被修改,可以采用深度拷贝的方法来复制一个对象并传入副本的引用来确保类的不可变
public final class MyImmutableDemo {
private final int[] myArray;
public MyImmutableDemo(int[] array) {
this.myArray = array.clone();
}
}
有必要时类中的方法返回内部可变对象的副本而不是原对象
C. 不要提供任何可以修改对象状态的方法(不仅仅是set方法, 还有任何其它可以改变状态的方法)
举个例子:
import java.util.Date;
public final class Planet {
//声明为final的基本类型数据总是不可变的
private final double fMass;
//不可变的对象属性 (String对象不可变)
private final String fName;
//可变对象的属性,因为可变属性只能被这个类改变,采用深度拷贝的方法来复制一个对象并传入副本的引用
private final Date fDateOfDiscovery;
public Planet(double aMass, String aName, Date aDateOfDiscovery) {
fMass = aMass;
fName = aName;
//创建aDateOfDiscovery的一个私有拷贝
//这是保持fDateOfDiscovery属性为private的唯一方式, 并且保护这个
//类不受调用者对于原始aDateOfDiscovery对象所做任何改变的影响
fDateOfDiscovery = new Date(aDateOfDiscovery.getTime());
}
//返回一个基本类型值.
public double getMass() {
return fMass;
}
//调用者得到内部属性的一个直接引用. 由于String是不可变的所以没什么影响
public String getName() {
return fName;
}
//返回一个可变对象的一个保护性拷贝.调用者可以任意改变返回的Date对象,但是不会
//影响类的内部.为什么? 因为它们没有fDate的一个引用. 更准确的说, 它们
//使用的是和fDate有着相同数据的另一个Date对象
public Date getDateOfDiscovery() {
return new Date(fDateOfDiscovery.getTime());
}
public static void main(String[] args) {
Planet planet = new Planet(1.0D, "earth", new Date());
Date date = planet.getDateOfDiscovery();
date.setTime(111111111L);
System.out.println("the value of fDateOfDiscovery of internal class : " + planet.fDateOfDiscovery.getTime());
System.out.println("the value of date after change its value : " + date.getTime());
}
}
运行结果:
the value of fDateOfDiscovery of internal class : 1393943752205
the value of date after change its value : 111111111
不可变对象的优缺点
优点:
* 构造、测试和使用都很简单
* 不可变对象是线程安全的,在线程之间可以相互共享,不需要利用特殊机制来保证同步问题,因为对象的值无法改变。可以降低并发错误的可能性,因为不需要用一些锁机制等保证内存一致性问题也减少了同步开销。
* 不可变对象可以被重复使用,可以将它们缓存起来重复使用,就像字符串字面量和整型数字一样。可以使用静态工厂方法来提供类似于valueOf()这样的方法,它可以从缓存中返回一个已经存在的Immutable对象,而不是重新创建一个。
public class CacheImmutale {
private final String name;
private static CacheImmutale[] cache = new CacheImmutale[10];
private static int pos = 0;
public CacheImmutale(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public static CacheImmutale valueOf(String name) {
// 遍历已缓存的对象
for (int i = 0; i < pos; i++) {
// 如果已有相同实例,直接返回该缓存的实例
if (cache[i] != null && cache[i].getName().equals(name)) {
return cache[i];
}
}
// 如果缓冲池已满
if (pos == 10) {
// 把缓存的第一个对象覆盖
cache[0] = new CacheImmutale(name);
pos = 1;
return cache[0];
} else {
// 把新创建的对象缓存起来,pos加1
cache[pos++] = new CacheImmutale(name);
return cache[pos - 1];
}
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CacheImmutale) {
CacheImmutale ci = (CacheImmutale) obj;
if (name.equals(ci.getName())) {
return true;
}
}
return false;
}
public static void main(String[] args) {
CacheImmutale c1 = CacheImmutale.valueOf("hello");
CacheImmutale c2 = CacheImmutale.valueOf("hello");
System.out.println(c1 == c2);// 输出结果为true
}
}
缺点:
* 不可变对象最大的缺点就是创建对象的开销,因为每一步操作都会产生一个新的对象,制造大量垃圾,由于他们不能被重用而且对于它们的使用就是”用“然后”扔“,会创造很多的垃圾,给垃圾收集带来很大的麻烦