版权声明:本文系博主原创,未经博主许可,禁止转载。保留所有权利。

引用网址: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为枚举的Mapjava.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. 只存枚举的Setjava.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方法,则借助了EnumvalueOf方法。

 

另外,静态初始化方法会收集枚举类型中定义的静态语句执行,该方法创建了指定的几个枚举常量,并将它们存进数组$VALUES。可以看到,前面几个常量,都是生成了OrderStatus对象后,调用其含4参的<init>,是私有的构造方法,最后一个枚举常量仍然是OrderStatus类型,但实例化用的是它的匿名子类,该匿名子类也是用含4参的<init>

 

OrderStatus4参(分别对应nameordinal和两个自定义的字段)的<init>,实际上内部会调用枚举的默认私有的含2参的构造方法。

 

OrderStatus匿名子类含4参的<init>,最后调用OrderStatus5参的<init>

OrderStatus5参(另一参为匿名子类实例)的<init>由调用其含4参的<init>,实际上在OrderStatus中,匿名子类的实例没有用到,所以指定为null的。

1.7. 参考资料

JDK 1.8源码

http://blog.lichengwu.cn/java/2011/09/26/the-usage-of-enum-in-java/

https://blog.csdn.net/javazejian/article/details/71333103

posted on 2018-12-01 18:41  Firefly(萤火虫)  阅读(1389)  评论(0编辑  收藏  举报