单例和枚举原理
枚举
简单介绍
枚举类能够统一管理一些全局的变量,封装对于他们的逻辑与方法。还能和switch-case结合,简化大量的if-else,让代码更加优雅。
直接Demo
public enum Week {
//本文的枚举类变量,枚举类实例,name属性指的就是MONDAY
//这类的变量
MONDAY(0,"星期一"),
TUESDAY(1,"星期二"),
WEDNESDAY(2,"星期三"),
THURSDAY(3,"星期四"),
FRIDAY(4,"星期五"),
SATURDAY(5,"星期六"),
//最后一个类型必须要用分号结束
SUNDAY(6,"星期日");
private int num;
private String desc;
/**
* 构造方法必然是private修饰的
* 就算不写,也是默认的
*
* @param num
* @param desc
*/
private Week(int num, String desc) {
this.num=num;
this.desc = desc;
}
public String getDesc() {
return desc;
}
public int getNum() {
return num;
}
/**
* 用switch重写toString方法,提高代码健壮性
* @return
*/
@Override
public String toString() {
//switch支持Enum类型
switch (this) {
case MONDAY:
return "今天星期一";
case TUESDAY:
return "今天星期二";
case WEDNESDAY:
return "今天星期三";
case THURSDAY:
return "今天星期四";
case FRIDAY:
return "今天星期五";
case SATURDAY:
return "今天星期六";
case SUNDAY:
return "今天星期日";
default:
return "Unknow Day";
}
}
public static void main(String[] args) {
//通过values()获取枚举数组
Week[] weeks = Week.values();
//遍历Week枚举类
for (Week day : weeks) {
System.out.println("name:" + day.name() +
",desc:" + day.getDesc());
}
//不符合则抛出java.lang.IllegalArgumentException
System.out.println(Week.valueOf("MONDAY"));
//返回对应的name属性
System.out.println(Week.FRIDAY.toString());
//返回4,根据我们定义的次序,从0开始。如果在定义时调换FRIDAY
//的次序,返回的数字也会对应的变化
System.out.println(Week.FRIDAY.ordinal());
}
}
输出:
name:MONDAY,desc:星期一
name:TUESDAY,desc:星期二
name:WEDNESDAY,desc:星期三
name:THURSDAY,desc:星期四
name:FRIDAY,desc:星期五
name:SATURDAY,desc:星期六
name:SUNDAY,desc:星期日
今天星期一
今天星期五
4
由上面和基础知识可知:
- 使用enum定义的枚举类默认继承了java.lang.Enum。枚举类的实例是final修饰的。不能继承其他类,也不可以被继承。
- 枚举类的所有实例必须放在第一行显示,不需使用new,不需显示调用构造方法,每个变量都是public static final修饰的,最终以分号结束。其实是一个语法糖。
- 枚举类的构造方法是私有的,默认的就是private,定义的时候不加也没事。
- 每一个实例,就相当于静态最终内部类。
- Enum阻止反序列话的方式,生成单例不会被破坏。
抽象方法
如果写抽象方法,枚举类的所有实例必须实现抽象方法。MONDAY本身就是一个AbstractWeek对象的引用。在初始化这个枚举类的时候,等同于执行的是AbstractWeek MONDAY= new AbstractWeek(0,“星期一”)。然后用匿名内部类的方式实现getNextDay()。
/**
* 枚举类可以有抽象方法,但是必须在它的实例中实现
*/
public enum AbstractWeek {
MONDAY(0,"星期一") {
@Override
public AbstractWeek getNextDay() {
return TUESDAY;
}
}, TUESDAY(1,"星期二") {
@Override
public AbstractWeek getNextDay() {
return WEDNESDAY;
}
}, WEDNESDAY(2,"星期三") {
@Override
public AbstractWeek getNextDay() {
return THURSDAY;
}
}, THURSDAY(3,"星期四") {
@Override
public AbstractWeek getNextDay() {
return FRIDAY;
}
}, FRIDAY(4,"星期五") {
@Override
public AbstractWeek getNextDay() {
return SATURDAY;
}
}, SATURDAY(5,"星期六") {
@Override
public AbstractWeek getNextDay() {
return SUNDAY;
}
}, SUNDAY(6,"星期日") {
@Override
public AbstractWeek getNextDay() {
return MONDAY;
}
};
private int num;
private String desc;
AbstractWeek(int num,String desc) {
this.num = num;
this.desc=desc;
}
//一个抽象方法
public abstract AbstractWeek getNextDay();
public static void main(String[] args) {
String nextDay=AbstractWeek.MONDAY.getNextDay().toString();
System.out.println(nextDay);
}
}
反编译
通过反编译来查看AbstractWeek,可以发现继承了Enum,里面的所有成员变量都是**public static final修饰的!**而且values()方法是编译器生成的。其实每一个实例都是一个内部类
public abstract class AbstractWeek extends java.lang.Enum<AbstractWeek> {
public static final AbstractWeek MONDAY;
public static final AbstractWeek TUESDAY;
public static final AbstractWeek WEDNESDAY;
public static final AbstractWeek THURSDAY;
public static final AbstractWeek FRIDAY;
public static final AbstractWeek SATURDAY;
public static final AbstractWeek SUNDAY;
public static solution1.AbstractWeek[] values();
Code:
0: getstatic #2 // Field $VALUES:[Lsolution1/AbstractWeek;
3: invokevirtual #3 // Method "[Lsolution1/AbstractWeek;".clone:()Ljava/lang/Object;
6: checkcast #4 // class "[Lsolution1/AbstractWeek;"
9: areturn
......
}
单例
单例模式就是在程序运行中只实例化一次,创建一个全局唯一对象,有点像 Java 的静态变量,但是单例模式要优于静态变量,静态变量在程序启动的时候JVM就会进行加载,如果不使用,会造成大量的资源浪费,单例模式能够实现懒加载,能够在使用实例的时候才去创建实例。开发工具类库中的很多工具类都应用了单例模式,比例线程池、缓存、日志对象等,它们都只需要创建一个对象,如果创建多份实例,可能会带来不可预知的问题,比如资源的浪费、结果处理不一致等问题。
单例的实现思路
- 静态化实例对象
- 私有化构造方法,禁止通过构造方法创建实例
- 提供一个公共的静态方法,用来返回唯一实例
单例的好处
- 只有一个对象,内存开支少、性能好
- 避免对资源的多重占用
- 在系统设置全局访问点,优化和共享资源访问
单例模式的实现
单例模式的写法有饿汉模式、懒汉模式、双重检查锁模式、静态内部类单例模式、枚举类实现单例模式五种方式,其中懒汉模式、双重检查锁模式,如果你写法不当,在多线程情况下会存在不是单例或者单例出异常等问题,具体的原因,在后面的对应处会进行说明。我们从最基本的饿汉模式开始我们的单例编写之路。
饿汉模式
饿汉模式采用一种简单粗暴的形式,在定义静态属性时,直接实例化了对象。代码如下:
//在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快
public class SingletonObject1 {
// 利用静态变量来存储唯一实例
private static final SingletonObject1 instance = new SingletonObject1();
// 私有化构造函数
private SingletonObject1(){
// 里面可能有很多操作
}
// 提供公开获取实例接口
public static SingletonObject1 getInstance(){
return instance;
}
}
饿汉模式的优缺点
优点
- 由于使用了static关键字,保证了在引用这个变量时,关于这个变量的所以写入操作都完成,所以保证了JVM层面的线程安全
缺点
- 不能实现懒加载,造成空间浪费,如果一个类比较大,我们在初始化的时就加载了这个类,但是我们长时间没有使用这个类,这就导致了内存空间的浪费。
饱汉-双重检查锁模式
public class SingletonObject5 {
// 添加volatile关键字
private static volatile SingletonObject5 instance;
private SingletonObject5(){
}
public static SingletonObject5 getInstance(){
// 第一次判断,如果这里为空,不进入抢锁阶段,直接返回实例
if (instance == null)
synchronized (SingletonObject5.class){
// 抢到锁之后再次判断是否为空
if (instance == null){
instance = new SingletonObject5();
}
}
return instance;
}
}
如果不加volatile关键字,会出现空指针的问题
private SingletonObject5(){
1 int x = 10;
2 int y = 30;
3 Object o = new Object();
}
上面的构造函数
SingletonObject4()
,我们编写的顺序是1、2、3,JVM 会对它进行指令重排序,所以执行顺序可能是3、1、2,也可能是2、3、1,不管是那种执行顺序,JVM 最后都会保证所以实例都完成实例化。 如果构造函数中操作比较多时,为了提升效率,JVM 会在构造函数里面的属性未全部完成实例化时,就返回对象。双重检测锁出现空指针问题的原因就是出现在这里,当某个线程获取锁进行实例化时,其他线程就直接获取实例使用,由于JVM指令重排序的原因,其他线程获取的对象也许不是一个完整的对象,所以在使用实例的时候就会出现空指针异常问题。
简单说:
如果构造方法里较多方法时,指令重排。JVM 会在构造函数里面的属性未全部完成实例化时,就返回对象。,其他线程获取的对象也许不是一个完整的对象,所以在使用实例的时候就会出现空指针异常问题。
饱汉-静态内部类单例模式
静态内部类单例模式也称单例持有者模式,实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。实现懒加载静态属性由static
修饰,保证只被实例化一次,并且严格保证实例化顺序。静态内部类单例模式代码如下:
public class SingletonObject6 {
private SingletonObject6(){
}
// 单例持有者
private static class InstanceHolder{
private final static SingletonObject6 instance = new SingletonObject6();
}
//
public static SingletonObject6 getInstance(){
// 调用内部类属性
return InstanceHolder.instance;
}
}
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
简单来说:
- 加载外部类时候,不会加载内部类。 只有内部类的属性/方法被调用,才会初始化内部类。实现了懒加载
- 静态属性由
static
修饰,保证只被实例化一次。保证了唯一性。
饱汉-枚举类实现单例模式
枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
public class SingletonObject7 {
private SingletonObject7(){
}
/**
* 枚举类型是线程安全的,并且只会装载一次
*/
private enum Singleton{
INSTANCE;
private final SingletonObject7 instance;
Singleton(){
instance = new SingletonObject7();
}
private SingletonObject7 getInstance(){
return instance;
}
}
public static SingletonObject7 getInstance(){
return Singleton.INSTANCE.getInstance();
}
}