枚举
枚举的用法:
enum Weekday {
MON("1",1){
@Override
public String toString() {
return super.toString();
}
// 注意这里重写了
@Override
public boolean isOrdered() {
return true;
}
},STA("2",2){
@Override
public String toString() {
return super.toString();
}
};
public boolean isOrdered() {
return false;
}
public final String dayValue;
public int num;
private Weekday(String dayValue,int num) {
this.dayValue = dayValue;
this.num = num;
}
}
public static void main(String[] args) {
Weekday weekday = Weekday.MON;
System.out.println(weekday.isOrdered());
Weekday sta = Weekday.STA;
System.out.println(sta.isOrdered());
// 可以像下面这样进行比较,因为jvm中,仅仅存在单个实例
Weekday weekday = Weekday.MON;
assert weekday == Weekday.MON;
}
在enum中可以定义自己的变量和方法。并且每一个实例都可以自己去重写定义在enum中的方法。
在枚举中可以很方便的使用 ==
来比较两个枚举是否相等。因为在jvm中,保证了枚举实例的唯一性。它是唯一存在的所以可以直接使用 ==
来进行比较。
并且枚举可以说是实现单例模式最好的模式。先复习一下其它的方式:
-
静态内部类
利用一个特性:静态内部类在没有访问它的成员属性或者成员方法的时候,static代码块或者static变量是不会被初始化的,那么就可以做到懒加载。(只有主动去访问了,才会加载)并且能够做到线程安全。代码如下:
public class Singleton {
// 静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
// 只有调用了本方法才会进行静态内部类的初始化
return SingletonHolder.INSTANCE;
}
}
- 饿汉
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
这种方式的缺点就是不管用不用都已经被加载了。如果我们希望延迟初始化这个单例对象,就不能使用上述的“饿汉式”实现。为啥这个能够保证线程的安全性呢?因为在类的生命周期内,静态变量只会被初始化一次。那么这个就从JVM的角度来保证了线程的安全性。下图可以看出来静态变量是在什么时候初始化的。
可以发现非final类型的变量是和类的初始化是相同的。那么类的初始化是在什么时候呢?可以去搜一下类的初始化时机
。主要是主动引用就会初始化类(比如说new一个对象,或者反射创建,或者反序列化创创建…),那么在初始化类的时候就会去初始化static变量了。所以static修饰的变量只会被初始化一次。
- DCL双重校验锁
//实现双重校验锁实现单例模式
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这里第一点是volatile
关键字。这里使用这个原因是禁止指令的重排序。如果没有用这个修饰的话,那么指令可能会被重排。出现这个现象的原因就是,初始化一个对象在CPU中并不是一个原子性的操作。对应着几条的字节码,比如说分配内存,将引用赋值给instance等。那么如果被重排序的话,将最后一句给instance赋值的指令排到最前面的话,那么其它的线程就可能判断到instance不为空,这样就得到了一个还未初始化完全的对象。
第二点是为啥要进行第二次的 instance == null
的判断。因为如果有两个线程都通过了第一个if,被阻塞在同步代码块外面,当一个线程进了同步代码块执行了初始化的操作之后,然后退出了同步代码块,第二个线程进入同步代码块,此时因为第一个线程已经初始化好了,所以如果不再进行一次判空的话就又会进行一次初始化了。
- 枚举实现单例
上面的实现都是把构造方法设置成私有的,然后暴露一个方法来获取instance。这样的话可以使用反射去获取所有的构造器,然后newinstance()来获取新的对象。或者是通过序列化反序列化来获取新的对象。都是不安全的。枚举实现单例的代码参考如下:
获取资源的方式很简单,只要 SomeThing.INSTANCE.getInstance() 即可获得所要实例。并且enum是实现了Serializable接口,就提供了序列化的机制。public class InstanceDemo { /** * 构造方法私有化 */ private InstanceDemo(){ } /** * 返回实例 * @return */ public static InstanceDemo getInstance() { return Singleton.INSTANCE.getInstance(); } /** * 使用枚举方法实现单利模式 */ private enum Singleton {//内部枚举类 INSTANCE; private InstanceDemo instance; /** * JVM保证这个方法绝对只调用一次 */ Singleton() { instance = new InstanceDemo(); } public InstanceDemo getInstance() { return instance; } } }