【Java基础】关于枚举类你可能不知道的事
- 2019 年 10 月 3 日
- 筆記
谈谈枚举
如果一个类的对象个数是有限的而且是不变的,我们通常将这样的类设计成枚举类。
1. 枚举类的定义
枚举类有如下特点:
- 枚举类默认是使用final关键字修饰的,所以枚举类不能被继承;
- 枚举类的构造函数默认是使用private修饰的;
- 定义枚举类时所有实例必须在第一行全部列出;
- 枚举类也可以实现接口;
- 枚举类可以包含抽象方法。
//默认final修饰,不能被继承 public enum EnumDemo implements Runnable { //枚举的字段必须加注释 //男性 MALE("male"){ @Override //这边每个枚举类都单独实现了接口方法,也可以统一实现 //在枚举类的定义中实现一个就好了 public void run() { System.out.println("l like run..."); } @Override public void tellSex() { System.out.println("l am a man"); } }, //女性 Female("female"){ @Override public void run() { System.out.println("l hate running..."); } @Override public void tellSex() { System.out.println("l am a girl"); } }; private String sex; public String getSex(){ return sex; } /** * 构造函数默认是priva的 * @param sex */ EnumDemo(String sex){ this.sex = sex; } /** * 抽象方法,需要枚举类实例实现这个方法 */ public abstract void tellSex(); }
2. 枚举类的底层实现
假如有如下的一个枚举类定义
public enum T { SPRING,SUMMER,AUTUMN,WINTER; }
经过Java编译器编译后,如果我们反编译class文件可以看到如下代码:
public final class T extends Enum { private T(String s, int i) { super(s, i); } public static T[] values() { T at[]; int i; T at1[]; System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i); return at1; } public static T valueOf(String s) { return (T)Enum.valueOf(demo/T, s); } public static final T SPRING; public static final T SUMMER; public static final T AUTUMN; public static final T WINTER; private static final T ENUM$VALUES[]; static { SPRING = new T("SPRING", 0); SUMMER = new T("SUMMER", 1); AUTUMN = new T("AUTUMN", 2); WINTER = new T("WINTER", 3); ENUM$VALUES = (new T[] { SPRING, SUMMER, AUTUMN, WINTER }); } }
可以看到经过编译后,枚举类也是一个普通的Java类。这个类继承了Enum这个类,并且使用final修饰,所以枚举类都是不能被继承的。枚举类中定义的枚举值都是这个枚举类的静态成员变量,而且在初始化代码块中会一次性初始化,放在value数组中。我们调用枚举类的values()方法能一次性拿到所有的枚举值。关于枚举类,有几个重要的方法需要说下。从上面的代码可以看出我们定义的枚举类都会继承Enum这个类。
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable { //枚举的名字,我们可以通valueof(name)来获取枚举值 //这个就是指代我们定义的枚举实例的名字,比如上面的“SPRING”和“SUMMER”等 private final String name; public final String name() { return name; } //枚举的大小,枚举值比较大小默认的就是比较这个值的大小 //这个值的大小是根据我们定义枚举值的顺序来的,比如一个 //枚举值我们第一个定义那么这个枚举的ordinal就是0,比如上面定义的“SPRING”的ordinal值就是0 // 上面定义的“SUMMER”的值就是1,还有一点需要说明的就是:当我们在switch中使用枚举类型时, //编译后就是匹配的这个ordinal值 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; } public final int hashCode() { return super.hashCode(); } //禁止克隆 protected final Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } //比较ordinal的大小 public final int compareTo(E o) { Enum<?> other = (Enum<?>)o; Enum<E> self = this; if (self.getClass() != other.getClass() && // optimization self.getDeclaringClass() != other.getDeclaringClass()) throw new ClassCastException(); return self.ordinal - other.ordinal; } @SuppressWarnings("unchecked") public final Class<E> getDeclaringClass() { Class<?> clazz = getClass(); Class<?> zuper = clazz.getSuperclass(); return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper; } //通过name来获得枚举 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); } protected final void finalize() { } //禁止从流中获取对象 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { throw new InvalidObjectException("can't deserialize enum"); } private void readObjectNoData() throws ObjectStreamException { throw new InvalidObjectException("can't deserialize enum"); } }
3. 枚举类的序列化实现
以前的所有的单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是,为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定,即在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。
//Quarter是一个枚举类 Quarter[] values = Quarter.values(); Quarter one = values[0]; ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:\enum.txt")); objectOutputStream.writeObject(one); objectOutputStream.close(); ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\enum.txt")); Object object = objectInputStream.readObject(); //返回true System.out.println(object==one);
枚举类在序列化的时候只会将name属相序列化。在上面代码中ObjectInputStream类的readObject方法进行反序列化时会先判断被反序列化的类的类型,如果是枚举类就获取这个枚举类的类型再调用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); }
通过上面的代码可以看出,valueof方法根据获得的类还是枚举类初始化时缓存起来的类。所以系统中还是只会存在一个枚举类。
4. 用枚举实现单列
普通的单列实现方式有一个比较大的问题就是如果将单列类序列化后再进行反序列化那么同一个jvm中将存在两个单列类。通过上面的分析枚举类的反序列化不会出现这个问题,所以通过枚举类来实现单例模式是一个很好的选择。
public enum EnumSingleton { INSTANCE; public EnumSingleton getInstance(){ return INSTANCE; } }
5. 枚举实例的创建过程是线程安全的
从第二部分的代码我们可以看出,枚举实例的创建是在静态代码块中创建的。静态代码块会在类的初始化过程中执行,而类的初始化过程是线程安全的,所以枚举实例的创建过程是线程安全的。