枚举——用enum代替int常量

枚举类型(enum type) 

是指一组固定的常量组成合法值的类型,例如一年中的季节、太阳系中的行星或者一副牌中的花色。

int枚举模式的缺点

在我们平常的开发中,为表示同种类型的不同种类,经常的做法是声明一组具名的int常量来表示,每个类型成员一个常量,如:

public static final int DAY_MONDAY = 1;
public static final int DAY_TUESDAY = 2;
public static final int DAY_WEDNESDAY = 3;
public static final int DAY_THURSDAY = 4;
public static final int DAY_FRIDAY = 5;
public static final int DAY_SATURDAY = 6;
public static final int DAY_SUNDAY = 7;

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

这种方法称做 int枚举模式,这种方式在安全性和使用方便性方面没有任何帮助。

a、将day传到想要orange的方法中,编译器不会警告,执行也不会出现错误;
b、用==操作符将day与orange比较,编译器不会警告,执行也不会出现错误;
c、int枚举是编译时常量,被编译到客户端中,如果枚举常量关联的int发生变化,客户端必须重新编译,如果没有重新编译,程序仍可以运行,但行为就不确定了,如DAY_MONDAY关联的常量不再是1,而是0。
d、将int枚举常量翻译成可打印的字符串很麻烦
e、如果想要遍历一个组中的所有int 枚举常量,甚至获得int枚举组的大小,这种实现没有啥方便可靠的方法

因此,推荐使用枚举类型来代替这种int枚举常量:

public enum DAY {
    DAY_MONDAY, 
    DAY_TUESDAY, 
    DAY_WEDNESDAY,				
    DAY_THURSDAY, 
    DAY_FRIDAY, 
    DAY_SATURDAY, 
    DAY_SUNDAY
}
public enum ORANGE {
    ORANGE_NAVEL, 
    ORANGE_TEMPLE, 
    ORANGE_BLOOD
}

这种枚举类型,提供了编译时的类型安全检查,如果声明了一个参数的类型为DAY,就可以保证,被传到该参数上的任何非null的对象引用一定属于其他有效值中的一个,试图传递类型错误的值时,会导致编译时错误,就像试图将某种枚举类型的表达式赋给另一种枚举类型的变量,或者试图利用==操作符比较不同枚举类型的值一样。同时包含同名常量的多个枚举类型可以共存,因为每个类型有自己的命名空间,增加或重新排列枚举类型的常量,无需重新编译客户端的代码。如果想获取类型对应的字符串,直接通过toString方法即可。

enum枚举常量与行为关联

如采用枚举来写加、减、乘、除的运算。代码如下:

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;
            case DIVIDE: return x / y;
        }
        throw new AssertionError("Unknown op: " + this);
    }

    public static void main(String[] args) {
        Operation divide = Operation.DIVIDE;
        double apply = divide.apply(1, 2);
        System.out.println(apply);
    }

}

大家一开始都会这样写的。实际开发中,有很多开发者也这样写。但是有个不足:如果需要新增加运算,譬如模运算,不仅仅需要添加枚举类型常量,还需要修改apply方法。万一忘记修改了,那就是运行时错误。将代码修改如下: 

public enum Operation {
  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);

}

每次新增加运算种类,都需要重写apply方法,这样就不会有遗漏修改。

你可以写的更详细些:

public enum Operation {
 
  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 String symbol;

  abstract double apply(double x, double y);

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

  @Override
  public String toString() {
    return symbol;
  }
  
  public static void main(String[] args) {
    Operation minus = Operation.MINUS;
    double apply = minus.apply(1, 2);
    System.out.println(minus.toString());
    System.out.println(apply);
  }
}

一般,enum中重写了toString方法之后,enum中自生成的valueOf(String)方法不能根据枚举常量的字符串(toString生成)来获取枚举常量。

 Operation operation = Operation.valueOf("DIVIDE");

我们通常需要在enum中新增个静态常量来获取。如:

  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 String symbol;
  public static final Map<String, Operation> OPERS_MAP = new HashMap();

  static {
    for (Operation op : Operation.values()) {
      OPERS_MAP.put(op.toString(), op);
    }
  }

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

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

  abstract double apply(double x, double y);
}
public class OperationDemo {

  public static void main(String[] args) {
    double x = Double.parseDouble(args[0]);
    double y = Double.parseDouble(args[1]);

    for (Operation op : Operation.values()) {
      System.out.println(String.format("%f %s %f = %f%n", x, op, y, op.apply(x, y)));
    }

    //输入2 4
    //2.000000 + 4.000000 = 6.000000
    //2.000000 - 4.000000 = -2.000000
    //2.000000 * 4.000000 = 8.000000
    //2.000000 / 4.000000 = 0.500000
  }
}

可以通过调用Operation.OPERS_MAP.get(op.toString())来获取对应的枚举常量。

在有些特定的情况下,此写法有个缺点,即如果每个枚举常量都有公共的部分处理该怎么办,如果每个枚举常量关联的方法里都有公共的部分,那不仅不美观,还违反了DRY原则。这就是下面的枚举策略模式。

枚举策略模式

直接上例子来分析:

enum PayrollDay {
    MONDAY, 
    TUESDAY, 
    WEDNESDAY, 
    THURSDAY, 
    FRIDAY,
    SATURDAY, 
    SUNDAY;

    private static final int HOURS_PER_SHIFT = 8;

    double pay(double hoursWorked, double payRate) {
        double basePay = hoursWorked * payRate;

        double overtimePay; // Calculate overtime pay
        switch(this) {
            case SATURDAY: case SUNDAY:
                overtimePay = hoursWorked * payRate / 2;
                break;
            default: // Weekdays
                overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
        }

        return basePay + overtimePay;
    }
}

以上代码是计算工人工资。平时工作8小时,超过8小时,以加班工资方式另外计算;如果是双休日,都按照加班方式处理工资。

上面代码的写法和上一小节给出的差不多,通过switch来分拆计算。还是一样的问题,如果此时新增加一种工资的计算方式,枚举常量需要改,pay方法也需要改。按上一小节的介绍继续修改:

enum PayrollDay {
    MONDAY {
        @Override
        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                    0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
            return basePay + overtimePay;
        }
    }, 
    TUESDAY {
        @Override
        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                    0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
            return basePay + overtimePay;
        }
    }, 
    WEDNESDAY {
        @Override
        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                    0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
            return basePay + overtimePay;
        }
    }, 
    THURSDAY {
        @Override
        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                    0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
            return basePay + overtimePay;
        }
    }, 
    FRIDAY {
        @Override
        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                    0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
            return basePay + overtimePay;
        }
    },
    SATURDAY {
        @Override
        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            double overtimePay = overtimePay = hoursWorked * payRate / 2;
            return basePay + overtimePay;
        }
    }, 
    SUNDAY {
        @Override
        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            double overtimePay = overtimePay = hoursWorked * payRate / 2;
            return basePay + overtimePay;
        }
    }, ;

    private static final int HOURS_PER_SHIFT = 8;

    abstract double pay(double hoursWorked, double payRate);
}

 看了上面的代码,我觉得大家都不会这样写吧。其实细想一下,最主要的不同就是计算加班时间的工资方式不同,也就是分工作日和双休日的。继续修改:

public enum PayRoll {
  MONDY(PayType.WEEKDAY),
  TUESDAY(PayType.WEEKDAY),
  WEDNESDAY(PayType.WEEKDAY),
  THURSDAY(PayType.WEEKDAY),
  FRIDAY(PayType.WEEKDAY),
  SATURDAY(PayType.WEEKEND),
  SUNDAY(PayType.WEEKEND);

  private final PayType payType;
  PayRoll(PayType payType) {
    this.payType = payType;
  }

  double pay(double hoursWorked, double payRate) {
    return payType.pay(hoursWorked, payRate);
  }

  private enum PayType {
    WEEKDAY {
      @Override
      double overtimePay(double hoursWorked, double payRate) {
        double overtime = hoursWorked - HOURS_PER_SHIFT;
        return overtime <= 0 ? 0 : overtime * payRate / 2;
      }
    },

    WEEKEND {
      @Override
      double overtimePay(double hoursWorked, double payRate) {
        return hoursWorked * payRate / 2;
      }
    };

    private static final int HOURS_PER_SHIFT = 8;
    abstract double overtimePay(double hoursWorked, double payRate);

    double pay(double hoursWorked, double payRate) {
      double basePay = hoursWorked * payRate;
      return basePay + overtimePay(hoursWorked, payRate);
    }
  }
}

虽然看起来代码不够简洁,但是修改起来确实比较安全,不怕有遗漏。 

枚举的实现原理

枚举是通过共有的静态final域为每个枚举常量导出实例的类,因为没有可以访问的构造器,枚举类型是真正的final,因为客户端既不能创建枚举类型的实例,也不能对他进行扩展,因为很可能没有实例,而只有声明过的枚举常量。换句话说,枚举类型是实例受控的,他们是单例的泛型化,本质上是单元素的枚举。如我们上面的ORANGE枚举,
 

public enum ORANGE {
    ORANGE_NAVEL,
    ORANGE_TEMPLE,
    ORANGE_BLOOD;
}

在编译完成之后,生成的字节码反编译之后

public final class ORANGE extends Enum
{
 
    public static ORANGE[] values()
    {
        return (ORANGE[])$VALUES.clone();
    }
 
    public static ORANGE valueOf(String name)
    {
        return (ORANGE)Enum.valueOf(com/example/demo/ORANGE, name);
    }
 
    private ORANGE(String s, int i)
    {
        super(s, i);
    }
 
    public static final ORANGE ORANGE_NAVEL;
    public static final ORANGE ORANGE_TEMPLE;
    public static final ORANGE ORANGE_BLOOD;
    private static final ORANGE $VALUES[];
 
    static 
    {
        ORANGE_NAVEL = new ORANGE("ORANGE_NAVEL", 0);
        ORANGE_TEMPLE = new ORANGE("ORANGE_TEMPLE", 1);
        ORANGE_BLOOD = new ORANGE("ORANGE_BLOOD", 2);
        $VALUES = (new ORANGE[] {
            ORANGE_NAVEL, ORANGE_TEMPLE, ORANGE_BLOOD
        });
    }
}

可以看到

a、枚举类是继承于java.lang.Enum的类。
b、枚举值是类对象, 且是静态常量(被static final修饰)。
c、静态代码块内实例化枚举值,由于静态代码块的语法特性,该代码块只执行一次;
d、默认值0、1、2是在编译时生成的。

小结

总而言之,与int常量相比,枚举类型的优势是不言而喻的。枚举的可读性要好很多,也更加安全,功能也更加强大,许多枚举都不需要显示的构造器或成员,但许多其他枚举则受益于“每个常量与属性的关联”以及“提供行为受这个属性影响的方法”。只有极少数的枚举受益于将多种行为与单个方法关联,在这种相对少见的情况下,特定于常量的方法要优先于启用自由值的枚举,如果多个枚举常量同时共享相同的行为,则考虑策略枚举。
 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值