简介
枚举是Java1.5引入的新特性,通过关键字enum来定义枚举类。枚举类是一种特殊类,它和普通类一样可以使用构造器、定义成员变量和方法,也能实现一个或多个接口,但枚举类不能继承其他类.
原理分析
枚举类型使用的最常用类型就是枚举常量.下面通过一个简单的Demo来说明枚举的原理.
// 定义
public enum Color {
BLACK, WHITE
}
// 使用
public class Main {
public static void main(String[] args) {
System.out.println(Color.BLACK);
}
}
// 结果
// BLACK
这样只是能够知道枚举简单的使用方法,不能看出枚举的特点和枚举的具体实现.
下面我们通过 jad
工具来反编译Color
类, 通过jad -sjava Color.class
反编译出一份java文件.
// final修饰,无法被继承
public final class Color extends Enum {
// 为了避免 返回的数组修改,而引起内部values值的改变,返回的是原数组的副本
public static Color[] values() {
return (Color[]) $VALUES.clone();
}
// 按名字获取枚举实例
public static Color valueOf(String name) {
return (Color) Enum.valueOf(em / Color, name);
}
// 私有的构造函数
private Color(String name, int ordinal) {
super(name, ordinal);
}
// enum第一行的声明的变量,都对应一个枚举实例对象
public static final Color BLACK;
public static final Color WHITE;
//
private static final Color $VALUES[];
// 静态域初始化,说明在类加载的cinit阶段就会被实例化,jvm能够保证类加载过程的线程安全
static {
BLACK = new Color("BLACK", 0);
WHITE = new Color("WHITE", 1);
$VALUES = (new Color[]{
BLACK, WHITE
});
}
}
从反编译的类中,可以看出, 我们使用enum
关键字编写的类,在编译阶段编译器会自动帮我们生成一份真正在jvm中运行的代码.
该类继承自 Enum
类,public abstract class Enum
.
Enum类接受一个继承自Enum的泛型.(在反编译java文件中没有体现泛型是因为,泛型在阶段就会被类型类型擦除,替换为具体的实现.
).
从反编译的Color
类中可以看出,在enum
关键字的类中,第一行 (准确的说是第一个分号前) 定义的变量,都会生成一个 Color
实例,且它是在静态域中进行初始化的, 而静态域在类加载阶段的cinit
中进行初始化,所以枚举对象是线程安全的,由JVM来保证.
生成的枚举类有 Color $VALUES[];
成员变量,外部可以通过values()
方法获取当前枚举类的所有实例对象.
Enum成员变量和方法分析
Enum类实现了 Comparable
接口,表明它是支持排序的,可以通过 Collections.sort
进行自动排序.实现了public final int compareTo(E o)
接口,方法定义为final
且其实现依赖的ordinal
字段也是final类型,说明他只能根据ordinal
排序,排序规则不可变.
public final int compareTo(E o) {
Enum> other = (Enum>)o;
Enum self = this;
if (self.getClass() != other.getClass() && // optimization
self.getDeclaringClass() != other.getDeclaringClass())
throw new ClassCastException();
return self.ordinal - other.ordinal;
}
ordinal: 表示枚举的顺序,从Color
类中可以看出,它是从0开始按自然数顺序增长,且其值是final类型,外部无法更改.对于 ordinal()
方法,官方建议尽量不要使用它,它主要是提供给EnumMap
,EnumSet
使用的.
/**
* Returns the ordinal of this enumeration constant (its position
* in its enum declaration, where the initial constant is assigned
* an ordinal of zero).
*
* Most programmers will have no use for this method. It is
* designed for use by sophisticated enum-based data structures, such
* as {@link java.util.EnumSet} and {@link java.util.EnumMap}.
*
* @return the ordinal of this enumeration constant
*/
public final int ordinal() {
return ordinal;
}
name: 表示枚举类的名字,从Color
类的构造函数可以看出,它的值就是我们定义的实例的名称.
我们在例子中之所以能打印出实例名称,是因为 它的toString()
方法直接返回了name属性.
/**
* Returns the name of this enum constant, as contained in the
* declaration. This method may be overridden, though it typically
* isn't necessary or desirable. An enum type should override this
* method when a more "programmer-friendly" string form exists.
*
* @return the name of this enum constant
*/
public String toString() {
return name;
}
equals(): 从其实现来看, 我们程序中使用 ==
或者 equals
来判断两个枚举相等都是一样的.
public final boolean equals(Object other) {
return this==other;
}
getDeclaringClass(): 方法返回枚举声明的Class对象
每一个枚举类型极其定义的枚举变量在JVM中都是唯一的
这句话的意思是枚举类型它拥有的实例在编写的时候,就已经确定下,不能通过其他手段进行创建,且枚举变量在jvm有且只有一个对应的实例.
为了达到这个效果,它通过以下方法来确保.
- 类加载时创建,保证线程安全
从Color
类中可以看出, Color对象是在静态域创建,由类加载时初始化,JVM保证线程安全,这样就能确保Color对象不会因为并发同时请求而错误的创建多个实例.
- 对序列化进行特殊处理,防止反序列化时创建新的对象
我们知道一旦实现了Serializable接口之后,反序列化时每次调用 readObject()方法返回的都是一个新创建出来的对象.
而枚举则不同,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过Enum的valueOf()
方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化进行定制,因此禁用了writeObject
、readObject
、readObjectNoData
、writeReplace
和readResolve
等方法。
public static > T valueOf(Class 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);
}
/**
* prevent default deserialization
*/
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");
}
- 私有构造函数, 无法正常的
new
出对象
// 私有的构造函数
private Color(String name, int ordinal) {
super(name, ordinal);
}
- 无法通过
clone()
方法,克隆对象
/**
* Throws CloneNotSupportedException. This guarantees that enums
* are never cloned, which is necessary to preserve their "singleton"
* status.
*/
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
- 无法通过反射的方式创建枚举对象
枚举类型,在 JVM 层面禁止了通过反射构造枚举实例的行为,如果尝试通过反射创建,将会报Cannot reflectively create enum objects
.
static void reflectTest() throws Exception {
// 获取类对象
Class> cls = Class.forName("em.Color");
// 获取 color 的构造函数
Constructor> constructor = cls.getDeclaredConstructor(String.class, int.class);
// 获取访问权限
constructor.setAccessible(true);
// 实例化
Object reflectColor = constructor.newInstance("name", 0);
}
// 报错
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at Main.reflect(Main.java:24)
at Main.main(Main.java:13)
枚举类的特点总结
- 枚举实例必须在
enum
关键字声明的类中显式的指定(首行开始的以第一个分号结束) - 除了1, 没有任何方式(new,clone,反射,序列化)可以手动创建枚举实例
- 枚举类不可被继承
- 枚举类是线程安全的
- 枚举类型是类型安全的(typesafe)
- 无法继承其他类(已经默认继承Enum)
枚举的使用
- 枚举常量
如上诉 Color
枚举类,就是典型的枚举常量.
它可以在 switch
语句中使用
void enmuTest() {
Color tag = Color.BLACK;
switch (tag) {
case WHITE:
break;
case BLACK:
break;
}
}
枚举类型是类型安全的,可以对传入的值进行类型检查:
如有个 handleColor(Color color)
方法,那么方法参数自动会对类型进行检查,只能传入 Color.WHITE
和Color.BLACK
,如果使用 static final
定义的常量则不具备 类型安全的特点.
- 枚举与构造函数
枚举类可以编写自己的构造函数,但是不能声明public,protected
,为了是不让外部创建实例对象,默认为private
且只能为它.
- 枚举与类
除了枚举常量外, enum是一个完整的类,它也可以编写自己的构造方法以及方法,甚至实现接口.
这里需要注意,枚举类不能继承其他类,因为在编译时它已经继承了 Enum
,java无法多继承
// 实现Runnable接口,在这个类中没有意义,只是为了举例
public enum Color implements Runnable {
WHITE("黑色"),
BLACK("白色");
private final String value;
// 自定义构造,虽然没有写private,但是默认就是private
Color(String v) {
value = v;
}
// 自定义方法
public void draw() {
System.out.println("绘制 " + value);
}
// 重写方法
@Override
public String toString() {
return value;
}
// 实现接口方法
@Override
public void run() {
// todo ...
}
}
枚举与单例模式
单例模式网上有6-7中写法,除了 枚举方式外, 都有两个致命的缺点, 不能完全保证单例在jvm中保持唯一性.
- 反射创建单例对象
解决方案 : 在构造上述中判断,当多于一个实例时,再调用构造函数,直接报错.
- 反序列化时创建对象
解决方案 : 使用readResolve()方法来避免此事发生.
这两种缺点虽然都有方式解决,但是不免有些繁琐.
枚举类天生有这些特性.而且实现单例相当简单.
public enum Singleton {
INSTANCE;
public void method() {
// todo ...
}
}
所以,枚举实现的单例,可以说是最完美和简洁的单例了.推荐大家使用这种方式创建单例.
但是,枚举类的装载和初始化时会有时间和空间的成本. 它的实现比其他方式需要更多的内存空间,所以在Android这种受资源约束的设备中尽量避免使用枚举单例,而选择
双重检查锁(DCL)
和静态内部类
的方式实现单例.
枚举与策略模式
特定的常量类型与主体中的方法或行为有关时,即当数据与行为之间有关联时,可以考虑使用枚举来实现策略模式.
如我们需要实现加减运算,就可以在枚举类型中声明一个 apply
抽象方法,在特定于常量的方法(Constant-specific class body的Constant -specific method implementation)中,用具体实现抽象方法.
public enum Operation {
PLUS {
// 实例中实现抽象方法
public double apply(double x, double y) {
return x + y;
}
},
MINUS {
public double apply(double x, double y) {
return x - y;
}
};
// 声明抽象方法
public abstract double apply(double x, double y);
}
//调用
double result = Operation.PLUS.apply(1, 2);
枚举与Android
在旧版的Android开发者官网的指南 Managing Your App's Memory,新版中已经被移除.
有这么一句话 :
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
使用枚举常量比使用
final static
来实现常量定义,枚举的内存消耗比后高不止两倍. 你应该严格避免在Android上使用枚举.
导致很多开发者把它当成了教条,禁止在Android上使用枚举.
从反编译的Color
类中可以发现, 枚举为每一个对象创建一个枚举对象,枚举对象里面至少有 一个String
类型(name),和一个int
类型(ordinal)再加上对象头部占用的内存.(此处还忽略了$VALUS数组的创建消耗).
单个枚举类型常量,比static final
声明的常量占用的内存大的多.
因此,不建议在Android中使用枚举常量,而更偏向于使用 static final
来定义常量.
但是,枚举常量中有类型安全检查的功能,使用常规的实现,没有这种功能.
这里我们可以使用android提供的注解来实现类型检查. @StringDef
和@IntDef
具体可以参考这篇文章. Android Performance: Avoid using ENUM on Android
但是,一定不能使用枚举吗?
我觉得并不如此,当数据和行为有关联时,或者说数据受到行为的控制时,可以考虑使用策略枚举.
复杂的枚举
EnumSet
,EnumMap
并不常用,这里不做过多解释,想了解的可以参考 深入理解Java枚举类型(enum)
引用
- Android Performance: Avoid using ENUM on Android
- 深入理解Java枚举类型(enum)
- Java 枚举会比静态常量更消耗内存吗
- Should I strictly avoid using enums on Android?