《Effective Java》第3条:用私有构造器或者枚举类型强化Singleton属性

Posted by 代码无止境 on 2019-05-15

实现单例模式的几种方法

书中一共提到了三种创建单例模式的方法:

  • 静态成员变量
  • 静态工厂方法
  • 单元素枚举
    其中前面两种也是我们经常使用的,书中也分析了这几种方式各自的优劣,下面我们就分别来看一下:

静态成员变量

1
2
3
4
5
6
7
8
9
10
public class Elvis01 {

public static final Elvis01 INSTANCE = new Elvis01();

private Elvis01() {}

public void leaveTheBuilding() {
System.out.println("leaving...");
}
}

静态工厂方法

1
2
3
4
5
6
7
8
9
10
11
public class Elvis02 implements Serializable {

private static final Elvis02 INSTANCE = new Elvis02();

private Elvis02() {}

public static Elvis02 getInstance() {
return INSTANCE;
}

}

但是这两种方式按照上面的写法都不能保证全局只有一个实例对象,我们且来看一下如何获取多个对象:

  • 通过反射机制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Test02 {
public static void main(String[] args) throws Exception{
Elvis02 e01 = Elvis02.getInstance();
Elvis02 e02 = Elvis02.getInstance();
System.out.println("e01的地址:" + e01);
System.out.println("e02的地址:" + e02);

// 通过反射来获取多个实例。
Elvis02 e03 = null;
Constructor[] constructors = e01.getClass().getDeclaredConstructors();
AccessibleObject.setAccessible(constructors, true);
for (int i=0; i< constructors.length; i++) {
if (constructors[i].getParameterCount() == 0) {
// 无参构造器。
try {
e03 = (Elvis02) constructors[i].newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
System.out.println("e03的地址:" + e03);
}
}

上面这段代码的打印结果如下:

1
2
3
e01的地址:cn.gancy.item03.Elvis02@1540e19d
e02的地址:cn.gancy.item03.Elvis02@1540e19d
e03的地址:cn.gancy.item03.Elvis02@677327b6

可以看到e03的地址变了,也就证明我们通过反射机制成功获取了第二个对象。那么解决这个问题的方案书中也提到了,我们可以在私有构造方法中做一些特殊处理,当我们创建第二个对象的时候抛出异常即可。

  • 通过反序列化来获取对象
1
2
3
4
5
6
7
8
9
10
// 通过反序列化来获取多个实例。
Elvis02 e04 = null;
try (FileOutputStream fos = new FileOutputStream("test.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(e01);
}
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.txt"))) {
e04 = (Elvis02) ois.readObject();
}
System.out.println("e04的地址:" + e04);

e04的地址如下所示

1
2
3
4
e01的地址:cn.gancy.item03.Elvis02@1540e19d
e02的地址:cn.gancy.item03.Elvis02@1540e19d
e03的地址:cn.gancy.item03.Elvis02@677327b6
e04的地址:cn.gancy.item03.Elvis02@2d98a335

可以发现在我们通过反序列化也得到了一个全新的实例e04,为了维护并且保证我们的单例模式,必须将我们的实例域都声明成瞬时的(transient),并且提供一个readResolve方法。修改完成后我们的Elvis变成如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Elvis03 implements Serializable {

private transient static final Elvis03 INSTANCE = new Elvis03();

private Elvis03() {}

public static Elvis03 getInstance() {
return INSTANCE;
}

private Object readResolve() {
return INSTANCE;
}
}

最后通过测试发现确实发序列化后并没有产生新的实例了。

单元素枚举

文中提到了第三种也是最推荐使用的一种实现单例模式的方法,即通过单元素枚举来实现单例模式,当然这种方式只能在jdk1.5之后使用。

1
2
3
4
5
6
7
public enum  Elvis04 {
INSTANCE;

public void leaveTheBuilding() {

}
}

单元素枚举类型已经成为了实现单例模式的最佳方法,有下面几个优点:

  • 代码显得更为简洁
  • 无偿的提供了序列化机制
  • 可以绝对的防止多次实例化

ps:“学习不止,码不停蹄”,如果你喜欢我的文章,就关注我吧。

扫码关注“代码无止境”