zoukankan      html  css  js  c++  java
  • Java的枚举

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

    引用网址: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

  • 相关阅读:
    今天实现了 沿路径移动
    enum类型的本质(转)
    (转)成为优秀技术人员的两点建议
    深入理解 C# 协变和逆变
    web通信
    ajax入门(复习)
    git版本管理工具的使用
    在asp.net 中使用httpmodules对网页进行处理
    asp.net http概念原理复习
    web page复习笔记
  • 原文地址:https://www.cnblogs.com/zhizaixingzou/p/10050579.html
Copyright © 2011-2022 走看看