版权声明:本文系博主原创,未经博主许可,禁止转载。保留所有权利。
引用网址:https://www.cnblogs.com/zhizaixingzou/p/10050579.html
目录
1. 枚举
1.1. 枚举的概念
在我们网购的过程中,订单在某个时刻将始终处于几种状态中的一种,它们分别为待付款、已取消、已付款、已发货、交易完成、已退货等。类似这样的例子还有很多,即在描述某个事物时,它的属性总是可列举并且可能值是固定且有穷的,所谓枚举,就是列举出所有可能的值的过程。可以看到,枚举是个动词,然而其要义则是得到目标集合,所以直接将此目标集合划为重点,也叫枚举了,此时它是一个名词,用来描述事物属性的范围。
1.2. 枚举的引入
枚举类型是在JDK1.5引入的。
在此之前,描述枚举的现象是通过常量实现的,如仍以订单状态为例,则1代表待付款、2代表已取消、3代表已付款、4代表已发货、5代表交易完成、6代表已退货等。总之,我们要将枚举在整数范围内做一个映射,并不直观。作为弥补,往往要写注释或文档,描述什么数字代表什么状态。
我们知道,程序设计的要点在于更精确地模拟现实世界,而引入枚举达到了这个目的。引入后,我们只需要将可能用到的值集合在一起,而不用映射一个更大范围的集合,而且描述上也更加地体现自注释,因而也更好,这就是引入枚举的初衷。
1.3. 枚举的常规使用
这里定义一个打印接口。
1 package com.cjw.learning.enumeration; 2 3 public interface Printer { 4 void println(); 5 }
这里为前面讲的订单状态定义一个枚举类型。
1 package com.cjw.learning.enumeration; 2 3 public enum OrderStatus implements Printer { 4 UNPAID("unpaid", "goods are ready, wait to be paid"), 5 CANCELED("canceled", "goods are ready, but canceled at last"), 6 PAID("paid", "goods are payed"), 7 SHIPPED("shipped", "goods are in the way"), 8 DOWN("down", "goods are accepted by buyer"), 9 REFUNDED("refunded", "goods are returned for some reason"); 10 11 private String id; 12 private String description; 13 14 OrderStatus(String id, String description) { 15 this.id = id; 16 this.description = description; 17 } 18 19 public static OrderStatus fromId(String id) { 20 for (OrderStatus orderStatus : values()) { 21 if (orderStatus.id.equals(id)) { 22 return orderStatus; 23 } 24 } 25 return null; 26 } 27 28 public String getId() { 29 return id; 30 } 31 32 public String getDescription() { 33 return description; 34 } 35 36 public void println() { 37 System.out.println(toString()); 38 } 39 40 @Override 41 public String toString() { 42 return "OrderStatus [ id = " + id + ", description = " + description + "]"; 43 } 44 }
枚举的使用。
1 package com.cjw.learning.enumeration; 2 3 public class Demo01 { 4 private static void process(OrderStatus orderStatus) { 5 switch (orderStatus) { 6 case UNPAID: 7 // do something. 8 break; 9 case CANCELED: 10 // do something. 11 break; 12 case PAID: 13 // do something. 14 break; 15 case SHIPPED: 16 // do something. 17 break; 18 case DOWN: 19 // do something. 20 break; 21 case REFUNDED: 22 // do something. 23 break; 24 default: 25 throw new IllegalArgumentException("Unknown status"); 26 } 27 } 28 29 public static void main(String[] args) { 30 process(OrderStatus.fromId(args[0])); 31 } 32 }
枚举可以实现接口。
如果枚举类型要添加自定义方法,则枚举常量的定义必须放在最前面。
枚举常量的标识符全大写,如果是几个单词组成则单词间使用下划线分隔。
对于枚举值的判断,使用switch是极为自然的,此语法在JDK1.6引入。
枚举的公共基类是默认的,是java.lang.Enum。
枚举的构造方法默认是私有的。
每个枚举常量还可以重写父类的方法,定义自己的方法,只是此时的枚举常量实际上是枚举的子类了,而且是匿名子类。
1.4. 枚举的公共基类java.lang.Enum
1.4.1. 源码解析
Enum定义如下。
1 public abstract class Enum<E extends Enum<E>> 2 implements Comparable<E>, Serializable {
枚举实现了Comparable接口,不同枚举比较的前提是两个枚举值同类型,比较的结果依照其在枚举类型中定义的顺序。
1 public final int compareTo(E o) { 2 Enum<?> other = (Enum<?>)o; 3 Enum<E> self = this; 4 if (self.getClass() != other.getClass() && // optimization 5 self.getDeclaringClass() != other.getDeclaringClass()) 6 throw new ClassCastException(); 7 return self.ordinal - other.ordinal; 8 }
判断两个同类型的枚举值是否相等,可以直接用==号,equals方法等价。
1 public final boolean equals(Object other) { 2 return this==other; 3 }
枚举常量是单例的,为了保证这点,枚举直接不支持克隆方法。
1 protected final Object clone() throws CloneNotSupportedException { 2 throw new CloneNotSupportedException(); 3 }
根据枚举类型和枚举值名称得到枚举常量。
1 public static <T extends Enum<T>> T valueOf(Class<T> enumType, 2 String name) { 3 T result = enumType.enumConstantDirectory().get(name); 4 if (result != null) 5 return result; 6 if (name == null) 7 throw new NullPointerException("Name is null"); 8 throw new IllegalArgumentException( 9 "No enum constant " + enumType.getCanonicalName() + "." + name); 10 }
进一步跟踪代码,返回值乃是根据枚举值名称查询Map<枚举值名称, 枚举常量>得到。
1 Map<String, T> enumConstantDirectory() { 2 if (enumConstantDirectory == null) { 3 T[] universe = getEnumConstantsShared(); 4 if (universe == null) 5 throw new IllegalArgumentException( 6 getName() + " is not an enum type"); 7 Map<String, T> m = new HashMap<>(2 * universe.length); 8 for (T constant : universe) 9 m.put(((Enum<?>)constant).name(), constant); 10 enumConstantDirectory = m; 11 } 12 return enumConstantDirectory; 13 }
而此Map实际上是通过指定的枚举类型的反射调用values方法得到的数组转换而来。
1 T[] getEnumConstantsShared() { 2 if (enumConstants == null) { 3 if (!isEnum()) return null; 4 try { 5 final Method values = getMethod("values"); 6 java.security.AccessController.doPrivileged( 7 new java.security.PrivilegedAction<Void>() { 8 public Void run() { 9 values.setAccessible(true); 10 return null; 11 } 12 }); 13 @SuppressWarnings("unchecked") 14 T[] temporaryConstants = (T[])values.invoke(null); 15 enumConstants = temporaryConstants; 16 } 17 // These can happen when users concoct enum-like classes 18 // that don't comply with the enum spec. 19 catch (InvocationTargetException | NoSuchMethodException | 20 IllegalAccessException ex) { return null; } 21 } 22 return enumConstants; 23 }
1.4.2. java.lang.Enum的使用实例
1 package com.cjw.learning.enumeration; 2 3 public class Demo02 { 4 public static void main(String[] args) { 5 for (OrderStatus orderStatus : OrderStatus.values()) { 6 System.out.println(orderStatus.name() + ": " + orderStatus.ordinal()); 7 8 } 9 10 OrderStatus unpaid = OrderStatus.fromId("paid"); 11 System.out.println(unpaid == OrderStatus.PAID); 12 // 这里调用equals方法时让枚举常量在前,而非枚举变量在前,乃是一种好习惯:可以避免抛空指针异常。 13 System.out.println(OrderStatus.PAID.equals(unpaid)); 14 15 System.out.println(OrderStatus.UNPAID.compareTo(OrderStatus.PAID)); 16 System.out.println(OrderStatus.PAID.compareTo(OrderStatus.PAID)); 17 18 System.out.println(Enum.valueOf(OrderStatus.class, "PAID")); 19 System.out.println(OrderStatus.valueOf("PAID")); 20 } 21 }
输出如下:
1.5. 专为枚举设计的集合类
1.5.1. Key为枚举的Map:java.util.EnumMap
1.5.1.1. Key为枚举类型
它的Key必须来自同一个枚举类型,当Map创建时指定。
1 public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> 2 implements java.io.Serializable, Cloneable
1.5.1.2. 存储结构
如下分别存储枚举类型、所有枚举常量数组、Map的值集合(个数与枚举常量数一致,数组索引号即枚举常量的定义顺序)、Map当前的大小(为某个枚举常量放入了值,则加一,反之则减一)。
1 private final Class<K> keyType; 2 private transient K[] keyUniverse; 3 private transient Object[] vals; 4 private transient int size = 0;
1.5.1.3. 创建实例
定义实例。
1 public EnumMap(Class<K> keyType) { 2 this.keyType = keyType; 3 keyUniverse = getKeyUniverse(keyType); 4 vals = new Object[keyUniverse.length]; 5 }
此时会初始化Key的类型等值,可以看到值集合全为null,表示Map为空。
1.5.1.4. 存入键值对(null封装的巧妙)
在添加键值对时,首先检查Key是否是创建时指定的枚举类型,接着将值封装后存入指定的位置,对应的旧值如果是null,则大小加一,表示存入值了。
1 public V put(K key, V value) { 2 typeCheck(key); 3 4 int index = key.ordinal(); 5 Object oldValue = vals[index]; 6 vals[index] = maskNull(value); 7 if (oldValue == null) 8 size++; 9 return unmaskNull(oldValue); 10 }
可以看到,一般的Map的大小是动态增减的,而EnumMap不是,而是一开始就创建了指定枚举常量数的数组。区分是否添加了值,是通过为null还是不为null来的,而真正存放null,则是通过封装为NULL来表示的,这是一个技巧。
1 private static final Object NULL = new Object() { 2 public int hashCode() { 3 return 0; 4 } 5 6 public String toString() { 7 return "java.util.EnumMap.NULL"; 8 } 9 };
存入时,如果为null,则存NULL,以区别于没有存放null值。
1 private Object maskNull(Object value) { 2 return (value == null ? NULL : value); 3 }
读取时,如果得到的NULL,则表示之前存入的是null值。
1 private V unmaskNull(Object value) { 2 return (V)(value == NULL ? null : value); 3 }
1.5.1.5. 读取键对应的值
首先检查键的类型,如果不对,则返回null。否则返回值,如果没有为它赋过值,它为null,如果为它赋过值,如果是NULL,则还是返回null,否则返回对应的值。
1 public V get(Object key) { 2 return (isValidKey(key) ? 3 unmaskNull(vals[((Enum<?>)key).ordinal()]) : null); 4 }
1.5.1.6. 删除键对应的值
1 public V remove(Object key) { 2 if (!isValidKey(key)) 3 return null; 4 int index = ((Enum<?>)key).ordinal(); 5 Object oldValue = vals[index]; 6 vals[index] = null; 7 if (oldValue != null) 8 size--; 9 return unmaskNull(oldValue); 10 }
1.5.1.7. 总结
此Map的结构和原理在上面已经讲清,其他方法都自然而然可以推导。
清除:
1 public void clear() { 2 Arrays.fill(vals, null); 3 size = 0; 4 }
获取Map大小:
1 public int size() { 2 return size; 3 }
是否包含指定的键:
1 public boolean containsKey(Object key) { 2 return isValidKey(key) && vals[((Enum<?>)key).ordinal()] != null; 3 }
是否包含指定的值:
1 public boolean containsValue(Object value) { 2 value = maskNull(value); 3 4 for (Object val : vals) 5 if (value.equals(val)) 6 return true; 7 8 return false; 9 }
可以看到,此Map内部呈现为数组,数组是大小固定为枚举常量数的。通过元素值是否为null来区分是否为指定的枚举常量键指定了值,而在此前提下为了支持null值的存储,对null进行了封装,正如其方面名一样,“给null带上面具”。总之,EnumMap相对一般Map是紧凑而高效的。
另外,多提一句,此类被声明为可序列化的,但它使用了名为序列化代理的特性,关于该特性的具体描述见讲解序列化的章节。
1.5.2. 只存枚举的Set:java.util.EnumSet
1.5.2.1. EnumSet的使用实例
下面是EnumSet的主要使用方法(当然,它本身是个Set,因而也有Set的方法,只是这里只讲专属于它的方法)。
1 package com.cjw.learning.enumeration; 2 3 import java.util.Collection; 4 import java.util.EnumSet; 5 import java.util.stream.Collectors; 6 7 public class Demo03 { 8 private static Collection<String> listNames(EnumSet<OrderStatus> orderStatuses) { 9 return orderStatuses.stream().map(OrderStatus::name).collect(Collectors.toList()); 10 } 11 12 public static void main(String[] args) { 13 EnumSet<OrderStatus> enumSet01 = EnumSet.noneOf(OrderStatus.class); 14 System.out.println("noneOf:" + listNames(enumSet01)); 15 16 enumSet01.add(OrderStatus.UNPAID); 17 enumSet01.add(OrderStatus.PAID); 18 System.out.println("add:" + listNames(enumSet01)); 19 20 EnumSet<OrderStatus> enumSet02 = EnumSet.complementOf(enumSet01); 21 System.out.println("complementOf:" + listNames(enumSet02)); 22 23 EnumSet<OrderStatus> enumSet03 = EnumSet.allOf(OrderStatus.class); 24 System.out.println("allOf:" + listNames(enumSet03)); 25 26 EnumSet<OrderStatus> enumSet04 = EnumSet.copyOf(enumSet03); 27 System.out.println("copyOf:" + listNames(enumSet04)); 28 29 EnumSet<OrderStatus> enumSet05 = EnumSet.of(OrderStatus.UNPAID, OrderStatus.CANCELED, OrderStatus.PAID); 30 System.out.println("of:" + listNames(enumSet05)); 31 32 EnumSet<OrderStatus> enumSet06 = EnumSet.range(OrderStatus.CANCELED, OrderStatus.DOWN); 33 System.out.println("range:" + listNames(enumSet06)); 34 } 35 }
程序输出:
1.5.2.2. EnumSet的实现原理
1 public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E> 2 implements Cloneable, java.io.Serializable
EnumSet含如下字段:
1 final Class<E> elementType; 2 final Enum<?>[] universe;
其中,elementType指存放的枚举的类型,universe则为存储枚举的数组,EnumSet一创建就存了全部的枚举常量。后面看到,Set上的增减并不在这个字段上体现。
下面是创建空EnumSet的方法。
1 public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) { 2 Enum<?>[] universe = getUniverse(elementType); 3 if (universe == null) 4 throw new ClassCastException(elementType + " not an enum"); 5 6 if (universe.length <= 64) 7 return new RegularEnumSet<>(elementType, universe); 8 else 9 return new JumboEnumSet<>(elementType, universe); 10 }
下面是创建包含全部枚举常量EnumSet的方法。
1 public static <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) { 2 EnumSet<E> result = noneOf(elementType); 3 result.addAll(); 4 return result; 5 }
可以看到EnumSet的具体实现是由两个子类承担的。
如果枚举常量的个数不超过64个,就使用RegularEnumSet子类实现。
这里的elements是一个64位存储的long型,它就是实际用于Set存储的字段。如果Set中含有某个枚举常量,该枚举常量的定义序数为ordinal,那么该long型值第ordinal位低位为1。
而如果枚举常量的个数超过64个,就使用JumboEnumSet子类实现。它存储枚举常量使用的是如下字段:
1 private long elements[];
可以看到,是long的数组。它仍旧是一个比特对应一个枚举常量。
1.6. 枚举的实现原理
通过OrderStatus的字节码,可以看到枚举类型包含了该类型的几个常量,和这些常量的数组$VALUES,$VALUES编译器生成的私有静态字段。
也看到生成了公共方法values,它返回$VALUES的克隆结果。
而根据枚举常量名称生成枚举常量的valueOf方法,则借助了Enum的valueOf方法。
另外,静态初始化方法会收集枚举类型中定义的静态语句执行,该方法创建了指定的几个枚举常量,并将它们存进数组$VALUES。可以看到,前面几个常量,都是生成了OrderStatus对象后,调用其含4参的<init>,是私有的构造方法,最后一个枚举常量仍然是OrderStatus类型,但实例化用的是它的匿名子类,该匿名子类也是用含4参的<init>。
OrderStatus含4参(分别对应name、ordinal和两个自定义的字段)的<init>,实际上内部会调用枚举的默认私有的含2参的构造方法。
OrderStatus匿名子类含4参的<init>,最后调用OrderStatus含5参的<init>。
而OrderStatus含5参(另一参为匿名子类实例)的<init>由调用其含4参的<init>,实际上在OrderStatus中,匿名子类的实例没有用到,所以指定为null的。
1.7. 参考资料
JDK 1.8源码
http://blog.lichengwu.cn/java/2011/09/26/the-usage-of-enum-in-java/