枚举的定义
//枚举类型,使用关键字enum
enum Day {
MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
枚举的引用
public class EnumDemo {
public static void main(String[] args){
//直接引用
Day day =Day.MONDAY;
}
}
//定义枚举类型
enum Day {
MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
枚举实现的原理
实际上在使用关键字enum创建枚举类型并编译后,编译器会为我们生成一个相关的类,这个类继承了Java API中的java.lang.Enum类,也就是说通过关键字enum创建枚举类型在编译后事实上也是一个类类型而且该类继承自java.lang.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/zejian/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(),
valueOf(Class<T> enumType, String name)
方法是根据枚举类的Class对象和枚举名称获取枚举常量 - *
枚举的进阶用法
实际上使用关键字enum定义的枚举类,除了不能使用继承(因为编译器会自动为它继承Enum抽象类,而Java只支持单继承,因此枚举类是无法手动实现继承的),可以把enum类当成常规类,也就是说我们可以向enum类中添加方法和变量,甚至是mian方法。
* 向enum类添加方法与自定义构造函数
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:星期日
*/
}
但是我们必须注意到,如果打算在enum类中定义方法,务必在声明完枚举实例后使用分号分开,倘若在枚举实例前定义任何方法,编译器都将会报错,无法编译通过,同时即使自定义了构造函数且enum的定义结束,我们也永远无法手动调用构造函数创建枚举实例,毕竟这事只能由编译器执行。
枚举与单例模式
/**
* 枚举单例
*/
public enum SingletonEnum {
INSTANCE;
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
Java中单例模式大概有五种:饿汉式
、静态内部类
、懒汉式
、双重校验锁
、枚举式
。静态内部类
和双重校验锁
已经这么优秀了为什么还要有第五种枚举式
呢?
因为前面4种都存在一个序列化和反序列化时的安全问题。将单例对象序列化后,在反序列化时会重新创建一个单例对象,违背了单例模式的初衷。而枚举式单例则没有这个问题。
枚举序列化是由jvm保证的,每一个枚举类型和定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定:在序列化时Java仅仅是将枚举对象的name
属性输出到结果中,反序列化的时候则是通过java.lang.Enum
的valueOf()
方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的并禁用了writeObject
、readObject
、readObjectNoData
、writeReplace
和readResolve
等方法,从而保证了枚举实例的唯一性.
Enum类的valueOf方法:
public static <T extends Enum<T>> T (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) {
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);
}
执行报错,不能使用反射创建枚举类,是为什么呢?在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 volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
说明了创建枚举实例只有编译器能够做到而已。
结论
显然枚举单例模式确实是很不错的选择,因此我们推荐使用它。
不过由于使用枚举时占用的内存常常是静态变量的两倍还多,因此android官方在内存优化方面给出的建议是尽量避免在android中使用enum。
但是不管如何,关于单例,我们总是应该记住:线程安全
,延迟加载
,序列化与反序列化安全
,反射安全
是很重重要的。
参考:https://www.cnblogs.com/zhanqing/p/11076646.html
https://www.dazhuanlan.com/2019/12/09/5dee075380f8b/?__cf_chl_jschl_tk__=ed4f67f373f5206cda7c546acec6ca716eacc966-1600177577-0-Ae6Aendcpssw39Z1B7sPCgmAPGu_B2sQHoUH0HxbjJ_HgMJZzT0URCPSCGVejLBHC6AESG44QUnPDJ9-7m0HmSb7ZbBd0-kXDvYV3ilYwiZRHN7Cd_E1eUK7oc8bk3UnH4iEns3OP2Ulm8ZFDttH41cX16-M8w-aKuiaRSvvsr0tQDRrlN3DB-F4HzDc8Kg0IsJhWIuq-41UXux76E8zfWhQ55xvh7pvGiEKthB0t3lQMoEz2MOkp8O-p4yELBvSF81UA7Mi0xm5FRlO66RHxdyKX-UsoBwHDYRF9ak_jiPBmFy79C7HMeZwniIqUbLxZA
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。