由于双十一的缘故,忙着各种压测、演练以及值班,最近一直没什么时间写文章。还好,基本上双十一算是很平稳的度过了。作为电商平台,双十一带来的流量冲击,还是给我留下深刻的印象,特别订单、支付链路的环节,流量瞬间可以达到平时峰值的两倍以上。关于双十一这种突发流量冲击的解决方案,我自己还在了解中,本来想写一篇关于双十一的博文的,无奈了解还不够深入,先占个坑,回头补上。继续进行Java编程拾遗这一系列文章的写作,本篇文章来介绍一下Java中一种常见且特殊的类——枚举类。
枚举是一类具有有限取值的数据类型,比如一周的性别、七天、一年的四季等。在Java 5引入枚举类之前,通常用一组int常量表示枚举。如下所示,表示性别的枚举类中,1表示男性,2表示女性:
public class Sex {
public static final int MALE = 1;
public static final int FEMALE = 2;
public static String getDesc(int sexCode) {
String desc = null;
switch (sexCode) {
case 1:
desc = "MALE";
break;
case 2:
desc = "FEMALE";
break;
}
return desc;
}
}
类中添加了一个方法getDesc,通过Sex的枚举int值获取对code的含义。然后通过如下方式调用:
String desc0 = Sex.getDesc(Sex.MALE); //没问题
String desc1 = Sex.getDesc(3); //编译、运行都正常,但是结果非法
很明显,这种方式下无法保证类型安全。另外,我们通过这个类只能获取一堆int值,可读性也不好。枚举类的出现,很好地解决了上述问题。
1. 枚举类定义
Java中枚举是一个比较特殊的类型,既具有class的特性,又具有自己特殊的特性。定义枚举类型使用enum关键字,如下所示:
public enum SexEnum {
MALE,FEMALE
}
可以通过如下方式调用:
public static void main(String[] args) {
SexEnum sex = SexEnum.MALE;
//枚举类中,name和toString返回内容一样
System.out.println(sex.toString()); //MALE
System.out.println(sex.name()); //MALE
//枚举类中 == 和 equals效果一样,直接使用 == 即可
System.out.println(sex == SexEnum.MALE); //true
System.out.println(sex.equals(SexEnum.MALE)); //true
System.out.println(sex == SexEnum.FEMALE); //false
//ordinal()方法,表示枚举值声明的顺序,从0开始
System.out.println(sex.ordinal()); //0
System.out.println(SexEnum.FEMALE.ordinal()); //1
//枚举类中实现了Comparable接口,枚举值之间可以通过compareTo比较,比较的就是ordinal()
System.out.println(sex.compareTo(SexEnum.FEMALE)); //-1
}
上面就是枚举对象的通用方法,在注释中都说明了,不特殊讲述了。枚举变量可以和普通类型变量一样使用,比如类变量、实例变量等、方法参数,枚举还可以用于switch语句,如下:
public static String getSexDesc(SexEnum sex) {
String sexDesc = null;
switch (sex) {
case MALE:
sexDesc = "Male……";
break;
case FEMALE:
sexDesc = "Female……";
break;
default:
sexDesc = "Not Exit";
break;
}
return sexDesc;
}
跟上面的getDesc方法相比,使用枚举值的这个方法明显是类型安全的,因为入参只能是Sex.MALE或者Sex.Female,永远走不到default的逻辑。另外要注意的是,在switch语句内部,枚举值不能带枚举类型前缀,例如,直接使用MALE,不能使用SexEnum.SMALL。下面看一下枚举类中,编译器为我们自动生成的两个比较重要的方法values和valueOf,如下:
//values()用于获取枚举所有的值
for (SexEnum s : SexEnum.values()) {
System.out.println(s.name());
}
//valueOf(String)用于根据枚举名称反查枚举值,如果名称在枚举类中不存在,报IllegalArgumentException
System.out.println(SexEnum.valueOf("MALE")); //MALE
综上,我们可以看到枚举类有以下好处:
- 枚举类是类型安全的,一个枚举类型的变量,它的值要么为null,要么为枚举值之一,不可能为其他值
- 枚举类型自带很多便利方法(如values, valueOf, toString等),易于使用
2. 枚举类的实现原理
上面讲过枚举类其实也是一种类,只是声明方式跟普通的类不太一样。实际上枚举类会被编译器编译为一个对应的类,这个类继承了java.lang.Enum类,Enum类的定义如下:
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
private final String name;
private final int ordinal;
public final String name() {
return name;
}
public final int ordinal() {
return ordinal;
}
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
public String toString() {
return name;
}
public final boolean equals(Object other) {
return this==other;
}
public final int hashCode() {
return super.hashCode();
}
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
public final int compareTo(E o) {
Enum<?> other = (Enum<?>)o;
Enum<E> self = this;
if (self.getClass() != other.getClass() && // optimization
self.getDeclaringClass() != other.getDeclaringClass())
throw new ClassCastException();
return self.ordinal - other.ordinal;
}
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);
}
//……
}
可以看到,Enum类中有两个重要的成员变量name和ordinal,name(), toString(), ordinal(), compareTo(), equals()方法都是由Enum类根据其实例变量name和ordinal实现的。
另外可以看一下为什么Enum类的泛型声明如此奇怪,<E extends Enum<E>>,为什么不定义成 public abstract class Enum extends Object 呢?因为Enum需要实现Comparable接口。那似乎也可以定义成 public abstract class Enum<E>extends Object implements Comparable<E>, Serializable 。但仔细想,就会发现会有问题,Comparable 的compareTo()方法是这样的: public final int compareTo(E o),如果定义成这样,泛型擦除之后参数就会变为Object,我们肯定不希望一个枚举值跟任意对象进行比较,这是没有意义的。所以给了一个上界限定,这样被擦除之后,E被替换成Enum。
枚举类型编译后生成一个 class并且继承 Enum类,反编译枚举类SexEnum的字节码,对字节码进行还原可以得到如下的class。
public final class SexEnum extends Enum<SexEnum> {
public static final SexEnum MALE = new SexEnum("MALE",0);
public static final SexEnum FEMALE = new SexEnum("FEMALE",1);
private static SexEnum[] VALUES =
new SexEnum[]{MALE,FEMALE};
private SexEnum(String name, int ordinal){
super(name, ordinal);
}
public static SexEnum[] values(){
SexEnum[] values = new SexEnum[VALUES.length];
System.arraycopy(VALUES, 0,
values, 0, VALUES.length);
return values;
}
public static SexEnum valueOf(String name){
return Enum.valueOf(SexEnum.class, name);
}
}
- SexEnum有一个私有的构造方法,接受name和ordinal,传递给父类,私有表示不能在外部创建新的实例。
- 两个枚举值在类中实际是两个final静态变量,不能被修改。
- values方法是编译器添加的,内部有一个values数组保持所有枚举值。
- valueOf方法也是编译器添加的,调用了父类Enum的方法,额外传递了参数Size.class(用于反射),表示类的类型信息。
3. 枚举类的应用场景
3.1 枚举值添加属性
我们使用枚举的目的是希望可以方便、直观的使用枚举值实际代表的值。比如我们约定MALE在数据库里表示代码0上述枚举可以表示,但是如果我们约定MALE对应的代码是101,上述所以上面介绍的SexEnum就无法工作了。所以在使用枚举时,我们一般会自定义一些属性,用于描述枚举值,比如定义一个表示一年四季的枚举,除了枚举类自有的oridinal和name属性,我们给每个枚举值又添加了code和desc属性,如下:
public enum Season {
SPRING(1, "春天"),
SUMMER(2, "夏天"),
AUTUMN(3, "秋天"),
WINTER(4, "冬天");
// 成员变量
private int code;
private String desc;
// 构造方法
Season(int code, String desc) {
this.code = code;
this.desc = desc;
}
// 普通方法
public static String getDesc(int code) {
for (Season s : Season.values()) {
if (s.getCode() == code) {
return s.getDesc();
}
}
return null;
}
// get set 方法
public int getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
注意,这里的构造器是没有修饰符的,其实默认是private的,只是这里不用显示声明了。编译器编译后会默认生成一个private的构造函数,如下:
private Season(String name, int ordinal, int code, String desc) {
super(name, ordinal);
this.code = code;
this.desc = desc;
}
3.2 表示常量
使用枚举值表示常量也是比较常见的一个用法,用法就是最基础的那种用法,如下:
public enum Size {
SAMLL, MEDIUM, LARGE, EXTRA_LARGE
}
3.3 switch
这个用法上面讲过,如下:
public static String getSexDesc(SexEnum sex) {
String sexDesc = null;
switch (sex) {
case MALE:
sexDesc = "Male……";
break;
case FEMALE:
sexDesc = "Female……";
break;
default:
sexDesc = "Not Exit";
break;
}
return sexDesc;
}
要注意的是,在switch语句内部,枚举值不能带枚举类型前缀,例如,直接使用MALE,不能使用SexEnum.SMALL。
3.4 枚举类中定义抽象方法
枚举类定义中,可以声明一个抽象方法,每个枚举值中可以实现该方法,这种用法通常用来实现不同枚举值的特有行为。如下:
public enum Size {
SMALL {
@Override
public void onChosen() {
System.out.println("chosen small");
}
}, MEDIUM {
@Override
public void onChosen() {
System.out.println("chosen medium");
}
}, LARGE {
@Override
public void onChosen() {
System.out.println("chosen large");
}
}, EXTRA_LARGE {
@Override
public void onChosen() {
System.out.println("chosen extra_large");
}
};
public abstract void onChosen();
}
这样每个枚举值的onChosen方法都试各自实现的,可以给枚举值添加一些”个性”的属性。可以通过如下方式使用:
Size size = Size.SMALL;
switch (size) {
case SMALL:
size.onChosen();
break;
case MEDIUM:
size.onChosen();
break;
case LARGE:
size.onChosen();
break;
case EXTRA_LARGE:
size.onChosen();
break;
}
3.5 使用接口组织枚举
public interface Food {
enum Coffee implements Food{
BLACK_COFFEE,DECAF_COFFEE,LATTE,CAPPUCCINO
}
enum Dessert implements Food{
FRUIT, CAKE, GELATO
}
}
然后就可以这样使用了:
Food.Coffee coffee = Food.Coffee.BLACK_COFFEE;
Food.Dessert dessert = Food.Dessert.CAKE;
3.6 单例
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
根据之前分析的枚举类的特性,可以知道,这种方式可以保证只实例化一个实例INSTANCE。
4. 枚举容器
枚举类存在两种容器,EnumSet和EnumMap。EnumSet 是一个枚举集合,EnumMap是一个使用枚举值做key的Map实现。相比于普通Set集合和Map,EnumSet和EnumMap处理枚举类型数据更高效。下面简单介绍以下用法。
4.1 EnumSet
EnumSet 是一个枚举集合,是一个抽象类,它有两个继承类:JumboEnumSet和RegularEnumSet。与普通的HashSet不同,比如HashSet内部使用HashMap实现,EnumSet的实现与EnumMap没有任何关系,使用极为精简和高效的位向量实现,效率高于HashSet。有很多静态工厂方法可以用来构造EnumSet对象,如下:
// 初始集合包括指定枚举类型的所有枚举值
public static <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType)
// 初始集合包括枚举值中指定范围的元素
public static <E extends Enum<E>> EnumSet<E> range(E from, E to)
// 初始集合包括指定集合的补集
public static <E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s)
// 初始集合包括参数中的所有元素
public static <E extends Enum<E>> EnumSet<E> of(E e)
public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2)
public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3)
public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4)
public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5)
public static <E extends Enum<E>> EnumSet<E> of(E first, E... rest)
// 初始集合包括参数容器中的所有元素
public static <E extends Enum<E>> EnumSet<E> copyOf(EnumSet<E> s)
public static <E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c)
可以通过如下方式调用:
//创建一个包含指定元素类型的所有元素的枚举 set
EnumSet<SexEnum> setAll = EnumSet.allOf(SexEnum.class);
//创建一个指定范围的Set
EnumSet<SexEnum> setRange = EnumSet.range(SexEnum.MALE,SexEnum.FEMALE);
//创建一个指定枚举类型的空set
EnumSet<SexEnum> setEmpty = EnumSet.noneOf(SexEnum.class);
//复制一个set
EnumSet<SexEnum> setNew = EnumSet.copyOf(setRange);
另外Set集合的一些基础操作,EnumSet也是支持的,比如交集、差集操作。
4.2 EnumMap
EnumMap是一个使用枚举值做key的Map实现,可以通过如下方式构建一个EnumMap实例:
//创建一个具有指定键类型的空枚举映射
EnumMap<SexEnum, String> map1 = new EnumMap<>(SexEnum.class);
//从一个 EnumMap 创建
EnumMap<SexEnum, String> map2 = new EnumMap<SexEnum, String>(map1);
//从一个 Map 创建
Map<SexEnum, ? extends String> map3 = new HashMap<>();
EnumMap<SexEnum, String> map4 = new EnumMap<SexEnum, String>(map3);
参考链接: