枚举知识点
枚举的概念
enum
的全称为 enumeration, 是 JDK 1.5 中引入的新特性。在Java中,被 enum
关键字修饰的类型就是枚举类型。形式如下:
enum Color { RED, GREEN, BLUE }
//Day.class 枚举类型,使用关键字enum
enum Day {
MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
枚举类的基础知识点:
枚举类一般用于一个类只可能拥有有限个实例,比如季节只可拥有春夏秋冬,性别只有男女
枚举类和普通类有以下几个不同点:
1、枚举类不能指定继承的父类(因为继承了java.lang.Enum类),但是可以实现多个接口,其中java.lang.Enum类实现了java.lang.Serializable和java.lang.Comparable两个接口
2、枚举类的构造方法的访问权限只可为private
3、枚举类的实例必须显式列出,系统会自动添加public static final 修饰,无须程序员显式添加
4、枚举类默认提供了一个values()方法,该方法可以很方便地遍历所有的枚举值
5、使用enum定义、非抽象的枚举类默认会使用final修饰,因此枚举类不能派生子类
枚举的本质
实际上在使用关键字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()
。
③到此我们也就明白了,使用关键字enum定义的枚举类型,在编译期后,也将转换成为一个实实在在的类,而在该类中,会存在每个在枚举类型中定义好变量的对应实例对象,如上述的MONDAY
枚举类型对应public static final Day MONDAY;
,同时编译器会为该类创建两个方法,分别是values()和valueOf()
。到此相信我们对枚举的实现原理也比较清晰。下面我们深入了解一下java.lang.Enum类以及values()和valueOf()的用途。
枚举的方法
在enum中,提供了一些基本方法:
values():返回 enum 实例的数组,而且该数组中的元素严格保持在 enum 中声明时的顺序
例:展示enum的基本方法
package com.hanker.enmu;
public class EnumMethodDemo {
enum Color {RED, GREEN, BLUE;}
enum Size {BIG, MIDDLE, SMALL;}
public static void main(String args[]) {
System.out.println("=========== Print all Color ===========");
for (Color c : Color.values()) {
System.out.println(c + " ordinal: " + c.ordinal());
}
System.out.println("=========== Print all Size ===========");
for (Size s : Size.values()) {
System.out.println(s + " ordinal: " + s.ordinal());
}
Color green = Color.GREEN;
System.out.println("green name(): " + green.name());
System.out.println("green getDeclaringClass(): " + green.getDeclaringClass());
System.out.println("green hashCode(): " + green.hashCode());
System.out.println("green compareTo Color.GREEN: " + green.compareTo(Color.GREEN));
System.out.println("green equals Color.GREEN: " + green.equals(Color.GREEN));
System.out.println("green equals Size.MIDDLE: " + green.equals(Size.MIDDLE));
System.out.println("green equals 1: " + green.equals(1));
System.out.format("green == Color.BLUE: %b\n", green == Color.BLUE);
}
}
输出结果:
=========== Print all Color ===========
RED ordinal: 0
GREEN ordinal: 1
BLUE ordinal: 2
=========== Print all Size ===========
BIG ordinal: 0
MIDDLE ordinal: 1
SMALL ordinal: 2
green name(): GREEN
green getDeclaringClass(): class com.hanker.enmu.EnumMethodDemo$Color
green hashCode(): 1829164700
green compareTo Color.GREEN: 0
green equals Color.GREEN: true
green equals Size.MIDDLE: false
green equals 1: false
green == Color.BLUE: false
枚举的特性
枚举的特性,归结起来就是一句话:除了不能继承,基本上可以将 enum
看做一个常规的类。
一、枚举可以添加方法
在概念章节提到了,枚举值默认为从0开始的有序数值 。那么问题来了:如何为枚举显示的赋值。Java 不允许使用 = 为枚举常量赋值;枚举可以添加普通方法、静态方法、抽象方法、构造方法.Java 虽然不能直接为实例赋值,但是它有更优秀的解决方案:为 enum 添加方法来间接实现显示赋值。创建 enum
时,可以为其添加多种方法,甚至可以为其添加构造方法。注意一个细节:如果要为enum定义方法,那么必须在enum的最后一个实例尾部添加一个分号。此外,在enum中,必须先定义实例,不能将字段或方法定义在实例前面。否则,编译器会报错。
例:全面展示如何在枚举中定义普通方法、静态方法、抽象方法、构造方法
package com.hanker.enmu;
public enum ErrorCode {
OK(0) {
public String getDescription() {
return "成功";
}
},
ERROR_A(100) {
public String getDescription() {
return "错误A";
}
},
ERROR_B(200) {
public String getDescription() {
return "错误B";
}
},
ERROR_C(300){
public String getDescription() {
return "错误C";
}
};
private int code;
// 构造方法:enum的构造方法只能被声明为private权限或不声明权限
private ErrorCode(int number) { // 构造方法
this.code = number;
}
public int getCode() { // 普通方法
return code;
}
public abstract String getDescription(); // 抽象方法
public static void main(String args[]) { // 静态方法
for (ErrorCode s : ErrorCode.values()) {
System.out.println("code: " + s.getCode() + ", description: " + s.getDescription());
}
}
}
注:上面的例子并不可取,仅仅是为了展示枚举支持定义各种方法。下面是一个简化的例子
**例:一个错误码枚举类型的定义,**本例和上例的执行结果完全相同。
public enum ErrorCodeEn {
OK(0, "成功"),
ERROR_A(100, "错误A"),
ERROR_B(200, "错误B");
ErrorCodeEn(int number, String description) {
this.code = number;
this.description = description;
}
private int code;
private String description;
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
public static void main(String args[]) { // 静态方法
for (ErrorCodeEn s : ErrorCodeEn.values()) {
System.out.println("code: " + s.getCode() + ", description: " + s.getDescription());
}
}
}
二、枚举可以实现接口
enum
可以像一般类一样实现接口。同样是实现上一节中的错误码枚举类,通过实现接口,可以约束它的方法。
public interface INumberEnum {
int getCode();
String getDescription();
}
public enum ErrorCodeEn2 implements INumberEnum {
OK(0, "成功"),
ERROR_A(100, "错误A"),
ERROR_B(200, "错误B");
ErrorCodeEn2(int number, String description) {
this.code = number;
this.description = description;
}
private int code;
private String description;
@Override
public int getCode() {
return code;
}
@Override
public String getDescription() {
return description;
}
}
三、枚举不可以继承
**enum 不可以继承另外一个类,当然,也不能继承另一个 enum 。**因为 enum
实际上都继承自 java.lang.Enum
类,而 Java 不支持多重继承,所以 enum
不能再继承其他类,当然也不能继承另一个 enum
。
四、覆盖枚举的方法
下面给出一个toString()方法覆盖的例子。
public enum Color {
RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4);
// 成员变量
private String name;
private int index;
// 构造方法
private Color(String name, int index) {
this.name = name;
this.index = index;
}
//覆盖方法
@Override
public String toString() {
return this.index+"_"+this.name;
}
}
五、枚举的序列化
虽然说Enum
实现了Serializable
接口,但实际上枚举类的序列化只是将名称(成员变量name
)输出,反序列化则是默认调用Enum
静态方法valueOf
。可以看下面这一段测试代码:
public enum Season {
SPRING("春天"), SUMMER("夏天"), AUTUMN("秋天"), WINTER("冬天");
private final String chinese;
private Season(String chinese) {
this.chinese = chinese;
}
public String getChinese() {
return chinese;
}
}
public class Test {
public static void main(String[] arg)
throws IOException, ClassNotFoundException {
FileOutputStream fos = new FileOutputStream("D:\\out.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(Season.SPRING);
oos.close();
FileInputStream fis = new FileInputStream("D:\\out.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
Object obj = ois.readObject();
System.out.println(obj.getClass().getName());
System.out.println(obj == Season.SPRING);
}
}
序列化文件只保存了name
属性的值和对象类型;运行上述代码,输出结果为:
com.test.Season
true
可以看出反序列化只是通过序列化文件中name
属性调用valueOf
返回Season
类已经构造好的实例而已。
枚举的应用场景
一、组织常量
在JDK1.5 之前,在Java中定义常量都是public static final TYPE a;
这样的形式。有了枚举,你可以将有关联关系的常量组织起来,使代码更加易读、安全,并且还可以使用枚举提供的方法。
枚举声明的格式
**注:如果枚举中没有定义方法,也可以在最后一个实例后面加逗号、分号或什么都不加。**下面三种声明方式是等价的:
enum Color { RED, GREEN, BLUE }
enum Color { RED, GREEN, BLUE, }
enum Color { RED, GREEN, BLUE; }
案例2:
package com.hanker.enmu;
public enum StateType {
/**
* 成功返回状态
*/
OK(200, "OK"),
/**
* 请求格式错误
*/
BAD_REQUEST(400, "bad request"),
/**
* 未授权
*/
UNAUTHORIZED(401, "unauthorized"),
/**
* 没有权限
*/
FORBIDDEN(403, "forbidden"),
/**
* 请求的资源不存在
*/
NOT_FOUND(404, "not found"),
/**
* 该http方法不被允许
*/
NOT_ALLOWED(405, "method not allowed"),
/**
* 请求处理发送异常
*/
PROCESSING_EXCEPTION(406, "Handling Exceptions"),
/**
*
* 请求处理未完成
*/
PROCESSING_UNFINISHED(407, "To deal with unfinished"),
/**
* 登录过期
*/
BEOVERDUE(408, "Be overdue"),
/**
* 用户未登录
*/
NOT_LOGIN(409, "Not logged in"),
/**
* 这个url对应的资源现在不可用
*/
GONE(410, "gone"),
/**
* 请求类型错误
*/
UNSUPPORTED_MEDIA_TYPE(415, "unsupported media type"),
/**
* 校验错误时用
*/
UNPROCESSABLE_ENTITY(422, "unprocessable entity"),
/**
* 请求过多
*/
TOO_MANY_REQUEST(429, "too many request");
private int code;
private String value = null;
private StateType(int code, String value) {
this.code = code;
this.value = value;
}
public String value() {
return this.value;
}
public int getCode() {
return code;
}
public static Boolean isValidateStateType(String... stateType) {
for (int i = 0; i < stateType.length; i++) {
StateType[] value = StateType.values();
boolean falg = false;
for (StateType type : value) {
if (type.value.equals(stateType[i])) {
falg = true;
}
}
if (!falg) {
return falg;
}
}
return true;
}
/* 使用 */
public static void main(String[] args) {
System.out.println("状态码:" + StateType.OK.getCode());
System.out.println("错误信息:" + StateType.OK.value());
}
}
二、switch判断条件
我们经常使用switch语句来写状态机。JDK7以后,switch已经支持 int
、char
、String
、enum
类型的参数。这几种类型的参数比较起来,使用枚举的switch代码更具有可读性。
enum Signal {RED, YELLOW, GREEN}
public static String getTrafficInstruct(Signal signal) {
String instruct = "信号灯故障";
switch (signal) {
case RED:
instruct = "红灯停";
break;
case YELLOW:
instruct = "黄灯请注意";
break;
case GREEN:
instruct = "绿灯行";
break;
default:
break;
}
return instruct;
}
三、组织枚举
可以将类型相近的枚举通过接口或类组织起来。但是一般用接口方式进行组织。原因是:Java接口在编译时会自动为enum类型加上public static
修饰符;Java类在编译时会自动为 enum
类型加上static修饰符。看出差异了吗?没错,就是说,在类中组织 enum
,如果你不给它修饰为 public
,那么只能在本包中进行访问。
例:在接口中组织 enum
public interface Plant {
enum Vegetable implements INumberEnum {
POTATO(0, "土豆"),
TOMATO(0, "西红柿");
Vegetable(int number, String description) {
this.code = number;
this.description = description;
}
private int code;
private String description;
@Override
public int getCode() {
return 0;
}
@Override
public String getDescription() {
return null;
}
}
enum Fruit implements INumberEnum {
APPLE(0, "苹果"),
ORANGE(0, "桔子"),
BANANA(0, "香蕉");
Fruit(int number, String description) {
this.code = number;
this.description = description;
}
private int code;
private String description;
@Override
public int getCode() {
return 0;
}
@Override
public String getDescription() {
return null;
}
}
}
例:在类中组织 enum本例和上例效果相同。
public class Plant2 {
public enum Vegetable implements INumberEnum {...} // 省略代码
public enum Fruit implements INumberEnum {...} // 省略代码
}
四、枚举实现单例
单例模式网上有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);
案例2:
package com.hanker.enmu;
enum PayrollDay {
MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDNESDAY(
PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY), FRIDAY(PayType.WEEKDAY), SATURDAY(
PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) {
this.payType = payType;
}
double pay(double hoursWorked, double payRate) {
return payType.pay(hoursWorked, payRate);
}
// 策略枚举
private enum PayType {
WEEKDAY {
double overtimePay(double hours, double payRate) {
return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT)
* payRate / 2;
}
},
WEEKEND {
double overtimePay(double hours, double payRate) {
return hours * payRate / 2;
}
};
private static final int HOURS_PER_SHIFT = 8;
abstract double overtimePay(double hrs, double payRate);
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
return basePay + overtimePay(hoursWorked, payRate);
}
}
public static void main(String[] args) {
System.out.println("时薪100的人在周五工作8小时的收入:" + PayrollDay.FRIDAY.pay(8.0, 100));
System.out.println("时薪100的人在周六工作8小时的收入:" + PayrollDay.SATURDAY.pay(8.0, 100));
}
}
执行结果:
时薪100的人在周五工作8小时的收入:800.0
时薪100的人在周六工作8小时的收入:1200.0
枚举的工具类
Java 中提供了两个方便操作enum的工具类——EnumSet 和 EnumMap。
EnumSet
是枚举类型的高性能 Set
实现。它要求放入它的枚举常量必须属于同一枚举类型。
EnumMap
是专门为枚举类型量身定做的 Map
实现。虽然使用其它的 Map 实现(如HashMap)也能完成枚举类型实例到值得映射,但是使用 EnumMap 会更加高效:它只能接收同一枚举类型的实例作为键值,并且由于枚举类型实例的数量相对固定并且有限,所以 EnumMap 使用数组来存放与枚举类型对应的值。这使得 EnumMap 的效率非常高。
// EnumSet的使用
System.out.println("EnumSet展示");
EnumSet<ErrorCodeEn> errSet = EnumSet.allOf(ErrorCodeEn.class);
for (ErrorCodeEn e : errSet) {
System.out.println(e.name() + " : " + e.ordinal());
}
// EnumMap的使用
System.out.println("EnumMap展示");
EnumMap<StateMachine.Signal, String> errMap = new EnumMap(StateMachine.Signal.class);
errMap.put(StateMachine.Signal.RED, "红灯");
errMap.put(StateMachine.Signal.YELLOW, "黄灯");
errMap.put(StateMachine.Signal.GREEN, "绿灯");
for (Iterator<Map.Entry<StateMachine.Signal, String>> iter = errMap.entrySet().iterator(); iter.hasNext();) {
Map.Entry<StateMachine.Signal, String> entry = iter.next();
System.out.println(entry.getKey().name() + " : " + entry.getValue());
}
枚举与数据库交互
我们可以配合Mybatis将数据库字段转换为枚举类型。现在假设有一个数据库字段check_type的类型如下:
`check_type` int(1) DEFAULT NULL COMMENT '检查类型(1:未通过、2:通过)',
它对应的枚举类型为CheckType,代码如下:
package com.hanker.enmu;
import java.util.HashMap;
public enum CheckType {
NO_PASS(1, "未通过"), PASS(2, "通过");
private int key;
private String text;
private CheckType(int key, String text) {
this.key = key;
this.text = text;
}
public int getKey() {
return key;
}
public String getText() {
return text;
}
private static HashMap<Integer,CheckType> map = new HashMap<Integer,CheckType>();
static {
for(CheckType d : CheckType.values()){
map.put(d.key, d);
}
}
public static CheckType parse(Integer index) {
if(map.containsKey(index)){
return map.get(index);
}
return null;
}
}
第一,CheckType新添加了构造方法,还有两个字段,key为int型,text为String型。
第二,CheckType中有一个public static CheckType parse(Integer index)
方法,可将一个Integer通过key的匹配转化为枚举类型。那么现在,我们可以在Mybatis的配置文件中使用typeHandler
将数据库字段转化为枚举类型。
<resultMap id="CheckLog" type="com.entity.CheckLog">
<id property="id" column="id"/>
<result property="checkType" column="check_type" typeHandler="com.CheckTypeHandler"></result>
</resultMap>
其中checkType字段对应的类如下:
public class CheckLog implements Serializable {
private String id;
private CheckType checkType;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public CheckType getCheckType() {
return checkType;
}
public void setCheckType(CheckType checkType) {
this.checkType = checkType;
}
}
CheckTypeHandler转换器的类源码如下:
public class CheckTypeHandler extends BaseTypeHandler<CheckType> {
@Override
public CheckType getNullableResult(ResultSet rs, String index) throws SQLException {
return CheckType.parse(rs.getInt(index));
}
@Override
public CheckType getNullableResult(ResultSet rs, int index) throws SQLException {
return CheckType.parse(rs.getInt(index));
}
@Override
public CheckType getNullableResult(CallableStatement cs, int index) throws SQLException {
return CheckType.parse(cs.getInt(index));
}
@Override
public void setNonNullParameter(PreparedStatement ps, int index, CheckType val, JdbcType arg3) throws SQLException {
ps.setInt(index, val.getKey());
}
}
CheckTypeHandler 的核心功能就是调用CheckType枚举类的parse()
方法对数据库字段进行转换。