从零开始深入理解Java枚举

​ 这一篇内容有点多,但是肯定会很有帮助,很多内容来自《Java核心技术》和《Effective Java》(刚学Java的时候,这本中文版的书非常不建议阅读,本来就不是很好理解,加上令人崩溃的翻译,但是主要内容都写在了这篇最后一章)另外还参考了《Java学习笔记》(这本书虽然没那么出名,但是读起来很容易理解,非常适合入门)还有一些其他资料就不说了,最后就是自己的一些理解。后面部分很多代码都没有使用idea,所以可能一点点会有笔误,当做是个学习笔记好了。

1. 枚举使用时机

  • 当需要一组固定常量的时候。包括天然的枚举,比如 行星,一周的天数,衣服的尺码等;
  • 你在编译的时候就知道其所有可能值的集合
  • 变量值仅在一个范围内变化(尤其是保护延伸意义的时候,比如SUMMER(2)中的2就表示一年中的第二个季节),使用enum(阿里小册子)。

2. Enum源码解读

所有的枚举类都是继承自Enum(将在后续看到),Enum源码不多,就全部扣下来了

public abstract class Enum<E extends Enum<E>>
    implements Comparable<E>, Serializable {
	//枚举常数名字
	private final String name;
	//枚举常数的序号,默认0开始
	private final int ordinal;
	//返回该枚举常数的名字
    public final String name() {
        return name;
    }
	//同上
	public String toString() {
        return name;
    }
	//返回该枚举常数的序号
	public final int ordinal() {
        return ordinal;
    }
	//构造函数,传人枚举名字和序号
	protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
	//返回指定名字的枚举类型常数,静态的,使用Emun直接来调用
	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);
    }
	//hashCode(),equals,compareTo(),getDeclaringClass()方法不需要了解了
}

定义一个动作枚举Action 如下:

public enum  Action {
    STOP,LEFT,RIGHT,UP,DOWN;
}

这个枚举通过反编译是可以看到继承了Enum的,下面测试Enum的几个常用方法,解释写在了注释中,

@Test
public void test1(){
    Action left = Action.LEFT;
    String leftName = left.name();//名称就是LEFT
    String leftToString = left.toString();//LEFT
    int ordinal = left.ordinal();//枚举实例`LEFT`在枚举类中处于第二个位置,所以序号是1
	
    Action right = Enum.valueOf(Action.class, "RIGHT");//实例RIGHT的名称就是RIGHT
}

​ 静态方法似乎不好用,待会会介绍替代方法.对于ordinal方法也没啥用,就如<>>中第31条所说的,不要去依赖ordinal方法产生的序号,可控性不强,通常使用实例域来替代(需要关联值的时候别使用ordinal)。name,toString通常我们也很少用到。这些问题如何解决后面会说到。

3. 枚举原理

​ 要看Action枚举如何工作的,反编译看看一目了然。

public final class Action extends Enum
{

    private Action(String s, int i)
    {
        super(s, i);
    }

    public static Action[] values()
    {
        Action aaction[];
        int i;
        Action aaction1[];
        System.arraycopy(aaction = ENUM$VALUES, 0, aaction1 = new Action[i = aaction.length], 0, i);
        return aaction1;
    }

    public static Action valueOf(String s)
    {
        return (Action)Enum.valueOf(com/scu/enu/Action, s);
    }

    public static final Action STOP;
    public static final Action RIGHT;
    public static final Action LEFT;
    public static final Action UP;
    public static final Action DOWN;
    private static final Action ENUM$VALUES[];

    static 
    {
        STOP = new Action("STOP", 0);
        RIGHT = new Action("RIGHT", 1);
        LEFT = new Action("LEFT", 2);
        UP = new Action("UP", 3);
        DOWN = new Action("DOWN", 4);
        ENUM$VALUES = (new Action[] {
            STOP, RIGHT, LEFT, UP, DOWN
        });
    }
}

Action被final修饰了,说明了枚举类是不能再被继承了。同时拥有STOP,LEFT,RIGHT,UP,DOWN4个常量,再静态块里面被初始化,也就是类加载的时候就会初始化完成(类加载最后一步)。构造方法第一个参数是name第二个参数是ordinal,这些默认值可控性差,似乎不好用。

​ 此外,注意到Action类,额外提供了valueOf方法来获取枚举实例,这样就不再需要Enum.valueOf()来获取枚举实例了;提供了values方法来获取所有枚举实例。下面就简单测试这两个方法。

@Test
public void test2(){
    Action[] actions = Action.values();
    for (Action action : actions) {
        System.out.println(action.name() + " " + action.ordinal());
    }
    /*
        输出
        STOP 0
        LEFT 1
        RIGHT 2
        UP 3
        DOWN 4
         */
    Action right = Action.valueOf("RIGHT");
}

4. 枚举的高级应用

4.1 自定义构造函数

​ 之前说了,我们可以不适用Enum给我们提供的name,ordinal。比如这里就先使用value来替换掉 ordinal,做法很简单,提供一个成员变量value,一个getValue方法,最需要注意的是提供一个私有的构造函数来初始化value,不得在构造函数里面调用super(反编译后能看到自行调用)。。

public enum Action2 {
    STOP(2), LEFT(3), RIGHT(5), UP(9), DOWN(7);
    private int value;

    public int getValue() {//访问value
        return value;
    }
    private Action2(int value) {
        this.value = value;
    }
}

​ 枚举的本质还是类,实例是固定的,所以不允许在外部随意去new,需要将构造函数用private来进行修饰,这一点是很容易理解的。但是为什么写成STOP(2)这种诡异的形式呢,实际上是将隐含的name,ordinal没有写出来,比如之前的STOP实际上应该是STOP("STOP",0)。将Action2反编译就很清楚了。

public final class Action2 extends Enum
{

    private Action2(String s, int i, int value)
    {
        super(s, i);
        this.value = value;
    }

    public int getValue()
    {
        return value;
    }

    public static Action2[] values()
    {
        Action2 aaction2[];
        int i;
        Action2 aaction2_1[];
        System.arraycopy(aaction2 = ENUM$VALUES, 0, aaction2_1 = new Action2[i = aaction2.length], 0, i);
        return aaction2_1;
    }

    public static Action2 valueOf(String s)
    {
        return (Action2)Enum.valueOf(com/scu/enu/Action2, s);
    }

    public static final Action2 STOP;
    public static final Action2 RIGHT;
    public static final Action2 LEFT;
    public static final Action2 UP;
    public static final Action2 DOWN;
    private int value;
    private static final Action2 ENUM$VALUES[];

    static 
    {
        STOP = new Action2("STOP", 0, 2);
        RIGHT = new Action2("RIGHT", 1, 3);
        LEFT = new Action2("LEFT", 2, 5);
        UP = new Action2("UP", 3, 9);
        DOWN = new Action2("DOWN", 4, 7);
        ENUM$VALUES = (new Action2[] {
            STOP, RIGHT, LEFT, UP, DOWN
        });
    }
}

​ 注意到构造函数实际上是private Action2(String s, int i, int value)带三个参数,最后一个是我们自己写的valuename,ordinal还是赋了初值,只是后续我们使用对应于枚举实例的序号是时不使用ordinal而是使用valuegetValue()

​ 如果现在要将name也替换成我们自己的,不加解释的给出如下修改。

public enum Action3 {

    //其实通常会使用名称而不是动词
    STOP("暂停",2),LEFT("左转",3),RIGHT("右转",5),UP("上跳",9),DOWN("蹲下",7);

    private int value;
    private String name;//和Enum的name属性重名没关系

    public int getValue() {
        return value;
    }
    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return this.name + " " + this.value;
    }

    private Action3(String name, int value) {
        this.name = name;
        this.value = value;
    }
}

@Test
public void t3(){
    Action3[] action3s = Action3.values();
    for (Action3 action : action3s) {
        System.out.println(action);
    }
    Action3 stop = Action3.valueOf("STOP");//注意 不能填"暂停"  应该是Enum中的name
    System.out.println(stop.getName()+" " + stop.getValue());
}
/*
暂停 2
左转 3
右转 5
上跳 9
蹲下 7
暂停 2
*/

​ 其实构造函数是下面这个样子,父类中的name改成了s

private Action5(String s, int i, String name, int value)
{
super(s, i);
this.value = value;
this.name = name;
}

public static Action5 valueOf(String s)
{
return (Action5)Enum.valueOf(com/scu/enu/Action5, s);
}

4.2 特点于常量的方法

​ 什么叫特点于常量的方法?这是《effective java》中文版中的,意思就是每个常量实例都有自己特定的方法。

​ 上述介绍的方法getName,getAge都过于简单,但是有时候又希望枚举拥有自己的特定的成员方法。可能你会说,跟普通的类一样直接定义方法就好了,比如像定义一个·Student类,定义show方法,里面写点逻辑,创建每个实例都可以调用show方法。但是枚举的实例是有限个数的,比如上面的例子就是5个实例,每个实例的作用各不相同,通常我们需要根据实例的不同执行方法的逻辑差异也很大,所以需要让每个常量拥有自己的特定的方法。

​ 比如有枚举类Operation,定义了±*/ 4枚举实例,你想要提供一个方法来执行每个常量所代表的算术运算。可以通过下面较简单的方法来实现。

public enum  Operation {
    PLUS, MINUS, TIMES, DIVIDE;
    double apply(double x, double y) {
        switch (this){
            case PLUS:
                return x + y;
            case MINUS:
                return x - y;
            case TIMES:
                return x * y;
            default:
                return x / y;
        }
    }
}
//测试
@Test
public void test1(){
    Operation minus = Operation.MINUS;
    double ans = minus.apply(10, 2);
    System.out.println(ans);//8.0
}

​ 注意:如果涉及到金额,需要精确数值那么就不能用double类型了,double不够精确,因为浮点数小数位其实都是由1/2^k分数来表示的,比如0.75=1/2+1/4,用BigDecimal即可(《effective java》中第48条也有说)。

​ 这段代码运行起来是没有问题的。但是这段代码很脆弱,如果你添加了新的枚举常量,却忘记给switch添加相应的条件,程序仍可以编译,但是运行的话就可能得到错误的结果。这时候就是使用特点于常量的方法即可。

​ 即:在枚举中声音一个apply的抽象方法,并在各常量类主体中,用具体的方法覆盖每个常量的抽象apply方法。

public enum Operation2 {
    PLUS {
        @Override
        double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS {
        @Override
        double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES {
        @Override
        double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDE {
        @Override
        double apply(double x, double y) {
            return x / y;
        }
    };
    abstract  double apply(double x, double y);
}

​ 这个时候如果你添加了个新操作比如幂运算常量EXP,编译器会自动提醒你去实现apply方法。甚至我们可以各个常量与特定的数据绑定起来,写成下面的形式。

public enum Operation3 {
    PLUS("+") {
        @Override
        double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-") {
        @Override
        double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES("*") {
        @Override
        double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDE("/") {
        @Override
        double apply(double x, double y) {
            return x / y;
        }
    };
    private final String symbol;

    public String getSymbol() {
        return symbol;
    }

    private Operation3(String symbol) {
        this.symbol = symbol;
    }

    abstract  double apply(double x, double y);
}
//测试
@Test
@Test
public void test3(){
    double x = 10.0;
    double y = 2.0;
    Operation3[] ops = Operation3.values();
    for (Operation3 operation : ops) {
        System.out.println(x + operation.getSymbol() + y + "=" + operation.apply(x,y));
    }
}
/*
10.0+2.0=12.0
10.0-2.0=8.0
10.0*2.0=20.0
10.0/2.0=5.0
*/

4.3 用接口模拟可伸缩的枚举

​ 这个标题的名字就是《effective java》中的第34条。枚举其实就是final 类,所以一旦定义了枚举类,该类就不能被继承了,在该类的基础上也就很难使用extends去拓展了。再看Operation枚举,提供了加减乘除4中基本运算,全都有自己特点的apply方法。加入另外有人需要定义幂运算,定义求余运算该怎么做。

​ 虽然枚举是不可拓展的,但是接口则可以进行拓展,所以只需要将apply操作抽取到接口中。你可以定义另一个枚举类型来实现这个接口,并使用新的枚举实例以及特定于该实例的apply方法即可。现在将上述Operation该写成下面的扩展版本,

public interface  Operation {
    double apply(double x, double y);
}

public enum BasicOperation implements Operation{
    PLUS("+") {
        @Override
        public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-") {
        @Override
        public double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES("*") {
        @Override
        public double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDE("/") {
        @Override
        public double apply(double x, double y) {
            return x / y;
        }
    };
    private final String symbol;

    public String getSymbol() {
        return symbol;
    }

    private BasicOperation(String symbol) {
        this.symbol = symbol;
    }
}

​ 如果你想要定义一个上述操作类型的拓展,由求幂运算和求余运算组成,需要做的就是定义枚举类型实现Operation接口。

public enum  ExtendedOperation implements Operation{
    EXP("^") {
        @Override
        public double apply(double x, double y) {
            return Math.pow(x,y);
        }
    },
    REMINDER("%") {
        @Override
        public double apply(double x, double y) {
            return x % y;
        }
    };
    private final String symbol;

    public String getSymbol() {
        return symbol;
    }

    private ExtendedOperation(String symbol) {
        this.symbol = symbol;
    }
}

​ 说明:使用接口模拟可伸缩枚举有个小小的不足,即无法实现从一个枚举类型继承另一个枚举类型。在上述例子中,symbol的初始化和获取操作,在BasicOperation,ExtendedOperation中是相同的,由于就这两个子类且共享的功能不多,所以可以将以下共享代码从BasicOperation复制到ExtendedOperation即可。但是如果共享功能很多的时候,或者子类很多的时候就需要将这些功能封装到辅助类或者静态辅助方法中去了,从而避免代码的复制工作。

private final String symbol;

public String getSymbol() {
return symbol;
}

private ExtendedOperation(String symbol) {
this.symbol = symbol;
}

4.4 枚举中的匿名内部类

对于4.2,4.3节中特点于枚举常量的方法的写法有没有感觉到很诡异,其实是编译器进行了处理,实际上就是使用了匿名内部类,反编译后就能看清楚了。比如DIVIDE常量实际上反编译后差不多是下面这个样子。

public static final BasicOperation DIVIDE;
static{
	DIVIDE = new BasicOperation("DIVIDE",3,"/"){
		public double apply(double x, double y) {
            return x / y;
        }
	}
}

5. 策略枚举

​ 特点于常量的方法实现有一个美中不足的地方,它们使得在枚举常量中共享代码变得更加困难了,这一点在马上就会通过例子说到。

​ 考虑这样一个需求,用一个枚举表示薪资包中的工作天数。这个枚举有一个方法,根据给定的某工人的基本工作和当天工作时间来计算当天的报酬。在五个工作日中,工作时间超过8小时都会产生加班工资;在双休日工作的全部算加班工资。通过switch语句很容易实现这个功能。

public enum  PayRollDay {
    MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY;
    private static final int BASE_WORK_TIME = 8;//每个正常工作时间
    public double pay(double hoursWorked, double payRate) {
        double basicPay = hoursWorked * payRate;//基本薪资
        double overtimePay;//加班费
        switch (this) {
            case SATURDAY:case SUNDAY:
                overtimePay = hoursWorked * payRate/2;
                break;
            default:
                overtimePay = hoursWorked > BASE_WORK_TIME?(hoursWorked -
                    BASE_WORK_TIME)*payRate/2:0;
                break;
        }
        return basicPay + overtimePay;
    }
}
//测试
@Test
public void test1(){
    PayRollDay saturday = PayRollDay.SATURDAY;
    double pay = saturday.pay(10, 10);
    System.out.println(pay);//150
}
  1. 这段代码还是很简洁明了的,但是从维护的角度来看却不行。假设将一个假日常量,比如国庆加入其中,但是没有加入对应的case,编译也会通过,但是就会被当做工作日计算了,这显然是不对的。这个问题在4.2节也说到过,但是使用特定于常量的方法来完成这个任务会存在下面的问题。
  2. 如果使用特定于常量的方法来姐姐switch带来的问题的话,那么就会出现很多重复带买吗,比如5个工作日中的特定的方法都是相同的,即default中的那堆代码,而周末和节假日中的代码也是重复的。这种大量重复并不好,但是特定于常量的方法确实在共享代码方面很差。
  3. 我们需要做的是,每添加一个常量,就强制选择一种加班策略,同时不需要太多重复代码。一种可靠的想法就是将加班工资移到一个私有的嵌套枚举中,将这个策略枚举的实例传到PayrollDay的构造方法中。PayrollDay枚举将加班工资计算委托给策略枚举,PayrollDay中就不需要switch语句或者特定于常量的方法实现了。
public enum PayRollDay2 {
    MONDAY(PayType.WEEKDAY),TUESDAY(PayType.WEEKDAY),WEDNESDAY(PayType.WEEKDAY),
    THURSDAY(PayType.WEEKDAY),FRIDAY(PayType.WEEKDAY),SATURDAY(PayType.WEEKEND),
    SUNDAY(PayType.WEEKEND);

    private PayType payType;
    private PayRollDay2(PayType payType) {
        this.payType = payType;
    }

    public double pay(double hoursWorked, double payRate) {
        return  payType.pay(hoursWorked,payRate);
    }
    //策略枚举类
    private enum PayType{
        WEEKDAY{
            @Override
            double overtimePay(double hoursWorked, double payRate) {
                return hoursWorked > BASE_WORK_TIME?(hoursWorked -
                        BASE_WORK_TIME)*payRate/2:0;
            }
        },
        WEEKEND {
            @Override
            double overtimePay(double hoursWorked, double payRate) {
                return hoursWorked * payRate/2;
            }
        };
        private static final int BASE_WORK_TIME = 8;
        abstract double overtimePay(double hoursWorked, double payRate);
        double pay(double hoursWorked, double payRate) {
            return hoursWorked * payRate + overtimePay(hoursWorked,payRate);
        }
    }
}
//测试
@Test
public void test2(){
    PayRollDay2 saturday = PayRollDay2.SATURDAY;
    double pay = saturday.pay(10, 8);
    System.out.println(pay);//120
}

6. 总结

​ 能使用常量的时候一定要先考虑到枚举。只有极少数的枚举受益于将多种行为与单个方法关联。特定于常量的方法要优先于自有值的枚举。如果多个枚举常量同时共享相同的行为,则考虑策略枚举。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值