coding……
但行好事 莫问前程

Java编程拾遗『枚举类』

由于双十一的缘故,忙着各种压测、演练以及值班,最近一直没什么时间写文章。还好,基本上双十一算是很平稳的度过了。作为电商平台,双十一带来的流量冲击,还是给我留下深刻的印象,特别订单、支付链路的环节,流量瞬间可以达到平时峰值的两倍以上。关于双十一这种突发流量冲击的解决方案,我自己还在了解中,本来想写一篇关于双十一的博文的,无奈了解还不够深入,先占个坑,回头补上。继续进行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);

参考链接:

  1. 深入理解 Java 枚举类型,这篇文章就够了
  2. Java的枚举类型用法介绍

 

赞(0) 打赏
Zhuoli's Blog » Java编程拾遗『枚举类』
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址