【Java5 特性】3.Java枚举

目录

一、枚举概念

二、枚举的应用

1、switch语句支持枚举类型

2、常量接口与枚举类的对比

3、枚举实现单例模式

4、使用接口组织枚举

四、EnumMap容器与EnumSet容器

1、EnumMap映射

2、EnumSet集合

小结


一、枚举概念

枚举(Enum)是Java5时引入的特性,本质上也是一个class类型,属于引用数据类型,定义时用enum关键字标识。简单的定义一个枚举类:(注意:命名规范强制要求使用Enum结尾,这样可以清晰表明它的类型!)

public enum CarEnum {
    AUDI, BMW, GEELY, KIA, CHANGAN
}

与普通类相比,枚举类有哪些不同呢?

  • 枚举类用enum关键字标识,而普通类用class关键字标识;
  • 所有自定义的枚举类均默认继承自java.lang.Enum,且自定义的枚举类无法extends其他枚举类,也不能被继承;
  • 枚举的实例是直接定义的,比如AUDI, BMW, GEELY等,而普通类一般是new的方式构造的;
  • 使用也很简单,通过 CarEnum.AUDI 方式使用枚举常量,而枚举常量在JVM中的实例是唯一的,因此可直接使用==进行枚举常量的比较;
  • switch多重选择语句支持枚举类型,不支持普通类;

正是因为枚举这些独特的特征,使得枚举具有简便性和安全性,Enum类及常用的API需要了解一下,找到java.lang包下Enum类的源码,简单分析一下:

/**
 * Enum 是一个抽象泛型类,实现了Comparable接口和Serializable接口,说明Enum可排序,可序列化!
 **/
public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
    /** 常量名称属性 */
    private final String name;
    /** 常量在Enum 中的位置索引属性 */
    private final int ordinal;
    /** 构造方法 */
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
    /** 获取字符串常量名 */
    public final String name() {return name;}
    /** 获取常量在Enum 中的位置 */
    public final String ordinal() {return ordinal;}
    /** 同 name() */
    public String toString() {return name;}
    /** toString()的逆方法:将字符串常量名设置为枚举常量 */
    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);
    }
    
    public final boolean equals(Object other) {return this==other;}
    public final int hashCode() {return super.hashCode();}
    public final int compareTo(E o) {...}
}

需要注意一下name()方法和toString()方法,它们都是获取枚举类的常量字符串名称,并且修饰符不同,因此自定义枚举类时可以重写toString()方法,但不能重写final的name()方法,建议使用name()方法获取字符串名称。另外,静态的valueOf()方法是与toString()方法相反的一种方法,即可以往指定枚举类添加枚举常量。自定义枚举类中有一个静态的values()方法值得关注,它返回的是一个包含全部枚举值的数组,比如:CarEnum[] carValues = CarEnum.values(); 。

二、枚举的应用

1、switch语句支持枚举类型

Java5除了新增枚举特性之外,还在switch语句内提供了对枚举类型的支持,举个例子说明:

public class Test {
    public static void main(String[] args) {
        // 当前版本Java8,switch支持类型有:枚举,字符串,char/byte/short/int及对应的包装类
        CarEnum car = CarEnum.GEELY;
        switch (car) {
            case AUDI:
                System.out.println("我是奥迪汽车!");
                break;
            case BMW:
                System.out.println("我是宝马汽车!");
                break;
            case GEELY:
                System.out.println("我是吉利汽车!");
                break;
            case CHANGAN:
                System.out.println("我是长安汽车!");
                break;
            default:
                System.out.println("我是东风起亚汽车!");
        }
    }
}

2、常量接口与枚举类的对比

常量是一种定义后值不能再被改变的变量,实际开发中都会去统一管理我们用到的常量,比如会使用一个类或一个接口去维护,以常量接口Road为例:

public interface Road {
    /**
     * 道路平坦度
     */
    char ROAD_WORD_1 = '1';
    char ROAD_WORD_2 = '2';
    /**
     * 道路等级
     */
    int ROAD_LEVEL_1 = 1;
    int ROAD_LEVEL_2 = 2;
    /**
     * 道路类型名称
     */
    String HIGH_ROAD = "高速公路";
    String FAST_ROAD = "城市快速路";
    String MIDDLE_ROAD = "一般公路";
}

public class Test {
    public static void main(String[] args) {
        int level = 1;
        System.out.println("每日一问,你走的是什么路:" + Road.HIGH_ROAD);
        if (level == Road.ROAD_WORD_1)
            System.out.println("不同基本类型的常量能进行比较????");
        if (level == Road.ROAD_LEVEL_1)
            System.out.println("目前道路等级达标!");
    }
}

Road常量接口中可以定义任意Java类型的常量,常量均省略了默认的修饰符public static final,常量名称均大写且多个单词要使用下划线隔开,直接通过 接口名.常量名 调用,在不同类型的常量进行比较时,编译器只是提示了建议移除if表达式,没有报错。

如果使用枚举类型存储这些常量的话,如何改造上面的常量接口Road呢?定义一个枚举类RoadEnum:

public enum RoadEnum {
    /** 枚举常量只指定了默认名称,而索引顺序省略的话,则默认按照当前的位置顺序 */
    ROAD_WORD_1('1'), ROAD_WORD_2('2'),
    ROAD_LEVEL_1(1), ROAD_LEVEL_2(2),
    HIGH_ROAD("高速公路"), FAST_ROAD("城市快速路"), MIDDLE_ROAD("一般公路");

    /** 常量名称 */
    public char charName;
    public int intName;
    public String strName;

    /** 通过构造器初始化常量名称 */
    RoadEnum(char charName) {
        this.charName = charName;
    }

    RoadEnum(int intName) {
        this.intName = intName;
    }

    RoadEnum(String strName) {
        this.strName = strName;
    }

    /** 便于打印和追踪枚举信息,强制要求重写toString() */
    @Override
    public String toString() {
        return "RoadEnum{" +
                "charName=" + charName +
                ", intName=" + intName +
                ", strName='" + strName + '\'' +
                '}';
    }
}

public class Test {
    public static void main(String[] args) {
//        System.out.println("每日一问,你走的是什么路:" + RoadEnum.HIGH_ROAD); // RoadEnum{charName= , intName=0, strName='高速公路'}
        System.out.println("每日一问,你走的是什么路:" + RoadEnum.HIGH_ROAD.ordinal()); // 4
//        System.out.println("每日一问,你走的是什么路:" + RoadEnum.HIGH_ROAD.name()); // HIGH_ROAD
//        System.out.println("每日一问,你走的是什么路:" + RoadEnum.HIGH_ROAD.toString()); // RoadEnum{charName= , intName=0, strName='高速公路'}
        System.out.println("每日一问,你走的是什么路:" + RoadEnum.HIGH_ROAD.strName); // 高速公路

        /** 比较测试 */
        int level = 1;
        if (level == RoadEnum.ROAD_WORD_1.charName)
            System.out.println("不同基本类型的常量能进行比较????");
        if (level == RoadEnum.ROAD_LEVEL_1.intName)
            System.out.println("目前道路等级达标!");
        if (RoadEnum.ROAD_WORD_1 == RoadEnum.ROAD_LEVEL_1)
            System.out.println("枚举常量实例之间的比较");

        /** 遍历枚举 */
        RoadEnum[] roadEnums = RoadEnum.values();
        System.out.print("枚举集合内的元素有:");
        for (int i = 0; i < roadEnums.length; i++) {
//            System.out.print(roadEnums[i].charName + " "); // 12
//            System.out.print(roadEnums[i].intName + " "); // 0 0 1 2 0 0 0
            System.out.print(roadEnums[i].strName + " "); // null null null null 高速公路 城市快速路 一般公路 
        }
    }
}

从改造的结果来看,我们可以总结出:

  1. 枚举常量的实例可以省略常量名称(即name属性)和常量索引(即ordinal属性),也可以指定默认的常量名称,此时不指定常量索引的话,会将当前所在枚举的位置当做默认索引;
  2. 通过name()方法只是获取了枚举常量的实例,而要获取枚举常量实例的字符串名称,则需要调用构造器内维护的那个成员变量才行;
  3. 枚举类重写了toString()方法,在执行RoadEnum.HIGH_ROAD 或 RoadEnum.HIGH_ROAD.toString()时就可以很清晰的打印枚举中信息;
  4. 基本类型可以用 == 比较,枚举常量的实例之间也可以用 == 比较,枚举常量的实例是唯一的,没必要使用equals()比较;
  5. 枚举作为一种有限的集合容器,可以通过静态方法values()获取枚举数组,而遍历获取某一类型的枚举常量时,其他的枚举常量则全部设为默认值(比如:int默认0,String默认null)。

3、枚举实现单例模式

使用枚举,是实现单例模式的最简单方式,而且不会存在如 —— 反序列化时重新生成新对象破坏单例模式、反射时会强行调用构造器实例化单例类 等问题,推荐使用。代码实现如下:

public enum SingleInstanceEnum {
    INSTANCE; 
}

@Test
void testEnum(){
    SingleInstanceEnum instance = SingleInstanceEnum.INSTANCE;
}

哈哈哈,这里突然强迫症有些犯了,为什么枚举能避免上述提及的那两个问题呢,这篇作为专门整理枚举基础知识的文章,有必要去深究一下这些问题其中的原因。

枚举与反序列化

从上面Enum枚举的源码中可以看到:Enum类实现了Serializable接口,说明枚举实例是可序列化的,序列化输出的内容是枚举实例的name属性。而反序列化时是否也会像普通的类那样会生成新的对象呢?答案是不会。那么枚举实例在序列化过程中是一种什么状态呢,先看一下Enum类中的静态方法valueOf(),其源码的核心代码为:T result = enumType.enumConstantDirectory().get(name); ,意思是它会根据传入的 枚举实例的引用(enumType) 和 枚举常量的字符串名称(name) 返回一个枚举实例(T),其中,enumType调用enumConstantDirectory()方法返回一个Map集合,该Map集合存储了以name属性为key,以枚举实例为value的数据。

再看一下enumConstantDirectory()方法的源码,将获取的枚举实例存在了一个Map结构中,然后赋值给了另一个用transient标识的不参与序列化的Map,这里就真相大白了。由此可以得知:枚举实例本质上存放在了不参与序列化过程的Map结构中,反序列化后仍然是这些枚举实例,且枚举实例是唯一的。因此,使用枚举实现单例模式是高效的,安全的。

Map<String, T> enumConstantDirectory() {
    if (enumConstantDirectory == null) {
        // 获取枚举实例,并检查合法性
        T[] universe = getEnumConstantsShared();
        if (universe == null)
            throw new IllegalArgumentException(
                getName() + " is not an enum type");
        // 通过Map存储枚举实例:以name为key,以枚举常量为value
        Map<String, T> m = new HashMap<>(2 * universe.length);
        for (T constant : universe)
            m.put(((Enum<?>)constant).name(), constant);
        // 将该Map设置为不参与序列化的Map
        enumConstantDirectory = m;
    }
    return enumConstantDirectory;
}
/** transient标识该Map不会参与序列化 */ 
private volatile transient Map<String, T> enumConstantDirectory = null;

枚举与反射

定义一个普通的VO类和一个枚举类,通过.class属性的方式获取反射对象,测试如下:

public class Test {
    public static void main(String[] args) 
            throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        OrderVO order = OrderVO.class.newInstance();
        System.out.println(order.toString()); // Order@b1a58a3

        CarEnum car = CarEnum.class.newInstance();
        System.out.println(car.toString()); // java.lang.InstantiationException

        CarEnum car1 = CarEnum.class.getConstructor().newInstance();
        System.out.println(car1.toString()); // java.lang.NoSuchMethodException
    }
}

普通的VO类获取反射结构是正常的,而枚举类CarEnum分别通过Class类的newInstance()、构造器类的newInstance()方式均不能获取反射对象,从debug调试源码可知,Class类的newInstance()本质上运行的是构造器类的newInstance(),当断点最后运行到Class类的newInstance()方法中的 final Constructor c = getConstructor0(empty, Member.DECLARED);  c.setAccessible(true); 等处时抛出了NoSuchMethodException异常,具体异常信息为:com.it.entity.CarEnum.<init>(),注意<init>()是个啥?它看起来像泛型方法(我猜可能是一种泛型构造器??),接着联想起了运行期间泛型类型会被擦除,JVM只会处理原始类型的类或方法,然而通过反射出现了<init>(), JVM应该是没办法处理了,所以直接抛了异常。这说明枚举不能通过反射的方式获取构造器并进行初始化成员,因此枚举实现单例模式是可靠的。

4、使用接口组织枚举

上面为了改造常量接口Road,定义了一个枚举类RoadEnum,该枚举类中大致定义了三种类型的常量,直观上给人感觉耦合度较高,且不优雅。这里利用接口组织枚举的方式,更优雅的使用枚举,改造如下:

public interface RoadE2I {
    /** 道路平坦度枚举 */
    enum WordEnum {
        ROAD_WORD_1('1'), ROAD_WORD_2('2');
        public char charName;

        WordEnum(char charName) {
            this.charName = charName;
        }
    }

    /** 道路等级枚举 */
    enum LevelEnum {
        ROAD_LEVEL_1(1), ROAD_LEVEL_2(2);
        public int intName;

        LevelEnum(int intName) {
            this.intName = intName;
        }
    }
    /** 道路类型枚举 */
    enum SpeedEnum {
        HIGH_ROAD("高速公路"), FAST_ROAD("城市快速路"), MIDDLE_ROAD("一般公路");
        public String strName;

        SpeedEnum(String strName) {
            this.strName = strName;
        }
    }
}

public class Test {
    public static void main(String[] args) {
        System.out.println("--------->" + RoadE2I.WordEnum.ROAD_WORD_1.charName); // 1
        System.out.println("--------->" + RoadE2I.LevelEnum.ROAD_LEVEL_2.intName); // 2
        System.out.println("--------->" + RoadE2I.SpeedEnum.HIGH_ROAD.strName); // 高速公路
    }
}

四、EnumMap容器与EnumSet容器

1、EnumMap映射

EnumMap顾名思义是专门为枚举设计的一种Map结构,想要了解和使用它,还得看一下源码,分析见源码注释:

/**
 * EnumMap:key做了泛型上界限定,必须是枚举类型或枚举类型的子类型,
 *    继承自AbstractMap,具有Map通用的特点,又实现了Serializable、Cloneable等接口。
 *    这里只重点分析构造方法/get()/put()/remove()等方法
 **/
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
    implements java.io.Serializable, Cloneable
{
    /** 枚举类型 */
    private final Class<K> keyType;
    /** 枚举常量实例 ,做为EnumMap的key*/
    private transient K[] keyUniverse;
    /** 枚举实例的name属性 ,做为EnumMap的value*/
    private transient Object[] vals;
    /** EnumMap大小 */
    private transient int size = 0;

    /** EnumMap构造器入参有三种方式:Class<K>、 EnumMap、普通的Map,以Class<K>为例*/
    public EnumMap(Class<K> keyType) {
        this.keyType = keyType;
        keyUniverse = getKeyUniverse(keyType);
        vals = new Object[keyUniverse.length];
    }

    /** 添加k-v */
    public V put(K key, V value) {
        typeCheck(key);

        int index = key.ordinal();
        Object oldValue = vals[index];
        vals[index] = maskNull(value);
        if (oldValue == null)
            size++;
        return unmaskNull(oldValue);
    }

    /** 根据key获取value */
    public V get(Object key) {
        return (isValidKey(key) ?
                unmaskNull(vals[((Enum<?>)key).ordinal()]) : null);
    }

    /** 根据key删除value */
    public V remove(Object key) {
        if (!isValidKey(key))
            return null;
        int index = ((Enum<?>)key).ordinal();
        Object oldValue = vals[index];
        vals[index] = null;
        if (oldValue != null)
            size--;
        return unmaskNull(oldValue);
    }

    /** 判断是否存在指定key */
    public boolean containsKey(Object key) {...}
    /** 判断是否存在指定value */
    public boolean containsValue(Object value) {...}

    /** 验证key的合法性 */
    private boolean isValidKey(Object key) {...}
    /** key的类型检查 */
    private void typeCheck(K key) {...}
    /** 将value中的null标记为NULL */
    private Object maskNull(Object value) {return (value == null ? NULL : value);}
    /** 取消value中标记的NULL */
    private V unmaskNull(Object value) {return (V)(value == NULL ? null : value);}

    /** 返回所有的value */
    public Collection<V> values() {...}
    /** 返回带Map的Set集合 */
    public Set<Map.Entry<K,V>> entrySet() {...}
    /** 返回Set集合 */
    public Set<K> keySet() {...}
}

EnumMap中通过put()方法存放键值对k-v,get()方法获取键的值,remove()删除键值对,containsKey()/containsValue()判断key/value是否存在,及遍历等操作,当然也可以使用其他的Map如HashMap操作 ,EnumMap具有Map的通用特性,只不过EnumMap是面向Enum对象操作的,demo演示:

public class Test {
    public static void main(String[] args) {
        EnumMap<CarEnum, Long> map = new EnumMap<>(CarEnum.class);
        // 添加
        map.put(CarEnum.AUDI, 250000L);
        map.put(CarEnum.BMW, 650000L);
        map.put(CarEnum.GEELY, 147000L);
        // 获取
        System.out.println(map.get(CarEnum.GEELY)); // 147000L
        System.out.println(map.get(CarEnum.CHANGAN)); // null
        // 计算花费总和
        Long sum = map.get(CarEnum.AUDI) + map.get(CarEnum.BMW) + map.get(CarEnum.GEELY);
        System.out.println(sum); // 1047000
        // 判断和删除
        if (map.containsKey(CarEnum.KIA))
            map.remove(CarEnum.KIA);
        // 遍历值 
        map.values().forEach(r -> System.out.print(r + " ")); // 250000 650000 147000
        // 遍历枚举实例
        map.keySet().forEach(s -> System.out.print(s + " ")); // AUDI BMW GEELY
        // 遍历EnumMap
        for (Map.Entry<CarEnum, Long> ce : map.entrySet()) {
            System.out.println(ce.getKey().name() + ":" + ce.getValue()); // AUDI:250000 BMW:650000 BMW:650000
        }
    }

EnumMap的键Key维护的是一个泛型数组K[],准确的说是枚举类型的数组,用于存放枚举常量实例,而值Value维护的是一个对象数组,用于存放枚举常量实例的名称(即name属性)。数组的优势在于访问速度快,在EnumMap中的put()、remove()、get()巧妙地将枚举实例,在枚举类中的索引位置(即ordinal属性),与在数组上的位置(即index属性)一 一对应了起来,不需要进行像HashMap那样的hash计算了(或者说EnumMap不存在哈希冲突问题)。增删改查等操作时,通过该索引可以快速定位数组元素,因此,EnumMap的效率高且没有额外的内存空间开销。

2、EnumSet集合

EnumSet是为枚举专门设计的一种Set集合,同样地,通过源码去了解和使用EnumSet:

public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable
{
    /** EnumSet存储的元素类型 */
    final Class<E> elementType;
    /** 枚举类型 */
    final Enum<?>[] universe;
    /** EnumSet 构造器 */
    EnumSet(Class<E>elementType, Enum<?>[] universe) {
        this.elementType = elementType;
        this.universe    = universe;
    }
    
    /** 通过noneOf()方式创建一个空的EnumSet */
    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");
        // 这里是选择操作的实现类Set集合
        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }

    /** 通过allOf()方式创建一个包含所有枚举值的EnumSet */
    public static <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) {...}
    /** 通过copyOf()方式复制一个 EnumSet 或  Collection*/
    public static <E extends Enum<E>> EnumSet<E> copyOf(EnumSet<E> s) {...}
    public static <E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c) {...}
    /** 通过complementOf()方式创建一个 与传入EnumSet不包含的枚举值 的新EnumSet*/
    public static <E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s) {...}
    /** 通过range()方式创建一个 具有指定区间的枚举值 的EnumSet*/
    public static <E extends Enum<E>> EnumSet<E> range(E from, E to) {...}
    /** 通过of()方式创建一个 包含一个或多个枚举值 的EnumSet*/
    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) {...}
    ...
    @SafeVarargs
    public static <E extends Enum<E>> EnumSet<E> of(E first, E... rest) {...}
}

这里EnumSet只是一个抽象类,继承了AbstractSet类,说明EnumSet的实现类具有Set集合通用的特性。EnumSet类中提供了许多创建EnumSet集合的静态方法,具体用法已注释。有两个实现类RegularEnumSet和JumboEnumSet,在实现类中提供了remove()、add()、size()、contains()、iterator()及交并差操作等方法,使用时与其他的Set集合并没有多大差别,只不过EnumSet是面向枚举的。

小结

本文专门对Java枚举做了一番整理,从枚举基础的概念,到使用场景,再到枚举容器都进行了深入的分析,对枚举有了一些更深入而全面的理解。作为Java基础的一部分,值得花些时间去巩固,不积硅步无以至千里,点滴付出终将有所收获,共同进步吧 ~

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值