第19章 枚举

枚举知识点

在这里插入图片描述

枚举的概念

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已经支持 intcharStringenum 类型的参数。这几种类型的参数比较起来,使用枚举的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中保持唯一性.

  1. 反射创建单例对象

解决方案 : 在构造上述中判断,当多于一个实例时,再调用构造函数,直接报错.

  1. 反序列化时创建对象

解决方案 : 使用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()方法对数据库字段进行转换。

发布了91 篇原创文章 · 获赞 43 · 访问量 14万+

猜你喜欢

转载自blog.csdn.net/kongfanyu/article/details/104096811