Loading

Java枚举类型记录

枚举和注解

1. 枚举

枚举类型( enum type )是指由一组固定的常量组成合法值的类型,本质上是int值

Ⅰ. 用enum代替int常量

(1)int枚举模式

// FruitConsts.java
/**
 * @author cph
 *
 * <p>int 枚举模式</p>
 */
public class FruitConsts {
    public static final int APPLE_FUJI = 0;
    public static final int APPLE_PIPPIN = 1;
    public static final int APPLE_GRANNY_SMITH = 2;

    public static final int ORANGE_NAVEL = 0;
    public static final int ORANGE_TEMPLE = 1;
    public static final int ORANGE_BLOOD = 2;
}

缺陷:

  1. 不具备安全性
// 比如,int枚举模式可以参与运算,还可以进行==比较,这是极其不安全的。
@Test
public void testIntEnum() {
    int i = (FruitConsts.APPLE_FUJI - FruitConsts.ORANGE_TEMPLE) / 	
        FruitConsts.APPLE_PIPPIN;
}
  1. 是编译时常量,它的int值会被编译到使用它们的客户端中,如果关联的值发生变化,则客户端必须重新编译

  2. 没有描述性可言。很难将int枚举常量转换成可打印的字符串(String 枚举模式是int枚举模式的一种变体,会导致性能问题,因为它依赖字符串的比较)

(2)枚举类型

// Apple.java
/**
 * @author cph
 */
public enum Apple {
    /**
     * 苹果的品牌分类
     */
    FUJI,
    PIPPIN,
    GRANNY_SMITH
}

// Orange.java
/**
 * @author cph
 */
public enum Orange {
    /**
     * 桔子的品牌分类
     */
    NAVEL,
    TEMPLE,
    BLOOD
}

优势:

  1. 本质上是int值
  2. 没有可访问的构造器,是真正的final类
  3. 单例的泛型化,本质上是单元素的枚举
  4. 保证了编译时的类型安全
// Operation1.java
/**
 * @author cph
 *
 * <p>
 *     这段代码很脆弱,如果添加了新的枚举类型,却忘记给switch添加条件,在运用新的运算时,就会运行失败
 * </p>
 */
public enum Operation1 {

    /**
     * 加、减、乘、除、取余
     */
    PLUS, MINUS, TIMES, DIVIDE, MOD;

    public double apply(double x, double y) {
        switch (this) {
            case PLUS: return x + y;
            case MINUS: return x - y;
            case TIMES: return x * y;
            case DIVIDE: return x / y;
            default:
        }
        // 没有throw语句,代码就无法编译
        throw new AssertionError("Unknown op: " + this);
    }

}

// 测试
@Test
public void testOperation1() {
    double apply = Operation1.PLUS.apply(1, 2);
    System.out.println(apply);

    /*
    使用MOD运算符时引发:java.lang.AssertionError: Unknown op: MOD

    double apply1 = Operation1.MOD.apply(3, 2);
    System.out.println(apply1);
    */
}

上述代码可以使用特定于常量的方法实现,如下:

// Operation2.java
/**
 * @author cph
 *
 * <p>
 *     对第一版运算符枚举进行了改进。
 *     在添加新的常量时,就不会忘记提供apply方法,因为编译器会提示你抽象方法必须被覆盖,否则编译不通过。
 *     避免了运算操作不存在而导致程序报错。
 * </p>
 */
public enum Operation2 {
    /**
     * 加、减、乘、除、取余
     */
    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;
        }
    },
    MOD {
        @Override
        public double apply(double x, double y) {
            return x % y;
        }
    };

    public abstract double apply(double x, double y);
}

// 测试
@Test
public void testOperation2() {
    // 运行成功, apply = 1.0
    double apply = Operation2.MOD.apply(3, 2);
    System.out.println(apply);
}

经过改进,可以将特定于常量的方法与特定于常量的数据结合起来,如下:

// Operation3.java 
/**
 * @author cph
 *
 * <p>
 * 特定的常量方法实现可以与特定于常量的数据结合起来
 * </p>
 */
public enum Operation3 {
    /**
     * 加、减、乘、除、取余
     */
    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;
        }
    },
    MOD("%") {
        @Override
        public double apply(double x, double y) {
            return x % y;
        }
    };

    private static final Map<String, Operation3> STRING_TO_ENUM = Stream.of(values()).
        collect(Collectors.toMap(Object::toString, op -> op));

    private final String symbol;

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

    @Override
    public String toString() {
        return symbol;
    }

    public abstract double apply(double x, double y);

    public static Optional<Operation3> fromString(String symbol) {
        return Optional.ofNullable(STRING_TO_ENUM.get(symbol));
    }

}

// 测试
@Test
public void testOperation3() {
    for (Operation3 op : Operation3.values()) {
        double x = 3.0, y = 2.0;
        double apply = op.apply(x, y);
        System.out.printf("%.2f %s %.2f = %.2f%n", x, op, y, apply);
    }
}
/*
测试结果:
3.00 + 2.00 = 5.00
3.00 - 2.00 = 1.00
3.00 * 2.00 = 6.00
3.00 / 2.00 = 1.50
3.00 % 2.00 = 1.00
*/

使用Lambda表达优化后,代码更加简洁:

// Operation.java
public enum Operation {
    /**
     * 加、减、乘、除、取余
     */
    PLUS("+", (x, y) -> x + y),
    MINUS("-", (x, y) -> x - y),
    TIMES("*", (x, y) -> x * y),
    DIVIDE("/", (x, y) -> x / y),
    MOD("%", (x, y) -> x % y);

    private final String symbol;
    private final DoubleBinaryOperator op;

    Operation(String symbol, DoubleBinaryOperator op) {
        this.symbol = symbol;
        this.op = op;
    }

    @Override
    public String toString() {
        return symbol;
    }

    public double apply(double x, double y) {
        return op.applyAsDouble(x, y);
    }
}
  1. 包含同名常量的多个枚举类型可以在一个系统中和平共处,因为每个类型都有自己的命名空间

Ⅱ. 策略枚举

​ 特定于常量的方法实现有一个美中不足的地方,它们使得在枚举常量中共享代码变得 更加困难了。比如下面的例子:

// PayrollDay1.java
/**
 * @author cph
 *
 * <p>
 *     计算一周的工资
 *     优势:
 *       - 代码简洁
 *     劣势:
 *       - 难以维护。
 *         如果添加了新的枚举元素,没有在switch中添加相应语句,就会导致计算结果出错。
 *         为了实现安全的计算,可能会增加样板代码,降低可读性,增加出错概率。
 * </p>
 */
public enum PayrollDay1 {
    /**
     * 周一至周日
     */
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
    SATURDAY, SUNDAY;

    public static final int MINS_PER_SHIFT = 8 * 60;

    int pay(int minuteWorked, int payRate) {
        int basePay = minuteWorked * payRate;
        int overtimePay;
        switch (this) {
            case SATURDAY:
            case SUNDAY:
                overtimePay = basePay / 2;
                break;
            default:
                overtimePay = minuteWorked <= MINS_PER_SHIFT ? 0 
                    : (minuteWorked - MINS_PER_SHIFT) * payRate / 2;
        }
        return basePay + overtimePay;
    }

}

使用策略枚举优化上述例子,如下:

// PayrollDay2.java
/**
 * @author cph
 *
 * <p>
 *     <b>策略枚举</b>改进上述计算工资的方法。
 *
 *     这种模式虽然没有switch语句简洁,但是更加安全,更加灵活
 * </p>
 */
public enum PayrollDay2 {
    /**
     * 周一至周日
     */
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
    SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);

    private final PayType payType;

    PayrollDay2(PayType payType) {
        this.payType = payType;
    }

    PayrollDay2() {
        this(PayType.WEEKDAY);
    }

    int pay(int minutesWorked, int payRate) {
        return payType.pay(minutesWorked, payRate);
    }

    /**
     * 策略枚举
     */
    private enum PayType {
        /**
         * 加班类型:工作日,周末
         */
        WEEKDAY{
            @Override
            int overtimePay(int minsWorked, int payRate) {
                return minsWorked <= MINS_PER_SHIFT ? 0 
                    : (minsWorked - MINS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            @Override
            int overtimePay(int minsWorked, int payRate) {
                return minsWorked * payRate / 2;
            }
        };
        public static final int MINS_PER_SHIFT = 8 * 60;

        abstract int overtimePay(int minsWorked, int payRate);

        int pay(int minsWorked, int payRate) {
            int basePay = minsWorked * payRate;
            return  basePay + overtimePay(minsWorked, payRate);
        }
    }
}

Ⅲ. 使用实例域代替序数

​ ordinal()方法返回的序数是根据枚举在枚举类型中的顺序决定的,当更换顺序时,就会使ordinal()的返回值改变,可能会造成不必要的Bug,一般情况下,尽量避免使用ordinal方法

/**
 * @author cph
 *
 * <title>
 *     使用实例域代替序数
 *     每个枚举都有一个ordinal()方法,返回每个枚举常量在类型中的位置。
 *     - 一般情况下,尽量避免使用ordinal方法。
 * </title>
 */
public enum Ensemble {
    // 1 - 10, 12
    SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
    SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
    NONET(9), DECTET(10), TRIPLE_QUARTET(12);
    private final int numberOfMusicians;

    Ensemble(int numberOfMusicians) {
        this.numberOfMusicians = numberOfMusicians;
    }

    public int numberOfMusicians() {
        return numberOfMusicians;
    }
}

Ⅳ. 用接口模拟可扩展的枚举

虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础 枚举类型来对它进行模拟。比如:

// Operation.class
/**
 * @author cph
 * create datetime 2021/6/20 20:06
 */
public interface Operation {
    double apply(double x, double y);
}

// BasicOperation.class
/**
 * @author cph
 */
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;

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

    @Override
    public String toString() {
        return symbol;
    }
}

// ExtendedOperation.class
/**
 * @author cph
 */
public enum ExtendedOperation implements Operation{
    /**
     * 求幂、取余
     */
    EXP("^") {
        @Override
        public double apply(double x, double y) {
            return Math.pow(x, y);
        }
    },
    MOD("%") {
        @Override
        public double apply(double x, double y) {
            return x % y;
        }
    };

    private final String symbol;

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

    @Override
    public String toString() {
        return symbol;
    }
}

// 测试
@Test
public void test() {
    double x = 3.0;
    double y = 2.0;
    operate(Arrays.asList(ExtendedOperation.values()), x, y);
    operate(Collections.singletonList(BasicOperation.PLUS), x, y);
}

private void operate(Collection<? extends Operation> opSet, double x, double y) {
    opSet.forEach(op -> System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y)));
}

/*
3.000000 ^ 2.000000 = 9.000000
3.000000 % 2.000000 = 1.000000
3.000000 + 2.000000 = 5.000000
*/

源代码

参考:《Effective Java》

posted @ 2021-06-15 22:11  kosihpc  阅读(62)  评论(0编辑  收藏  举报