单例器(Singleton)是实例控制的极端情况。但也非常常见。这本书列举了3中常见的单例器的惯用实现方法。但构建单例器的基本思路是不变的:
隐藏构造器,只保留一个实例,置于类的静态域中。
最朴素的Singleton实现就是 直接公开那个静态域中的唯一实例。
public class OurPlanet {
public static final OurPlanet EARTH = new OurPlanet("The Earth"); // 公有访问权限
private String name;
private OurPlanet(String name) {
this.name = name;
}
}
前面已经讲过,用静态工厂方法替代构造器,能提供灵活性。
public class OurPlanet {
private static final OurPlanet EARTH = new OurPlanet("The Earth"); // 私有化静态域中的实例
private String name;
private OurPlanet(String name) {
this.name = name;
}
public OurPlanet getInstance() { // 公有的静态工厂方法成为唯一的访问途径
return EARTH; // 总是返回唯一的实例
}
}
这样可以在不改变API的情况下(用户还是访问newInstance()
方法),改变是否应该是Singleton的想法。比如,若干年后,人类可以居住在火星。
public class OurPlanet {
private static final OurPlanet EARTH = new OurPlanet("The Earth"); // 私有化静态域中的实例
private static final OurPlanet MARS = new OurPlanet("The Mars"); // 私有化静态域中的实例
private static boolean switch = true;
private String name;
private OurPlanet(String name) {
this.name = name;
}
public OurPlanet getInstance() { // 公有的静态工厂方法还是唯一的访问途径
switch = !switch;
return switch? EARTH : MARS; // 人类已经有两个家园,地球和火星
}
}
枚举型是个大大的语法糖,它其实是一个实实在在的类。只需编写一个只包含单个元素的枚举型,我们就有了一个质量可靠的Singleton。
public enum OurPlanet { EARTH }
上面EARTH
的实际身份就是一个用static final
修饰过的公有域。这都和Singleton的模式一模一样。
记住,单元素的枚举型已经成为实现Singleton的最佳方法。和传统的Singleton比,枚举明显的优势有两个,
Serializable
接口是不够的,而且所有实例域必须是transient
的,而且必须重写readResolve()
方法,否则反序列化的过程会产生一个假冒的实例。防御这些问题的工作,枚举型做的很好,编译器无偿替我们做了。AccessibleObject.setAccessable()
方法可以改变私有构造器的访问权限。这方面枚举型的构造器能够抵御这样的攻击。在接到生产额外实例的请求时,枚举型的构造器会抛出异常。关于抵御反射攻击,在下一节会讲到。为了控制系统中存在的实例数量,就必须隐藏类的构造器。禁止用户访问它。
最简单的设置成抽象类,并不能禁止用户将它实例化。因为虽然不能实例化抽象类本身,但用户可以实例化抽象类的子类。
像前面的Singleton的三个实现,构造器都被设为了private
权限。
AccessibleObject.setAccessable()
方法可以私有构造器的访问权限改为公有。
最简单的比如增加一个计数器,在创造了足够数量的实例之后,构造器再接到实例化请求就抛出异常。
public class TenUnits {
private static int max = 10;
private TenUnits() {
if(max++ >= 10) { // 超出10个实例,抛出异常
throw new RuntimeException("Only 10 Objects allowed!");
}
// some code
}
}
记住,一般来说最好能重用对象,而不是在每次需要的时候就创建一个相同功能的新对象。最简单的,当类的某个方法总是重复创建某些相同的对象时,设置一个域来储存这些对象,能防止每次调用这个方法都重复创建对象。
下面的代码片段用来判断一个人是否出生于1946-1965
年间的“婴儿潮”。
public class Person {
private final Date birthDate;
// other fields, methods ... ...
public boolean isBabyBoomer() {
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946,Calendar.JANUARY,1,0,0,0);
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
Date boomStart = gmtCal.getTime();
gmtCal.set(1965,Calendar.JANUARY,1,0,0,0);
Date boomEnd = gmtCal.getTime();
return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
}
}
把Date
对象设置成静态域以后,每次调用isBabyBoomer()
方法都不会再创建这么多对象了。
public class Person {
public boolean isBabyBoomer() {
private static final Date BOOM_START;
private static final Date BOOM_END;
private final Date birthDate;
// other fields, methods ... ...
static {
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946,Calendar.JANUARY,1,0,0,0);
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
Date boomStart = gmtCal.getTime();
gmtCal.set(1965,Calendar.JANUARY,1,0,0,0);
Date boomEnd = gmtCal.getTime();
}
public boolean isBabyBoomer() {
return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
}
}
}
当尝试修改一个不可变对象,获得的会是一个拥有不同值的全新的对象,而不是在原对象上修修补补。一个极端的例子就是String
类。下面的代码其实是返回了字面量为abc
的另一个String
对象,虽然变量名还是s
,但却指向了不同的对象。因为String
是不可变的,声明的时候是ABC
就一直是ABC
,要abc
只能重新创建一个新对象。
String s = "ABC";
s.toLowerCase();
下面的代码,实际产生了两个String
对象。字面量ABC
本身已经是一个完整的String
对象,最后的变量s
又是一个拥有不同内存地址的新对象。
String s = new String("ABC"); // 不要这样做
如果直接使用字面量,就不会产生多余的对象。
String s = "ABC"; // 这样比较好
下面这个例子,计算所有int正值的总和,
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
避免创建不必要的类,不等于说就一定对象越少越好。小对象的创建开销相当廉价,因此有意识地添加一些附加对象,提升程序的可读性,功能性还是很好的。而且在创建“对象池”以重用以后对象的时候,也要想清楚,因此带来的代码混乱度是不是值得这么做。