顾名思义,一个类实例化一个对象后,对象的属性无法被改变,可称之为不可变类。如JDK中的八大包装类、String类等。不可变类各有用处,如包装类用于对基本类型的装箱操作,把基本类型化身为对象使用。而String类作为我们最常用的类之一,通过字符串常量池大大提升了性能。不可变类因为是不可变的,所以天然具有线程安全性。那么如何定义一个类为不可变类呢?
要使类成为不可变类,遵循以下5条规则:
1、 不要提供任何会修改类状态的方法;
2、 保证类不会被继承;
3、 使所有值域都为final;
4、 使所有值域都成为私有;
5、如果类具有指向可变对象的域,则必须确保该类的使用者无法获得指向这些对象的引用。
接下来我们从另一个角度来仔细过一下上面的规则:
首先,关于类的成员变量(也就是属性),如果它们都不是不可变类,那么它们应该是私有的、final的。通过私有的封装来让属性外部无法修改,而final的作用是让属性初始化后就不能再改变(第3、4点);
其次,关于类的方法,我们不能提供任何修改属性是方法(第1点),比如常见的setXXX方法就不能再出现了。如果类成员变量本身不是不可变的,那么需要注意两点:1、在初始化(比如构造器里)给该成员变量赋值时,应该取的是外部对象引用的克隆;2、如果不可变类提供给外部用于获取该成员变量的方法时,应该使用该成员变量的克隆,而非直接返回成员变量本身。这两点的目的都是避免外部通过对象引用修改不可变类的内部成员变量(第5点)。详见下面例子:
import java.util.Date;
public class Immutable {
private String name;
private final Date date;
public Immutable(String name, Date date) {
this.name = name;
this.date = date;
}
public String getName() {
return name;
}
public Date getDate() {
return date;
}
@Override
public String toString() {
return "Immutable{" +
"name='" + name + '\'' +
", date=" + date +
'}';
}
public static void main(String[] args) {
String name = "wlf";
Date today = new Date();
Immutable t = new Immutable(name, today);
System.out.println(t.toString());
// 构造器初始化方法漏洞
name = "wms";
today.setTime(today.getTime() + 100000);
System.out.println("Change: " + t.toString());
// get方法漏洞
name = t.name;
today = t.getDate();
name = "hello";
today.setTime(today.getTime() + 100000);
System.out.println("Change: " + t.toString());
}
}
运行结果:
Immutable{name='wlf', date=Mon Jun 03 22:55:10 CST 2019}
Change: Immutable{name='wlf', date=Mon Jun 03 22:56:50 CST 2019}
Change: Immutable{name='wlf', date=Mon Jun 03 22:58:30 CST 2019}
我们看到String是不可变类,所以我们很放心,而Date并非不可变类,所以它变了。我们通过克隆来让对象的引用不被外部操纵:
import java.util.Date;
public class Immutable {
private String name;
private final Date date;
public Immutable(String name, Date date) {
this.name = name;
this.date = (Date) date.clone();
}
public String getName() {
return name;
}
public Date getDate() {
return (Date) date.clone();
}
@Override
public String toString() {
return "Immutable{" +
"name='" + name + '\'' +
", date=" + date +
'}';
}
public static void main(String[] args) {
String name = "wlf";
Date today = new Date();
Immutable t = new Immutable(name, today);
System.out.println(t.toString());
// 构造器初始化方法漏洞
name = "wms";
today.setTime(today.getTime() + 100000);
System.out.println("Change: " + t.toString());
// get方法漏洞
name = t.name;
today = t.getDate();
name = "hello";
today.setTime(today.getTime() + 100000);
System.out.println("Change: " + t.toString());
}
}
运行结果:
Immutable{name='wlf', date=Mon Jun 03 22:57:16 CST 2019}
Change: Immutable{name='wlf', date=Mon Jun 03 22:57:16 CST 2019}
Change: Immutable{name='wlf', date=Mon Jun 03 22:57:16 CST 2019}
上面我们看到,克隆为我们保证了不可变性,它是如何做到的?克隆就像分身术一样,实际上就是针对原生对象,克隆了一个分身对象出来。
最后,关于类本身,为了防止子类复写父类的方法从而破坏不可变性,我们把类定义为final的(第2点)。
只要做到这三点,基本上就能保证你的类为不可变类了。但要想破解依然可以做到,做法类似破解单例,可以用反射来改变属性的私有为公有,这里就不展开了。