一、初始枚举
枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。下面先来看看什么是枚举?如何定义枚举?
1、枚举的定义
用类来枚举:
/**
*使用普通方式定义日期常量
*/
public class DayDemo {public static final int MONDAY =1;public static final int TUESDAY=2;public static final int WEDNESDAY=3;public static final int THURSDAY=4;public static final int FRIDAY=5;public static final int SATURDAY=6;public static final int SUNDAY=7;}
上述的常量定义常量的方式称为int枚举模式,这样的定义方式并没有什么错,但它存在许多不足,如在类型安全和使用方便性上并没有多少好处,如果存在定义int值相同的变量,混淆的几率还是很大的,编译器也不会提出任何警告,因此这种方式在枚举出现后并不提倡,现在我们利用枚举类型来重新定义上述的常量,同时也感受一把枚举定义的方式,如下定义周一到周日的常量。
//枚举类型,使用关键字enum
enum Day {MONDAY, TUESDAY, WEDNESDAY,THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
相当简洁,在定义枚举类型时我们使用的关键字是enum,与class关键字类似,只不过前者是定义枚举类型,后者是定义类类型。枚举类型Day中分别定义了从周一到周日的值,这里要注意,值一般是大写的字母,多个值之间以逗号分隔。同时我们应该知道的是枚举类型可以像类(class)类型一样,定义为一个单独的文件,当然也可以定义在其他类内部,更重要的是枚举常量在类型安全性和便捷性都很有保证,如果出现类型问题编译器也会提示我们改进,但务必记住枚举表示的类型其取值是必须有限的,也就是说每个值都是可以枚举出来的,比如上述描述的一周共有七天。那么该如何使用呢?如下:
public class EnumDemo {public static void main(String[] args){//直接引用Day day =Day.MONDAY;}}
//定义枚举类型
enum Day {MONDAY, TUESDAY, WEDNESDAY,THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
2、枚举实现原理
我们大概了解了枚举类型的定义与简单使用后,现在有必要来了解一下枚举类型的基本实现原理。实际上在使用关键字enum创建枚举类型并编译后,编译器会为我们生成一个相关的类,这个类继承了Java API中的java.lang.Enum类,也就是说通过关键字enum创建枚举类型在编译后事实上也是一个类类型而且该类继承自java.lang.Enum类。
利用javac编译前面定义的EnumDemo.java文件后分别生成了Day.class和EnumDemo.class文件,而Day.class就是枚举类型,这也就验证前面所说的使用关键字enum定义枚举类型并编译后,编译器会自动帮助我们生成一个与枚举相关的类。我们再来看看反编译Day.class文件:
这个反编译
//反编译Day.class
final class Day extends Enum
{//编译器为我们添加的静态的values()方法public static Day[] values(){return (Day[])$VALUES.clone();}//编译器为我们添加的静态的valueOf()方法,注意间接调用了Enum也类的valueOf方法public static Day valueOf(String s){return (Day)Enum.valueOf(com/feng/enumdemo/Day, s);}//私有构造函数private Day(String s, int i){super(s, i);}//前面定义的7种枚举实例public static final Day MONDAY;public static final Day TUESDAY;public static final Day WEDNESDAY;public static final Day THURSDAY;public static final Day FRIDAY;public static final Day SATURDAY;public static final Day SUNDAY;private static final Day $VALUES[];static { //实例化枚举实例MONDAY = new Day("MONDAY", 0);TUESDAY = new Day("TUESDAY", 1);WEDNESDAY = new Day("WEDNESDAY", 2);THURSDAY = new Day("THURSDAY", 3);FRIDAY = new Day("FRIDAY", 4);SATURDAY = new Day("SATURDAY", 5);SUNDAY = new Day("SUNDAY", 6);$VALUES = (new Day[] {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY});}
}
从反编译的代码可以看出编译器确实帮助我们生成了一个Day类(注意该类是final类型的,将无法被继承)而且该类继承自java.lang.Enum类,该类是一个抽象类(稍后我们会分析该类中的主要方法),除此之外,编译器还帮助我们生成了7个Day类型的实例对象分别对应枚举中定义的7个日期,这也充分说明了我们前面使用关键字enum定义的Day类型中的每种日期枚举常量也是实实在在的Day实例对象,只不过代表的内容不一样而已。
注意编译器还为我们生成了两个静态方法,分别是values()和 valueOf(),稍后会分析它们的用法,到此我们也就明白了,使用关键字enum定义的枚举类型,在编译期后,也将转换成为一个实实在在的类,而在该类中,会存在每个在枚举类型中定义好变量的对应实例对象,如上述的MONDAY枚举类型对应public static final Day MONDAY
;,同时编译器会为该类创建两个方法,分别是values()和valueOf()。
ok~,到此相信我们对枚举的实现原理也比较清晰,下面我们深入了解一下java.lang.Enum类以及values()和valueOf()的用途。
3、枚举的常见方法
Enum抽象类常见方法
Enum是所有 Java 语言枚举类型的公共基本类(注意Enum是抽象类),以下是它的常见方法:
这里主要说明一下ordinal()方法,该方法获取的是枚举变量在枚举类中声明的顺序,下标从0开始,如日期中的MONDAY在第一个位置,那么MONDAY的ordinal值就是0,如果MONDAY的声明位置发生变化,那么ordinal方法获取到的值也随之变化,注意在大多数情况下我们都不应该首先使用该方法,毕竟它总是变幻莫测的。
compareTo(E o)方法则是比较枚举的大小,注意其内部实现是根据每个枚举的ordinal值大小进行比较的。name()方法与toString()几乎是等同的,都是输出变量的字符串形式。至于valueOf(Class enumType, String name)方法则是根据枚举类的Class对象和枚举名称获取枚举常量,注意该方法是静态的,后面在枚举单例时,我们还会详细分析该方法,下面的代码演示了上述方法:
public class EnumDemo {public static void main(String[] args){//创建枚举数组Day[] days=new Day[]{Day.MONDAY, Day.TUESDAY, Day.WEDNESDAY,Day.THURSDAY, Day.FRIDAY, Day.SATURDAY, Day.SUNDAY};for (int i = 0; i <days.length ; i++) {System.out.println("day["+i+"].ordinal():"+days[i].ordinal());}System.out.println("-------------------------------------");//通过compareTo方法比较,实际上其内部是通过ordinal()值比较的System.out.println("days[0].compareTo(days[1]):"+days[0].compareTo(days[1]));System.out.println("days[0].compareTo(days[1]):"+days[0].compareTo(days[2]));//获取该枚举对象的Class对象引用,当然也可以通过getClass方法Class<?> clazz = days[0].getDeclaringClass();System.out.println("clazz:"+clazz);System.out.println("-------------------------------------");//name()System.out.println("days[0].name():"+days[0].name());System.out.println("days[1].name():"+days[1].name());System.out.println("days[2].name():"+days[2].name());System.out.println("days[3].name():"+days[3].name());System.out.println("-------------------------------------");System.out.println("days[0].toString():"+days[0].toString());System.out.println("days[1].toString():"+days[1].toString());System.out.println("days[2].toString():"+days[2].toString());System.out.println("days[3].toString():"+days[3].toString());System.out.println("-------------------------------------");Day d=Enum.valueOf(Day.class,days[0].name());Day d2=Day.valueOf(Day.class,days[0].name());System.out.println("d:"+d);System.out.println("d2:"+d2);}/**执行结果:day[0].ordinal():0day[1].ordinal():1day[2].ordinal():2day[3].ordinal():3day[4].ordinal():4day[5].ordinal():5day[6].ordinal():6-------------------------------------days[0].compareTo(days[1]):-1days[0].compareTo(days[1]):-2clazz:class com.zejian.enumdemo.Day-------------------------------------days[0].name():MONDAYdays[1].name():TUESDAYdays[2].name():WEDNESDAYdays[3].name():THURSDAY-------------------------------------days[0].toString():MONDAYdays[1].toString():TUESDAYdays[2].toString():WEDNESDAYdays[3].toString():THURSDAY-------------------------------------d:MONDAYd2:MONDAY*/}
enum Day {MONDAY, TUESDAY, WEDNESDAY,THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
到此对于抽象类Enum类的基本内容就介绍完了,这里提醒大家一点,Enum类内部会有一个构造函数,该构造函数只能有编译器调用,我们是无法手动操作的,不妨看看Enum类的主要源码:
//实现了Comparable
public abstract class Enum<E extends Enum<E>>implements Comparable<E>, Serializable {private final String name; //枚举字符串名称public final String name() {return name;}private final int ordinal;//枚举顺序值public final int ordinal() {return ordinal;}//枚举的构造方法,只能由编译器调用protected Enum(String name, int ordinal) {this.name = name;this.ordinal = ordinal;}public String toString() {return name;}public final boolean equals(Object other) {return this==other;}//比较的是ordinal值public final int compareTo(E o) {Enum<?> other = (Enum<?>)o;Enum<E> self = this;if (self.getClass() != other.getClass() && // optimizationself.getDeclaringClass() != other.getDeclaringClass())throw new ClassCastException();return self.ordinal - other.ordinal;//根据ordinal值比较大小}@SuppressWarnings("unchecked")public final Class<E> getDeclaringClass() {//获取class对象引用,getClass()是Object的方法Class<?> clazz = getClass();//获取父类Class对象引用Class<?> zuper = clazz.getSuperclass();return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;}public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {//enumType.enumConstantDirectory()获取到的是一个map集合,key值就是name值,value则是枚举变量值 //enumConstantDirectory是class对象内部的方法,根据class对象获取一个map集合的值 T result = enumType.enumConstantDirectory().get(name);if (result != null)return result;if (name == null)throw new NullPointerException("Name is null");throw new IllegalArgumentException("No enum constant " + enumType.getCanonicalName() + "." + name);}//.....省略其他没用的方法
}
通过Enum源码,可以知道,Enum实现了Comparable接口,这也是可以使用compareTo比较的原因,当然Enum构造函数也是存在的,该函数只能由编译器调用,毕竟我们只能使用enum关键字定义枚举,其他事情就放心交给编译器吧。
//由编译器调用
protected Enum(String name, int ordinal) {this.name = name;this.ordinal = ordinal;}
编译器生成的Values方法与ValueOf方法
values()方法和valueOf(String name)方法是编译器生成的static方法,因此从前面的分析中,在Enum类中并没出现values()方法,但valueOf()方法还是有出现的,只不过编译器生成的valueOf()方法需传递一个name参数,而Enum自带的静态方法valueOf()则需要传递两个方法,从前面反编译后的代码可以看出,编译器生成的valueOf方法最终还是调用了Enum类的valueOf方法,下面通过代码来演示这两个方法的作用:
Day[] days2 = Day.values();
System.out.println("day2:"+Arrays.toString(days2));
Day day = Day.valueOf("MONDAY");
System.out.println("day:"+day);/**输出结果:day2:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]day:MONDAY*/
从结果可知道,values()方法的作用就是获取枚举类中的所有变量,并作为数组返回,而valueOf(String name)方法与Enum类中的valueOf方法的作用类似根据名称获取枚举变量,只不过编译器生成的valueOf方法更简洁些只需传递一个参数。这里我们还必须注意到,由于values()方法是由编译器插入到枚举类中的static方法,所以如果我们将枚举实例向上转型为Enum,那么values()方法将无法被调用,因为Enum类中并没有values()方法,valueOf()方法也是同样的道理,注意是一个参数的。
//正常使用
Day[] ds=Day.values();
//向上转型Enum
Enum e = Day.MONDAY;
//无法调用,没有此方法
//e.values();
二、枚举与Class对象
上述我们提到当枚举实例向上转型为Enum类型后,values()方法将会失效,也就无法一次性获取所有枚举实例变量,但是由于Class对象的存在,即使不使用values()方法,还是有可能一次获取到所有枚举实例变量的,在Class对象中存在如下方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6PC07K8w-1649148972069)(https://cdn.jsdelivr.net/gh/ladidol/figurebed@main/img/c5e6bea7ca9e4dad94b8f568f86396a6.png)]
因此通过getEnumConstants()方法,同样可以轻而易举地获取所有枚举实例变量下面通过代码来演示这个功能:
//正常使用
Day[] ds=Day.values();
//向上转型Enum
Enum e = Day.MONDAY;
//无法调用,没有此方法
//e.values();
//获取class对象引用
Class<?> clasz = e.getDeclaringClass();
if(clasz.isEnum()) {Day[] dsz = (Day[]) clasz.getEnumConstants();System.out.println("dsz:"+Arrays.toString(dsz));
}/**输出结果:dsz:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]*/
正如上述代码所展示,通过Enum的class对象的getEnumConstants方法,我们仍能一次性获取所有的枚举实例常量。
三、枚举与switch
关于枚举与switch是个比较简单的话题,使用switch进行条件判断时,条件参数一般只能是整型,字符型。而枚举型确实也被switch所支持,在java 1.7后switch也对字符串进行了支持。这里我们简单看一下switch与枚举类型的使用:
enum Color {GREEN,RED,BLUE}public class EnumDemo4 {public static void printName(Color color){switch (color){case BLUE: //无需使用Color进行引用System.out.println("蓝色");break;case RED:System.out.println("红色");break;case GREEN:System.out.println("绿色");break;}}public static void main(String[] args){printName(Color.BLUE);printName(Color.RED);printName(Color.GREEN);//蓝色//红色//绿色}
}
需要注意的是使用在于switch条件进行结合使用时,无需使用Color引用。
四、枚举进阶
在前面的分析中,我们都是基于简单枚举类型的定义,也就是在定义枚举时只定义了枚举实例类型,并没定义方法或者成员变量,实际上使用关键字enum定义的枚举类,除了不能使用继承(因为编译器会自动为我们继承Enum抽象类而Java只支持单继承,因此枚举类是无法手动实现继承的),可以把enum类当成常规类,也就是说我们可以向enum类中添加方法和变量,甚至是mian方法,下面就来感受一把。
1、向enum类添加方法与自定义构造函数
重新定义一个日期枚举类,带有desc成员变量描述该日期的对于中文描述,同时定义一个getDesc方法,返回中文描述内容,自定义私有构造函数,在声明枚举实例时传入对应的中文描述,代码如下:
public enum Day2 {MONDAY("星期一"),TUESDAY("星期二"),WEDNESDAY("星期三"),THURSDAY("星期四"),FRIDAY("星期五"),SATURDAY("星期六"),SUNDAY("星期日");//记住要用分号结束private String desc;//中文描述/*** 私有构造,防止被外部调用* @param desc*/private Day2(String desc){this.desc=desc;}/*** 定义方法,返回描述,跟常规类的定义没区别* @return*/public String getDesc(){return desc;}public static void main(String[] args){for (Day2 day:Day2.values()) {System.out.println("name:"+day.name()+",desc:"+day.getDesc());}}/**输出结果:name:MONDAY,desc:星期一name:TUESDAY,desc:星期二name:WEDNESDAY,desc:星期三name:THURSDAY,desc:星期四name:FRIDAY,desc:星期五name:SATURDAY,desc:星期六name:SUNDAY,desc:星期日*/
}
下面的例子就是, 开发中可能会常用到的枚举场景(这是博主在实验室一个项目中用到的工具类):
package com.feng.utils.result;import lombok.experimental.Accessors;/*** 规定:* #1表示成功* #1001~1999 区间表示参数错误* #2001~2999 区间表示用户错误* #3001~3999 区间表示接口异常* 统一错误枚举类*/
@Accessors(chain = true)
public enum ResultCode {/* 成功 */SUCCESS(200, "成功"),/* 默认失败 */COMMON_FAIL(999, "失败"),CUSTOM_FAIL(9999, "自定义错误"),/* 参数错误:1000~1999 */PARAM_NOT_VALID(1001, "参数无效"),PARAM_IS_BLANK(1002, "参数为空"),PARAM_TYPE_ERROR(1003, "参数类型错误"),PARAM_NOT_COMPLETE(1004, "参数缺失"),FAIL_COPY_PROPERTIES(1076, "创建对象失败或拷贝对象属性失败"),/* 微信小程序登录 */WEI_XIN_CODE_ERROR(3001, "携带code向小程序请求失败"),WEI_XIN_SKEY_VALID(3002, "skey无效"),/* 用户错误 */USER_NOT_LOGIN(2001, "用户未登录"),USER_ACCOUNT_EXPIRED(2002, "账号已过期"),USER_CREDENTIALS_ERROR(2003, "密码错误"),USER_CREDENTIALS_EXPIRED(2004, "密码过期"),USER_ACCOUNT_DISABLE(2005, "账号不可用"),USER_ACCOUNT_LOCKED(2006, "账号被锁定"),USER_ACCOUNT_NOT_EXIST(2007, "账号不存在"),USER_ACCOUNT_ALREADY_EXIST(2008, "账号已存在"),USER_ACCOUNT_USE_BY_OTHERS(2009, "账号下线"),USER_IS_EMPTY(2010, "用户为空"),/*Token错误*/TOKEN_EXPIRED(4001, "登陆信息过期"),TOKEN_ERROR(4002, "信息被更改,请重新登陆"),/* 业务错误 */NO_PERMISSION(5001, "没有权限"),/*订单错误*/EXISTED_ORDER(6001, "该商品的订单已存在,请支付或取消后再重新下单"),/*商品错误*/EXISTED_GOOD(7001, "已存在该商品"),EXISTED_GOOD_INCART(7002,"购物车中已经存在该商品"),/*招投标错误*/EXISTED_SUBMISSION(8001,"已经存在该投标了"),EXISTED_INVITATION(8002,"已经存在该招标了");private Integer code;private String message;//自定一个构造方法ResultCode(Integer code, String message) {this.code = code;this.message = message;}//get方法public Integer getCode() {return code;}//set方法public ResultCode setCode(Integer code) {this.code = code;return this;}public String getMessage() {return message;}public ResultCode setMessage(String message) {this.message = message;return this;}/*** 根据code获取message** @param code 状态码* @return String*/public static String getMessageByCode(Integer code) {for (ResultCode ele : values()) {if (ele.getCode().equals(code)) {return ele.getMessage();}}return null;}
}
从上述代码可知,在enum类中确实可以像定义常规类一样声明变量或者成员方法。但是我们必须注意到,如果打算在enum类中定义方法,务必在声明完枚举实例后使用分号分开,倘若在枚举实例前定义任何方法,编译器都将会报错,无法编译通过,同时即使自定义了构造函数且enum的定义结束,我们也永远无法手动调用构造函数创建枚举实例,毕竟这事只能由编译器执行。
2、覆盖enum类方法,就是override
public enum Day2 {MONDAY("星期一"),TUESDAY("星期二"),WEDNESDAY("星期三"),THURSDAY("星期四"),FRIDAY("星期五"),SATURDAY("星期六"),SUNDAY("星期日");//记住要用分号结束private String desc;//中文描述/*** 私有构造,防止被外部调用* @param desc*/private Day2(String desc){this.desc=desc;}/*** 覆盖* @return*/@Overridepublic String toString() {return desc;}public static void main(String[] args){for (Day2 day:Day2.values()) {System.out.println("name:"+day.name()+",desc:"+day.toString());}}/**输出结果:name:MONDAY,desc:星期一name:TUESDAY,desc:星期二name:WEDNESDAY,desc:星期三name:THURSDAY,desc:星期四name:FRIDAY,desc:星期五name:SATURDAY,desc:星期六name:SUNDAY,desc:星期日*/
}
3、enum类中定义抽象方法
与常规抽象类一样,enum类允许我们为其定义抽象方法,然后使每个枚举实例都实现该方法,以便产生不同的行为方式,注意abstract关键字对于枚举类来说并不是必须的如下:
public enum EnumDemo3 {FIRST{@Overridepublic String getInfo() {return "FIRST TIME";}},SECOND{@Overridepublic String getInfo() {return "SECOND TIME";}};/*** 定义抽象方法, 这个抽象方法会被上面的几个enum实例给重写;* @return*/public abstract String getInfo();//测试public static void main(String[] args){System.out.println("F:"+EnumDemo3.FIRST.getInfo());System.out.println("S:"+EnumDemo3.SECOND.getInfo());/**输出结果:F:FIRST TIMES:SECOND TIME*/}
}
通过这种方式就可以轻而易举地定义每个枚举实例的不同行为方式。我们可能注意到,enum类的实例似乎表现出了多态的特性,可惜的是枚举类型的实例终究不能作为类型传递使用,就像下面的使用方式,编译器是不可能答应的:
//无法通过编译,毕竟EnumDemo3.FIRST是个实例对象public void text(EnumDemo3.FIRST instance){ }
感觉到枚举的流弊, 它就好像是将实例和类放在一个类中
4、enum类与接口
由于Java单继承的原因,enum类并不能再继承其它类,但并不妨碍它实现接口,因此enum类同样是可以实现多接口的,如下:
interface food{void eat();
}interface sport{void run();
}public enum EnumDemo2 implements food ,sport{FOOD,SPORT,; //分号分隔@Overridepublic void eat() {System.out.println("eat.....");}@Overridepublic void run() {System.out.println("run.....");}
}
有时候,我们可能需要对一组数据进行分类,比如进行食物菜单分类而且希望这些菜单都属于food类型,appetizer(开胃菜)、mainCourse(主菜)、dessert(点心)、Coffee等,每种分类下有多种具体的菜式或食品,此时可以利用接口来组织,如下(代码引用自Thinking in Java):
public interface Food {enum Appetizer implements Food {SALAD, SOUP, SPRING_ROLLS;}enum MainCourse implements Food {LASAGNE, BURRITO, PAD_THAI,LENTILS, HUMMOUS, VINDALOO;}enum Dessert implements Food {TIRAMISU, GELATO, BLACK_FOREST_CAKE,FRUIT, CREME_CARAMEL;}enum Coffee implements Food {BLACK_COFFEE, DECAF_COFFEE, ESPRESSO,LATTE, CAPPUCCINO, TEA, HERB_TEA;}
}public class TypeOfFood {public static void main(String[] args) {Food food = Appetizer.SALAD;food = MainCourse.LASAGNE;food = Dessert.GELATO;food = Coffee.CAPPUCCINO;}
}
好屌
通过这种方式可以很方便组织上述的情景,同时确保每种具体类型的食物也属于Food,现在我们利用一个枚举嵌套枚举的方式,把前面定义的菜谱存放到一个Meal菜单中,通过这种方式就可以统一管理菜单的数据了。
public enum Meal{APPETIZER(Food.Appetizer.class),MAINCOURSE(Food.MainCourse.class),DESSERT(Food.Dessert.class),COFFEE(Food.Coffee.class);private Food[] values;private Meal(Class<? extends Food> kind) {//通过class对象获取枚举实例//枚举实例就是上面那四个values = kind.getEnumConstants();}public interface Food {enum Appetizer implements Food {SALAD, SOUP, SPRING_ROLLS;}enum MainCourse implements Food {LASAGNE, BURRITO, PAD_THAI,LENTILS, HUMMOUS, VINDALOO;}enum Dessert implements Food {TIRAMISU, GELATO, BLACK_FOREST_CAKE,FRUIT, CREME_CARAMEL;}enum Coffee implements Food {BLACK_COFFEE, DECAF_COFFEE, ESPRESSO,LATTE, CAPPUCCINO, TEA, HERB_TEA;}}
}
五、枚举与单例模式
单例模式可以说是最常使用的设计模式了,它的作用是确保某个类只有一个实例,自行实例化并向整个系统提供这个实例。在实际应用中,线程池、缓存、日志对象、对话框对象常被设计成单例,总之,选择单例模式就是为了避免不一致状态,下面我们将会简单说明单例模式的几种主要编写方式,从而对比出使用枚举实现单例模式的优点。首先看看饿汉式的单例模式:
//饿汉式(基于classloder机制避免了多线程的同步问题)
public class SingletonHungry {private static SingletonHungry instance = new SingletonHungry();private SingletonHungry() {}public static SingletonHungry getInstance() {return instance;}
}
显然这种写法比较简单,但问题是无法做到延迟创建对象,事实上如果该单例类涉及资源较多,创建比较耗时间时,我们更希望它可以尽可能地延迟加载,从而减小初始化的负载,于是便有了如下的懒汉式单例:
//懒汉式单例模式(适合多线程安全)
public class SingletonLazy {private static volatile SingletonLazy instance;private SingletonLazy() {}public static synchronized SingletonLazy getInstance() {if (instance == null) {instance = new SingletonLazy();}return instance;}
}
这种写法能够在多线程中很好的工作避免同步问题,同时也具备lazy loading机制,遗憾的是,由于synchronized的存在,效率很低,在单线程的情景下,完全可以去掉synchronized,为了兼顾效率与性能问题,改进后代码如下:
public class Singleton {private static volatile Singleton singleton = null;private Singleton(){}public static Singleton getSingleton(){if(singleton == null){synchronized (Singleton.class){if(singleton == null){singleton = new Singleton();}}}return singleton;}
}
这种编写方式被称为“双重检查锁”,主要在getSingleton()方法中,进行两次null检查。这样可以极大提升并发度,进而提升性能。毕竟在单例中new的情况非常少,绝大多数都是可以并行的读操作,因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,也就提高了执行效率。但是必须注意的是volatile关键字,该关键字有两层语义。第一层语义是可见性,可见性是指在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以其它线程会马上读取到已修改的值,关于工作内存和主内存可简单理解为高速缓存(直接与CPU打交道)和主存(日常所说的内存条),注意工作内存是线程独享的,主存是线程共享的。volatile的第二层语义是禁止指令重排序优化,我们写的代码(特别是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同,这在单线程并没什么问题,然而一旦引入多线程环境,这种乱序就可能导致严重问题。volatile关键字就可以从语义上解决这个问题,值得关注的是volatile的禁止指令重排序优化功能在Java 1.5后才得以实现,因此1.5前的版本仍然是不安全的,即使使用了volatile关键字。或许我们可以利用静态内部类来实现更安全的机制,静态内部类单例模式如下:
//静态内部类
public class SingletonInner {private static class Holder {private static SingletonInner singleton = new SingletonInner();}private SingletonInner(){}public static SingletonInner getSingleton(){return Holder.singleton;}
}
正如上述代码所展示的,我们把Singleton实例放到一个静态内部类中,这样可以避免了静态实例在Singleton类的加载阶段就创建对象,毕竟静态变量初始化是在SingletonInner类初始化时触发的,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的。从上述4种单例模式的写法中,似乎也解决了效率与懒加载的问题,但是它们都有两个共同的缺点:
序列化可能会破坏单例模式,比较每次反序列化一个序列化的对象实例时都会创建一个新的实例,解决方案如下:
//测试例子(四种写解决方式雷同)
public class Singleton implements java.io.Serializable { public static Singleton INSTANCE = new Singleton(); protected Singleton() { } //反序列时直接返回当前INSTANCEprivate Object readResolve() { return INSTANCE; }
}
使用反射强行调用私有构造器,解决方式可以修改构造器,让它在创建第二个实例的时候抛异常,如下:
public static Singleton INSTANCE = new Singleton();
private static volatile boolean flag = true;
private Singleton(){if(flag){flag = false; }else{throw new RuntimeException("The instance already exists !");}
}
如上所述,问题确实也得到了解决,但问题是我们为此付出了不少努力,即添加了不少代码,还应该注意到如果单例类维持了其他对象的状态时还需要使他们成为transient的对象,这种就更复杂了,那有没有更简单更高效的呢?当然是有的,那就是枚举单例了,先来看看如何实现:
//枚举单利
public enum SingletonEnum {INSTANCE;private String name;public String getName(){return name;}public void setName(String name){this.name = name;}
}
代码相当简洁,我们也可以像常规类一样编写enum类,为其添加变量和方法,访问方式也更简单,使用SingletonEnum.INSTANCE进行访问,这样也就避免调用getInstance方法,更重要的是使用枚举单例的写法,我们完全不用考虑序列化和反射的问题。枚举序列化是由jvm保证的,每一个枚举类型和定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定:在序列化时Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的唯一性,这里我们不妨再次看看Enum类的valueOf方法:
public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {T result = enumType.enumConstantDirectory().get(name);if (result != null)return result;if (name == null)throw new NullPointerException("Name is null");throw new IllegalArgumentException("No enum constant " + enumType.getCanonicalName() + "." + name);}
实际上通过调用enumType(Class对象的引用)的enumConstantDirectory方法获取到的是一个Map集合,在该集合中存放了以枚举name为key和以枚举实例变量为value的Key&Value数据,因此通过name的值就可以获取到枚举实例,看看enumConstantDirectory方法源码:
Map<String, T> enumConstantDirectory() {if (enumConstantDirectory == null) {//getEnumConstantsShared最终通过反射调用枚举类的values方法T[] universe = getEnumConstantsShared();if (universe == null)throw new IllegalArgumentException(getName() + " is not an enum type");Map<String, T> m = new HashMap<>(2 * universe.length);//map存放了当前enum类的所有枚举实例变量,以name为key值for (T constant : universe)m.put(((Enum<?>)constant).name(), constant);enumConstantDirectory = m;}return enumConstantDirectory;}private volatile transient Map<String, T> enumConstantDirectory = null;
到这里我们也就可以看出枚举序列化确实不会重新创建新实例,jvm保证了每个枚举实例变量的唯一性。再来看看反射到底能不能创建枚举,下面试图通过反射获取构造器并创建枚举:
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {//获取枚举类的构造函数(前面的源码已分析过)Constructor<SingletonEnum> constructor=SingletonEnum.class.getDeclaredConstructor(String.class,int.class);constructor.setAccessible(true);//创建枚举SingletonEnum singleton=constructor.newInstance("otherInstance",9);}
执行报错:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objectsat java.lang.reflect.Constructor.newInstance(Constructor.java:417)at zejian.SingletonEnum.main(SingletonEnum.java:38)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
显然告诉我们不能使用反射创建枚举类,这是为什么呢?不妨看看newInstance方法源码:
public T newInstance(Object ... initargs)throws InstantiationException, IllegalAccessException,IllegalArgumentException, InvocationTargetException{if (!override) {if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {Class<?> caller = Reflection.getCallerClass();checkAccess(caller, clazz, null, modifiers);}}//这里判断Modifier.ENUM是不是枚举修饰符,如果是就抛异常if ((clazz.getModifiers() & Modifier.ENUM) != 0)throw new IllegalArgumentException("Cannot reflectively create enum objects");ConstructorAccessor ca = constructorAccessor; // read volatileif (ca == null) {ca = acquireConstructorAccessor();}@SuppressWarnings("unchecked")T inst = (T) ca.newInstance(initargs);return inst;}
源码很了然,确实无法使用反射创建枚举实例,也就是说明了创建枚举实例只有编译器能够做到而已。显然枚举单例模式确实是很不错的选择,因此我们推荐使用它。但是这总不是万能的,对于android平台这个可能未必是最好的选择,在android开发中,内存优化是个大块头,而使用枚举时占用的内存常常是静态变量的两倍还多,因此android官方在内存优化方面给出的建议是尽量避免在android中使用enum。但是不管如何,关于单例,我们总是应该记住:线程安全,延迟加载,序列化与反序列化安全,反射安全是很重重要的。
END
这里可以再看看这个视频戳这里