目录
一、枚举概念
二、枚举的应用
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 高速公路 城市快速路 一般公路
}
}
}
从改造的结果来看,我们可以总结出:
- 枚举常量的实例可以省略常量名称(即name属性)和常量索引(即ordinal属性),也可以指定默认的常量名称,此时不指定常量索引的话,会将当前所在枚举的位置当做默认索引;
- 通过name()方法只是获取了枚举常量的实例,而要获取枚举常量实例的字符串名称,则需要调用构造器内维护的那个成员变量才行;
- 枚举类重写了toString()方法,在执行RoadEnum.HIGH_ROAD 或 RoadEnum.HIGH_ROAD.toString()时就可以很清晰的打印枚举中信息;
- 基本类型可以用 == 比较,枚举常量的实例之间也可以用 == 比较,枚举常量的实例是唯一的,没必要使用equals()比较;
- 枚举作为一种有限的集合容器,可以通过静态方法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基础的一部分,值得花些时间去巩固,不积硅步无以至千里,点滴付出终将有所收获,共同进步吧 ~