先说结论
用私有构造器或者枚举类型强化Singleton属性
——《Effective Java》第二版
是的,使用枚举类(Java 1.5发行版之后支持)实现是单例模式最完美的方式。
理由是:
- 首先无偿提供了序列化机制,绝对防止反序列化破坏单例。
- 其次面对反射攻击仍然安全。
单例模式
单例模式,式如其名,仅仅被实例化一次的对象就是单例。单例模式在windows操作系统中也应用广泛,例如文件管理器,无论你打开多少个,操作的始终是一个文件系统。
单例的好处不用多说,省空间-仅一份实例、省时间-创建一次之后再获取就是用已经有的了,是的,不用多说还是说了hhh。
单例也不是没缺点,如果一个类是单例的,那么测试它将会变得困难,因为没法给Singleton替换模拟实现,除非它实现了一个充当其类型的接口。
四种常见实现方式
相信但凡会点java的都知道实现单例有最常见的两种方式:懒汉式和饿汉式。稍微深入一些,你还会了解到静态内部类可以避免既想安全实现又不想上来就占空间的尴尬。
其实实现的思路无非是:构造器私有加上一个可以获取单例对象的统一出口。
一个一个说,首先是饿汉式:
public class FileSystem{
// 很多人忽略final 我觉得有必要写上
private static final FileSytem INSTANCE = new FileSystem();
private FileSystem(){}
public static FileSystem getInstance(){
return INSTANCE;
}
}
写法简单方便,一气呵成,有人说了,你这个上来就new一个,如果我用不上是不白new了,有道理
那就懒汉式:
public class FileSystem{
private static FileSystem instance;
private FileSystem(){}
public static FileSystem getInstance(){
if(instance == null){
instance = new FileSystem();
}
return instance;
}
}
逻辑也挺清晰,用的时候才new,有人说不行啊,你也不考虑并发的情况呀,多个线程一起进来势必会有错的,有道理
我加个同步机制
public class FileSystem{
private static volatile FileSystem instance;
private FileSystem(){}
public static FileSystem getInstance(){
if(instance == null){
synchronized(FileSystem.class){
if(instance == null){
instance = new FileSystem();
}
}
}
return instance;
}
}
看我多机智,双重检验锁,外加volatile关键字,已经挺完美了,有人说不行啊,你这个反序列化能保证唯一吗,有道理
这个简单,提供readResolve方法可以防止序列化再反序列化实例时创建新的实例。
//省略下 无论懒汉恶汉还是静态内部类,都是需要readResolve的
private Object readResolve(){
return instance;
}
//这样就行拉
序列化问题解决了,有人说不行啊,你写这么一大坨呀。。。
有道理,我这还有简洁一点儿的,静态内部类实现方式:
public class FileSystem{
private FileSystem(){}
public static FileSystem getInstance(){
return InnerObject.INSTANCE;
}
public static class InnerObject{
public static final FileSystem INSTANCE = new FileSystem();
}
private Object readResolve(){
return InnerObject.INSTANCE;
}
}
首先,类加载时,内部类不会马上被加载,所以不会事先占用内存空间,其次在初始化内部类的过程中,JVM保证同一时刻只有一个线程运行,怎么样,够简洁,够安全了吧。
有人说,这个算是最完美的单例吗?NO NO NO
最完美的在这里
public enum FileSystem{
INSTANCE;
}
没了。
单元素的枚举类型已经成为实现Sinleton的最佳方法 出自《Effective Java》第二版原文。
当然我在刚看到这个结论的时候,是有点蒙的,因为首先我不太熟悉枚举类,其实枚举类里边的元素就是实例好的对象,如果你也对上边的实现方式不能完全理解,那你应该先去了解下枚举类。
其次一个原因在于,我当时的想法是,单例对象有很多属性和方法怎么办那,其实,还是不熟悉枚举导致的,,,
贴一下我最近写的一个坦克大战游戏中写的单例吧
package com.tank.model;
import java.awt.*;
import java.util.LinkedList;
import java.util.List;
import com.tank.cor.ColliderChain;
import com.tank.frame.GameMonitor;
/**
* @Author SunHongmin
* @Description 游戏组件的组织者 相当于mvc当中的 model -tankframe只和GameModel打交道 -门面模式
* @Date 2020/5/8 21:19
*/
public enum GameModel {
INSTANCE;
// 这里设置访问级别为public 因为游戏画面属性会被经常访问
public static int GAME_WIDTH;
public static int GAME_HEIGHT;
// 主战坦克
private Tank tank1;
// 更改为持有公共父类,这里用LinkedList是因为这个对象的成员需要频繁的插入和删除
private List<GameObject> objects = new LinkedList<>();
private GameMonitor gameMonitor;
// 碰撞责任链
private ColliderChain colliderChain;
GameModel() {}
public void paint(Graphics g) {
// 主战坦克被消灭 游戏结束
// GAME WIN
// 由坦克类负责绘制坦克
tank1.paint(g);
for (int i = 0; i < objects.size(); i++) {
GameObject gameObject = objects.get(i);
if (!gameObject.getLiving()) {
objects.remove(i);
i--;
continue;
}
gameMonitor.countModels(gameObject);
gameObject.paint(g);
}
//判断游戏物体之间的碰撞
for (int i = 0; i < objects.size(); i++) {
for (int j = i + 1; j < objects.size(); j++) {
colliderChain.collide(objects.get(i), objects.get(j));
}
}
}
public Tank getMainTank() {
return tank1;
}
public void addGameObject(GameObject gameObject) {
objects.add(gameObject);
}
public void addGameObjectAll(List<GameObject> gameObjects) {
objects.addAll(gameObjects);
}
public void setTank1(Tank tank1) {
this.tank1 = tank1;
}
public void setObjects(List<GameObject> objects) {
this.objects = objects;
}
public void setGameMonitor(GameMonitor gameMonitor) {
this.gameMonitor = gameMonitor;
}
public void setColliderChain(ColliderChain colliderChain) {
this.colliderChain = colliderChain;
}
}
你看啥都不耽误,枚举并不会有任何限制你写业务代码的地方,所以它确实是实现的最佳方式了。
总结
其实没啥总结的,学习单例模式还能顺带枚举类的知识,挺好的。