1、为何使用Java枚举
Java5之前没有enum的情况下,我们一般使用几个int常量表示枚举值(int枚举类型)或者string枚举类型
public static final ZOO_CAT=1 #表示小猫
public static final ZOO_CAT=2 #表示小狗
如作为方法参数时,zooType只能是int类型
如 public void printZoo(int zooType){
}
这样的话 我们调用方法时只要是int类型就行 ,就没法控制方法调用的安全性
其次 作为一个数字 如果打印到日志中就是个裸数字,没有可读性
为此 java5 产生了enum , 固定且有限的类型都可以使用enum。
2、如何使用java枚举


package com.yangfei.test.meiju; public class EnumYf { enum ZOO{ CAT,DOG,PIG } public static void printZoo(ZOO zooType){ System.out.println("my name is " + zooType); } public static void main(String[] args){ printZoo(ZOO.DOG); } }
如上 一个简单的例子 EnumYf中定义了一个动作枚举类型。
当一个枚举是公共的分类时 我们应该定义单独的文件定义,如:
public enum xxx{
x1,x2,x3
}
3、枚举其实就是一个类
3.1 枚举就是一个类
定义一个enum


package com.yangfei.test.meiju; public class EnumYf { enum ZOO{ CAT,DOG,PIG } public static void printZoo(ZOO zooType){ System.out.println("my name is " + zooType); } public static void main(String[] args){ printZoo(ZOO.DOG); System.out.println(ZooEnum.CAT); } }
对编译生成的class文件 使用javap -p -c xx.class查看生成的字节码文件:
可以看到:
- enum编译后 实际生成了一个class ,此类是final的且继承抽象类Enum
- 构造方法时private 表面枚举是没法外部在new一个新对象的
- 枚举值实际就是当前类的几个常量实例
所以我们使用枚举是没法再继承其他类的,但可以实现接口。
3.2 我们再看看Enum抽象类的结构
实现了Comparable和Serializable接口 表面Enum是有内在的排序和可序列化的
我们看几个方法:
a. compareTo方法
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;
}
compareTo实际比较的是ordinal
b. ordinal方法
直接返回ordinal属性值 这个值表示在enum中枚举值的定义顺序 从0开始。
看java源码说明。一般用在EnumSet和EnumMap中 ,外部不要轻易使用ordinal(万一枚举定义顺序变了咋办!)
/**
* The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is assigned
* an ordinal of zero).
*
* Most programmers will have no use for this field. It is designed
* for use by sophisticated enum-based data structures, such as
* {@link java.util.EnumSet} and {@link java.util.EnumMap}.
*/
private final int ordinal;
4、枚举常用场景
4.1 values方法
values方法会返回所有的枚举实例,数组类型。
这个方法不是Enum类中的方法,是编译器编译中自动添加的方法。如下所示:
4.2 作为方法的入参
如:


public class EnumYf { public static void printZoo(ZooEnum zooEnum){ System.out.println(zooEnum.name()); } public static void main(String[] args){ printZoo(ZooEnum.DOG); } }
4.3 switch判断
如


public class EnumYf { public static void printZoo(ZooEnum zooEnum){ switch (zooEnum){ case DOG: System.out.println("this is dog"); break; case CAT: System.out.println("this is cat"); break; case PIG: System.out.println("this is pig"); break; } } public static void main(String[] args){ printZoo(ZooEnum.DOG); } }
5、枚举的序列化
枚举是可序列化的
但枚举的序列化比较特殊,序列化时只会将枚举的name(就是一字符串)序列化
反序列时 根据枚举的valueOf(String name)找到对应的枚举常量,也就是说即使反序列化也不会生成新的实例
这点对单例模式就有绝对的保护 ,单例默认参考博主另一篇博文:https://www.cnblogs.com/yangfei629/p/11370732.html
6、特定于常量的方法实现
看代码:


1 package com.yangfei.test.meiju; 2 3 public enum ZooEnum { 4 CAT("xiaomao"){ 5 @Override 6 public void printZoo() { 7 System.out.println("my name is xiaomao"); 8 } 9 }, 10 DOG("xoiaogou"){ 11 @Override 12 public void printZoo() { 13 System.out.println("my name is xoiaogou"); 14 } 15 }, 16 PIG("xiaozhu"){ 17 @Override 18 public void printZoo() { 19 System.out.println("my name is xiaozhu"); 20 } 21 }; 22 23 private String name; 24 ZooEnum(String name) { 25 this.name = name; 26 } 27 28 /** 29 * 抽象方法 具体实现在每个枚举中 30 */ 31 public abstract void printZoo(); 32 }
枚举中有抽象方法 每个枚举中有具体的实现
这么做通常用于每个枚举值有不同的实现。
同时在新增枚举值时 编译器会要求必须新增方法实现,防止漏实现。
7、枚举实现接口
枚举是final的 即不能被继承也不能继承其他类,但可以实现接口。


1 package com.yangfei.test.meiju; 2 3 public enum ZooEnum implements ZooInterface{ 4 CAT("xiaomao"), 5 DOG("xoiaogou"), 6 PIG("xiaozhu"); 7 8 private String name; 9 ZooEnum(String name) { 10 this.name = name; 11 } 12 13 /** 14 * 实现接口中方法 15 * @return 16 */ 17 @Override 18 public String getName() { 19 return name; 20 } 21 }
如上示例,enum可以实现接口,不仅仅接口方法,实际上可以像类一样定义任何方法和属性,因为其本质就是一个class。
8、EnumSet和EnumMap
针对枚举 JDK提供了专门的set和map , 更高效(后文详解),建议使用。
8.1 EnumMap使用
EnumMap的key就是枚举实例 不可以为null,但value可以为null,value可以为任何类型。


public static void main(String[] args){ EnumMap<ZooEnum,String> enumMap = new EnumMap(ZooEnum.class); enumMap.put(ZooEnum.DOG,"xiaogou"); enumMap.put(ZooEnum.CAT,"xiaomao"); System.out.println(enumMap.get(ZooEnum.DOG)); }
对于key为枚举类型时,使用EnumMap比HashMap更高效。因为EnumMap内部使用数组直接存取 时间复杂度为O(1)
看结构内部使用keyUniverse和vals两个数组
keyUniverse存放枚举所有的常量
vals放入枚举作为key所对应的值
数组的下标是使用枚举的oridinal(表示枚举的顺序 从0开始),这样存取记录时可以直接根据key的ordinal找到在数组中存储的位置。
/**
* All of the values comprising K. (Cached for performance.)
*/
private transient K[] keyUniverse;
/**
* Array representation of this map. The ith element is the value
* to which universe[i] is currently mapped, or null if it isn't
* mapped to anything, or NULL if it's mapped to null.
*/
private transient Object[] vals;
8.2 EnumSet的使用
EnumSet 用于存放相同枚举类型实例的集合
集中常用用法,如下


public static void main(String[] args){ //方式一:建立空的Set 再往其中添加枚举 EnumSet<ZooEnum> enumSet1 = EnumSet.noneOf(ZooEnum.class); enumSet1.add(ZooEnum.DOG); enumSet1.add(ZooEnum.CAT); System.out.println("enumSet1="+enumSet1); //方式二:取所有的枚举值放入set EnumSet<ZooEnum> enumSet2 = EnumSet.allOf(ZooEnum.class); System.out.println("enumSet2="+enumSet2); //方式三:取部门的枚举值放入set EnumSet<ZooEnum> enumSet3 = EnumSet.of(ZooEnum.DOG,ZooEnum.PIG); System.out.println("enumSet3="+enumSet3); enumSet3.forEach(tempEle -> System.out.println(tempEle)); }
输出:
enumSet1=[CAT, DOG]
enumSet2=[CAT, DOG, PIG]
enumSet3=[DOG, PIG]
DOG
PIG
看看EnumSet的结构:
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
implements Cloneable, java.io.Serializable
它是一个抽象类 有2种实现类RegularEnumSet和JumboEnumSet
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");
if (universe.length <= 64)
return new RegularEnumSet<>(elementType, universe);
else
return new JumboEnumSet<>(elementType, universe);
}
枚举常量个数<=64的情况:使用RegularEnumSet (一般我们使用枚举绝大多情况不会超过64)
枚举常量个数>64的情况:使用JumboEnumSet
RegularEnumSet: 使用位向量表示枚举集合
我们看add方法:
public boolean add(E e) {
typeCheck(e);
long oldElements = elements;
elements |= (1L << ((Enum<?>)e).ordinal());
return elements != oldElements;
}
添加一个枚举就是把1L左移(移动位数=当前枚举值的ordinal值);再与当前的保持值elements求或运算。
elements值默认为0,long类型占用64位,每位用1来表示一个枚举值是否存在
如果添加第一个枚举值,则elements为如下值:
如果再添加第3个枚举值,elements则为:
JumboEnumSet:
超过了64个枚举值 则用long数组表示
如下右移6位 相等于除以64 算出数组所需长度。
JumboEnumSet(Class<E>elementType, Enum<?>[] universe) {
super(elementType, universe);
elements = new long[(universe.length + 63) >>> 6];
}
还是看add方法:
public boolean add(E e) {
typeCheck(e);
int eOrdinal = e.ordinal();
int eWordNum = eOrdinal >>> 6;
long oldElements = elements[eWordNum];
elements[eWordNum] |= (1L << eOrdinal);
boolean result = (elements[eWordNum] != oldElements);
if (result)
size++;
return result;
}
先通过枚举的ordinal>>>6 右移6位得到的就是数组的索引下标
1L<<eOrdinal 溢出后得到的值 当做位向量处理
对于每个数组元素就是一个long类型 跟RegularEnumSet处理逻辑一致
全程都巧妙的用位运算来操作 所以比较高效。
注意EnumSet和EnumMap并不是线程安全的,多线程时可用锁或者Collections.synchronizedMap保护