彻底搞清楚Java中的Enum

在平时的开发中,枚举几乎是一个人人都会用的工具。如果某类业务变量是某些限定死的固定值,我们往往会使用枚举来表示。 看上去,枚举既直观又简单,利用它还能避免一些异常值扰乱我们的业务;给我们的印象是枚举非常简单,至少学习它的时候,甚至没有把它当做一个专门的知识点来应对。但是时间长了就能发现:即使看上去极其简单的东西也有一些弯弯绕是我们之前没有想过的。就好比武功中的太祖长拳,萧峰用起来能打死老虎,我学了太祖长拳却连一条狗都干不过。 有必要审视一下看似极其简单的枚举,下面我会根据我在项目中的经验,由简入繁地介绍一下这个看似简单的工具。

朴素的概念理解,枚举就是一组业务相关的常量集。 背后的逻辑呢? 写个简单的枚举看看。

package com.sptan.sbe.enumexample;


 * 简单枚举类.
 *
 * @author liupeng
 * @date 2024/5/26
 */
public enum SimpleWeekDay {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY,
    ;
}

看一下它的字节码(使用ASM Bytecode Viewer展示)

public final enum com/sptan/sbe/enumexample/SimpleWeekDay extends java/lang/Enum {




  public final static enum Lcom/sptan/sbe/enumexample/SimpleWeekDay; MONDAY


  public final static enum Lcom/sptan/sbe/enumexample/SimpleWeekDay; TUESDAY


  public final static enum Lcom/sptan/sbe/enumexample/SimpleWeekDay; WEDNESDAY


  public final static enum Lcom/sptan/sbe/enumexample/SimpleWeekDay; THURSDAY


  public final static enum Lcom/sptan/sbe/enumexample/SimpleWeekDay; FRIDAY


  public final static enum Lcom/sptan/sbe/enumexample/SimpleWeekDay; SATURDAY


  public final static enum Lcom/sptan/sbe/enumexample/SimpleWeekDay; SUNDAY


  private final static synthetic [Lcom/sptan/sbe/enumexample/SimpleWeekDay; $VALUES


  public static values()[Lcom/sptan/sbe/enumexample/SimpleWeekDay;
   L0
    LINENUMBER 9 L0
    GETSTATIC com/sptan/sbe/enumexample/SimpleWeekDay.$VALUES : [Lcom/sptan/sbe/enumexample/SimpleWeekDay;
    INVOKEVIRTUAL [Lcom/sptan/sbe/enumexample/SimpleWeekDay;.clone ()Ljava/lang/Object;
    CHECKCAST [Lcom/sptan/sbe/enumexample/SimpleWeekDay;
    ARETURN
    MAXSTACK = 1
    MAXLOCALS = 0


  public static valueOf(Ljava/lang/String;)Lcom/sptan/sbe/enumexample/SimpleWeekDay;

   L0
    LINENUMBER 9 L0
    LDC Lcom/sptan/sbe/enumexample/SimpleWeekDay;.class
    ALOAD 0
    INVOKESTATIC java/lang/Enum.valueOf (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
    CHECKCAST com/sptan/sbe/enumexample/SimpleWeekDay
    ARETURN
   L1
    LOCALVARIABLE name Ljava/lang/String; L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 1




  private <init>(Ljava/lang/String;I)V


   L0
    LINENUMBER 9 L0
    ALOAD 0
    ALOAD 1
    ILOAD 2
    INVOKESPECIAL java/lang/Enum.<init> (Ljava/lang/String;I)V
    RETURN
   L1
    LOCALVARIABLE this Lcom/sptan/sbe/enumexample/SimpleWeekDay; L0 L1 0
    MAXSTACK = 3
    MAXLOCALS = 3


  private static synthetic $values()[Lcom/sptan/sbe/enumexample/SimpleWeekDay;
   L0
    LINENUMBER 9 L0
    BIPUSH 7
    ANEWARRAY com/sptan/sbe/enumexample/SimpleWeekDay
    DUP
    ICONST_0
    GETSTATIC com/sptan/sbe/enumexample/SimpleWeekDay.MONDAY : Lcom/sptan/sbe/enumexample/SimpleWeekDay;
    AASTORE
    DUP
    ICONST_1
    GETSTATIC com/sptan/sbe/enumexample/SimpleWeekDay.TUESDAY : Lcom/sptan/sbe/enumexample/SimpleWeekDay;
    AASTORE
    DUP
    ICONST_2
    GETSTATIC com/sptan/sbe/enumexample/SimpleWeekDay.WEDNESDAY : Lcom/sptan/sbe/enumexample/SimpleWeekDay;
    AASTORE
    DUP
    ICONST_3
    GETSTATIC com/sptan/sbe/enumexample/SimpleWeekDay.THURSDAY : Lcom/sptan/sbe/enumexample/SimpleWeekDay;
    AASTORE
    DUP
    ICONST_4
    GETSTATIC com/sptan/sbe/enumexample/SimpleWeekDay.FRIDAY : Lcom/sptan/sbe/enumexample/SimpleWeekDay;
    AASTORE
    DUP
    ICONST_5
    GETSTATIC com/sptan/sbe/enumexample/SimpleWeekDay.SATURDAY : Lcom/sptan/sbe/enumexample/SimpleWeekDay;
    AASTORE
    DUP
    BIPUSH 6
    GETSTATIC com/sptan/sbe/enumexample/SimpleWeekDay.SUNDAY : Lcom/sptan/sbe/enumexample/SimpleWeekDay;
    AASTORE
    ARETURN
    MAXSTACK = 4
    MAXLOCALS = 0


  static <clinit>()V
   L0
    LINENUMBER 10 L0
    NEW com/sptan/sbe/enumexample/SimpleWeekDay
    DUP
    LDC "MONDAY"
    ICONST_0
    INVOKESPECIAL com/sptan/sbe/enumexample/SimpleWeekDay.<init> (Ljava/lang/String;I)V
    PUTSTATIC com/sptan/sbe/enumexample/SimpleWeekDay.MONDAY : Lcom/sptan/sbe/enumexample/SimpleWeekDay;
   L1
    LINENUMBER 11 L1
    NEW com/sptan/sbe/enumexample/SimpleWeekDay
    DUP
    LDC "TUESDAY"
    ICONST_1
    INVOKESPECIAL com/sptan/sbe/enumexample/SimpleWeekDay.<init> (Ljava/lang/String;I)V
    PUTSTATIC com/sptan/sbe/enumexample/SimpleWeekDay.TUESDAY : Lcom/sptan/sbe/enumexample/SimpleWeekDay;
   L2
    LINENUMBER 12 L2
    NEW com/sptan/sbe/enumexample/SimpleWeekDay
    DUP
    LDC "WEDNESDAY"
    ICONST_2
    INVOKESPECIAL com/sptan/sbe/enumexample/SimpleWeekDay.<init> (Ljava/lang/String;I)V
    PUTSTATIC com/sptan/sbe/enumexample/SimpleWeekDay.WEDNESDAY : Lcom/sptan/sbe/enumexample/SimpleWeekDay;
   L3
    LINENUMBER 13 L3
    NEW com/sptan/sbe/enumexample/SimpleWeekDay
    DUP
    LDC "THURSDAY"
    ICONST_3
    INVOKESPECIAL com/sptan/sbe/enumexample/SimpleWeekDay.<init> (Ljava/lang/String;I)V
    PUTSTATIC com/sptan/sbe/enumexample/SimpleWeekDay.THURSDAY : Lcom/sptan/sbe/enumexample/SimpleWeekDay;
   L4
    LINENUMBER 14 L4
    NEW com/sptan/sbe/enumexample/SimpleWeekDay
    DUP
    LDC "FRIDAY"
    ICONST_4
    INVOKESPECIAL com/sptan/sbe/enumexample/SimpleWeekDay.<init> (Ljava/lang/String;I)V
    PUTSTATIC com/sptan/sbe/enumexample/SimpleWeekDay.FRIDAY : Lcom/sptan/sbe/enumexample/SimpleWeekDay;
   L5
    LINENUMBER 15 L5
    NEW com/sptan/sbe/enumexample/SimpleWeekDay
    DUP
    LDC "SATURDAY"
    ICONST_5
    INVOKESPECIAL com/sptan/sbe/enumexample/SimpleWeekDay.<init> (Ljava/lang/String;I)V
    PUTSTATIC com/sptan/sbe/enumexample/SimpleWeekDay.SATURDAY : Lcom/sptan/sbe/enumexample/SimpleWeekDay;
   L6
    LINENUMBER 16 L6
    NEW com/sptan/sbe/enumexample/SimpleWeekDay
    DUP
    LDC "SUNDAY"
    BIPUSH 6
    INVOKESPECIAL com/sptan/sbe/enumexample/SimpleWeekDay.<init> (Ljava/lang/String;I)V
    PUTSTATIC com/sptan/sbe/enumexample/SimpleWeekDay.SUNDAY : Lcom/sptan/sbe/enumexample/SimpleWeekDay;
   L7
    LINENUMBER 9 L7
    INVOKESTATIC com/sptan/sbe/enumexample/SimpleWeekDay.$values ()[Lcom/sptan/sbe/enumexample/SimpleWeekDay;
    PUTSTATIC com/sptan/sbe/enumexample/SimpleWeekDay.$VALUES : [Lcom/sptan/sbe/enumexample/SimpleWeekDay;
    RETURN
    MAXSTACK = 4
    MAXLOCALS = 0
}

代码很长,捡干货:

  1. SimpleWeekDay这个枚举类型是一个java类,被final修饰,所以不能再被继承;
  2. SimpleWeekDay继承自java.lang.Enum;
  3. SimpleWeekDay中的成员(比如MONDAY),是SimpleWeekDay类型的常量,之所以说是常量,因为这些成员类型都是SimpleWeekDay,并且公有的、不可修改的、静态的;从这些修饰符来看,这不就是常量嘛,所以枚举值的命名我们约定都使用常量的命名方式:大写字母加下划线这种,而不是使用驼峰式;
  4. 字节码中有values和valueOf方法,既然不是我们写的,肯定就是编译器生成的了。

既然继承自java.lang.Enum,我们看看它的源码:

 * Copyright (c) 2003, 2023, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package java.lang;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.constant.ClassDesc;
import java.lang.constant.Constable;
import java.lang.constant.ConstantDescs;
import java.lang.constant.DynamicConstantDesc;
import java.lang.invoke.MethodHandles;
import java.util.Optional;

import jdk.internal.vm.annotation.Stable;

import static java.util.Objects.requireNonNull;


 * This is the common base class of all Java language enumeration classes.
 *
 * More information about enums, including descriptions of the
 * implicitly declared methods synthesized by the compiler, can be
 * found in section {@jls 8.9} of <cite>The Java Language
 * Specification</cite>.
 *
 * Enumeration classes are all serializable and receive special handling
 * by the serialization mechanism. The serialized representation used
 * for enum constants cannot be customized. Declarations of methods
 * and fields that would otherwise interact with serialization are
 * ignored, including {@code serialVersionUID}; see the
 * <a href="{@docRoot}/../specs/serialization/index.html"><cite>Java
 * Object Serialization Specification</cite></a> for details.
 *
 * <p> Note that when using an enumeration type as the type of a set
 * or as the type of the keys in a map, specialized and efficient
 * {@linkplain java.util.EnumSet set} and {@linkplain
 * java.util.EnumMap map} implementations are available.
 *
 * @param <E> The type of the enum subclass
 *
 * @spec serialization/index.html Java Object Serialization Specification
 * @serial exclude
 * @author  Josh Bloch
 * @author  Neal Gafter
 * @see     Class#getEnumConstants()
 * @see     java.util.EnumSet
 * @see     java.util.EnumMap
 * @jls 8.9 Enum Classes
 * @jls 8.9.3 Enum Members
 * @since   1.5
 */
@SuppressWarnings("serial") 

public abstract class Enum<E extends Enum<E>>
        implements Constable, Comparable<E>, Serializable {

     * The name of this enum constant, as declared in the enum declaration.
     * Most programmers should use the {@link #toString} method rather than
     * accessing this field.
     */
    private final String name;


     * Returns the name of this enum constant, exactly as declared in its
     * enum declaration.
     *
     * <b>Most programmers should use the {@link #toString} method in
     * preference to this one, as the toString method may return
     * a more user-friendly name.</b>  This method is designed primarily for
     * use in specialized situations where correctness depends on getting the
     * exact name, which will not vary from release to release.
     *
     * @return the name of this enum constant
     */
    public final String name() {
        return name;
    }


     * 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;


     * Returns the ordinal of this enumeration constant (its position
     * in its enum declaration, where the initial constant is assigned
     * an ordinal of zero).
     *
     * Most programmers will have no use for this method.  It is
     * designed for use by sophisticated enum-based data structures, such
     * as {@link java.util.EnumSet} and {@link java.util.EnumMap}.
     *
     * @return the ordinal of this enumeration constant
     */
    public final int ordinal() {
        return ordinal;
    }


     * Sole constructor.  Programmers cannot invoke this constructor.
     * It is for use by code emitted by the compiler in response to
     * enum class declarations.
     *
     * @param name The name of this enum constant, which is the identifier
     *               used to declare it.
     * @param ordinal The ordinal of this enumeration constant (its position
     *         in the enum declaration, where the initial constant is assigned
     *         an ordinal of zero).
     */
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }


     * Returns the name of this enum constant, as contained in the
     * declaration.  This method may be overridden, though it typically
     * isn't necessary or desirable.  An enum class should override this
     * method when a more "programmer-friendly" string form exists.
     *
     * @return the name of this enum constant
     */
    public String toString() {
        return name;
    }


     * Returns true if the specified object is equal to this
     * enum constant.
     *
     * @param other the object to be compared for equality with this object.
     * @return  true if the specified object is equal to this
     *          enum constant.
     */
    public final boolean equals(Object other) {
        return this==other;
    }


     * The hash code of this enumeration constant.
     */
    @Stable
    private int hash;


     * Returns a hash code for this enum constant.
     *
     * @return a hash code for this enum constant.
     */
    public final int hashCode() {





        int hc = hash;
        if (hc == 0) {
            hc = hash = System.identityHashCode(this);
        }
        return hc;
    }


     * Throws CloneNotSupportedException.  This guarantees that enums
     * are never cloned, which is necessary to preserve their "singleton"
     * status.
     *
     * @return (never returns)
     */
    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }


     * Compares this enum with the specified object for order.  Returns a
     * negative integer, zero, or a positive integer as this object is less
     * than, equal to, or greater than the specified object.
     *
     * Enum constants are only comparable to other enum constants of the
     * same enum type.  The natural order implemented by this
     * method is the order in which the constants are declared.
     */
    public final int compareTo(E o) {
        Enum<?> other = o;
        Enum<E> self = this;
        if (self.getClass() != other.getClass() && 
            self.getDeclaringClass() != other.getDeclaringClass())
            throw new ClassCastException();
        return self.ordinal - other.ordinal;
    }


     * Returns the Class object corresponding to this enum constant's
     * enum type.  Two enum constants e1 and  e2 are of the
     * same enum type if and only if
     *   e1.getDeclaringClass() == e2.getDeclaringClass().
     * (The value returned by this method may differ from the one returned
     * by the {@link Object#getClass} method for enum constants with
     * constant-specific class bodies.)
     *
     * @return the Class object corresponding to this enum constant's
     *     enum type
     */
    @SuppressWarnings("unchecked")
    public final Class<E> getDeclaringClass() {
        Class<?> clazz = getClass();
        Class<?> zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
    }


     * Returns an enum descriptor {@code EnumDesc} for this instance, if one can be
     * constructed, or an empty {@link Optional} if one cannot be.
     *
     * @return An {@link Optional} containing the resulting nominal descriptor,
     * or an empty {@link Optional} if one cannot be constructed.
     * @since 12
     */
    @Override
    public final Optional<EnumDesc<E>> describeConstable() {
        return getDeclaringClass()
                .describeConstable()
                .map(c -> EnumDesc.of(c, name));
    }


     * Returns the enum constant of the specified enum class with the
     * specified name.  The name must match exactly an identifier used
     * to declare an enum constant in this class.  (Extraneous whitespace
     * characters are not permitted.)
     *
     * <p>Note that for a particular enum class {@code T}, the
     * implicitly declared {@code public static T valueOf(String)}
     * method on that enum may be used instead of this method to map
     * from a name to the corresponding enum constant.  All the
     * constants of an enum class can be obtained by calling the
     * implicit {@code public static T[] values()} method of that
     * class.
     *
     * @param <T> The enum class whose constant is to be returned
     * @param enumClass the {@code Class} object of the enum class from which
     *      to return a constant
     * @param name the name of the constant to return
     * @return the enum constant of the specified enum class with the
     *      specified name
     * @throws IllegalArgumentException if the specified enum class has
     *         no constant with the specified name, or the specified
     *         class object does not represent an enum class
     * @throws NullPointerException if {@code enumClass} or {@code name}
     *         is null
     * @since 1.5
     */
    public static <T extends Enum<T>> T valueOf(Class<T> enumClass,
                                                String name) {
        T result = enumClass.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumClass.getCanonicalName() + "." + name);
    }


     * enum classes cannot have finalize methods.
     *
     * @deprecated Finalization has been deprecated for removal.  See
     * {@link java.lang.Object#finalize} for background information and details
     * about migration options.
     */
    @Deprecated(since="18", forRemoval=true)
    @SuppressWarnings("removal")
    protected final void finalize() { }


     * prevent default deserialization
     */
    @java.io.Serial
    private void readObject(ObjectInputStream in) throws IOException,
        ClassNotFoundException {
        throw new InvalidObjectException("can't deserialize enum");
    }

    @java.io.Serial
    private void readObjectNoData() throws ObjectStreamException {
        throw new InvalidObjectException("can't deserialize enum");
    }


     * A <a href="{@docRoot}/java.base/java/lang/constant/package-summary.html#nominal">nominal descriptor</a> for an
     * {@code enum} constant.
     *
     * @param <E> the type of the enum constant
     *
     * @since 12
     */
    public static final class EnumDesc<E extends Enum<E>>
            extends DynamicConstantDesc<E> {


         * Constructs a nominal descriptor for the specified {@code enum} class and name.
         *
         * @param constantClass a {@link ClassDesc} describing the {@code enum} class
         * @param constantName the unqualified name of the enum constant
         * @throws NullPointerException if any argument is null
         * @jvms 4.2.2 Unqualified Names
         */
        private EnumDesc(ClassDesc constantClass, String constantName) {
            super(ConstantDescs.BSM_ENUM_CONSTANT, requireNonNull(constantName), requireNonNull(constantClass));
        }


         * Returns a nominal descriptor for the specified {@code enum} class and name
         *
         * @param <E> the type of the enum constant
         * @param enumClass a {@link ClassDesc} describing the {@code enum} class
         * @param constantName the unqualified name of the enum constant
         * @return the nominal descriptor
         * @throws NullPointerException if any argument is null
         * @jvms 4.2.2 Unqualified Names
         * @since 12
         */
        public static<E extends Enum<E>> EnumDesc<E> of(ClassDesc enumClass,
                                                        String constantName) {
            return new EnumDesc<>(enumClass, constantName);
        }

        @Override
        @SuppressWarnings("unchecked")
        public E resolveConstantDesc(MethodHandles.Lookup lookup)
                throws ReflectiveOperationException {
            return Enum.valueOf((Class<E>) constantType().resolveConstantDesc(lookup), constantName());
        }

        @Override
        public String toString() {
            return String.format("EnumDesc[%s.%s]", constantType().displayName(), constantName());
        }
    }
}

简单地看一下源码就能得出以下结论:

  1. java.lang.Enum是个抽象类,所以不能直接用来示例化;
  2. 这个类实现了几个接口:Constable, Comparable, Serializable,就能猜出枚举是不可变的(典型的不可变类是String、BigDecimal这些)、可比较的,可序列化的;
  3. 构造方法是protected,意味着只能被它的子类(我们开发者定义的枚举类)调用,实际上在我们自定义的枚举中,这个构造函数就变成private了;
  4. 它有两个成员变量,name和ordinal,不过被private 和final修饰,并且对应的访问方法是public和final的,意味着这两个方法可以被调用,不能被覆盖;如果我们试图覆盖这两个方法,不好意思,只能得到编译错误;name就是我们给枚举命的名字,比如我们例子中的SUNDAY、MONDAY这些,ordinal就是序号,默认是从0开始递增;
    @Test
    void name() {
        SimpleWeekDay swd = SimpleWeekDay.SUNDAY;
        Assertions.assertEquals("SUNDAY", swd.name());
    }

    @Test
    void ordinal() {
        SimpleWeekDay sunday = SimpleWeekDay.SUNDAY;
        Assertions.assertEquals(6, sunday.ordinal());

        SimpleWeekDay monday = SimpleWeekDay.MONDAY;
        Assertions.assertEquals(0, monday.ordinal());
    }
  1. hash值私有的,被@Stable修饰,明显也是不可变的,所以hashCode()不可被覆盖也是理所当然的了;
  2. toString(), compareTo()这两个倒是共有的,也可以被覆盖的。

从上述分析,我概括一下:枚举的本质是一个被final修饰的不可再被继承的Java类,这个类继承自java.lang.Enum。 既然枚举是java类,很多java类能做的事情,枚举也能做:实现接口,加入成员变量等。不过限定了的东西是不行的,比如想覆盖name()方法就做不到了。

枚举基础使用

就是类似我们的SimpleWeekDay这种,再举一个例子

public enum Season {
    SPRING,
    SUMMER,
    FALL,
    WINTER,
    ;
}

也可以把它作为内部类:

public class Day {
    private LocalDate day;
    private Season season;

    public String getSeason() {
        return season.name();
    }
    public void setSeason(String season) {
        this.season = Season.valueOf(season);
    }

    public LocalDate getDay() {
        return day;
    }

    public void setDay(LocalDate day) {
        this.day = day;
    }


    private enum Season {
        SPRING,
        SUMMER,
        FALL,
        WINTER,
        ;
    }
}

但是枚举的定义不能出现在方法中,普通方法或者构造函数都不行。 枚举常量肯定不能重名。。。

覆盖枚举的toString()方法

默认情况下,toString()方法返回的是枚举常量的名字,因为toString是public并且没有被final修饰,我们可以覆盖它。

public enum Season {
    SPRING,
    SUMMER,
    FALL,
    WINTER,
    ;

    @Override
    public String toString() {

        switch (this) {
            case SPRING:
                return "春天";
            case SUMMER:
                return "夏天";
            case FALL:
                return "秋天";
            case WINTER:
                return "冬天";
            default:
                return "嗯?";
        }
    }
}

测试一下:

    @Test
    void testToString() {
        Season spring = Season.SPRING;
        Assertions.assertEquals("春天", spring.toString());
        Assertions.assertEquals("SPRING", spring.name());
    }

在switch中进行分支判断

class SeasonTest {

    @Test
    void testSwitch() {
        enumSwitchExample(Season.SUMMER); 
    }

    public static void enumSwitchExample(Season s) {
        switch(s) {
            case WINTER:
                System.out.println("It's pretty cold");
                break;
            case SPRING:
                System.out.println("It's warming up");
                break;
            case SUMMER:
                System.out.println("It's pretty hot");
                break;
            case FALL:
                System.out.println("It's cooling down");
                break;
        }
    }
}

枚举的比较

从jdk的代码看,枚举的比较就是地址比较,由于枚举成员就是常量,所以一个枚举常量在我们的运行环境中就只有一份。

Season.FALL == Season.WINTER 
Season.SPRING == Season.SPRING 

Season.FALL.equals(Season.FALL); 
Season.FALL.equals(Season.WINTER); 
Season.FALL.equals("FALL"); 

枚举中可以包含可变字段

枚举常量不可变,但是可以在枚举类中增加我们自定义的可变字段。

public enum MutableExample {
    A,
    B;

    private int count = 0;

    public void increment() {
        count++;
    }

    public void print() {
        System.out.println("The count of " + name() + " is " + count);
    }
}

测试一下

class MutableExampleTest {

    @Test
    void increment() {
        MutableExample.A.print(); 
        MutableExample.A.increment();
        MutableExample.A.print(); 
        MutableExample.B.print(); 
    }
}

输出结果为:

The count of A is 0
The count of A is 1
The count of B is 0

Process finished with exit code 0

可以这么做,但是一般来说不建议这么做!别忘了我们使用枚举的初心。

使用构造函数

枚举中默认的构造函数不能使用,但是可以增加我们自己的构造函数(毕竟java类可以有多个构造函数),这种情况用于我们的枚举有自定义字段的情况。

public enum YesNoEnum {

     * Yes yes no enum.
     */
    YES(1, "是"),

     * One risk level enum.
     */
    NO(0, "否"),

    ;

    @Getter
    private final Integer code;

    @Getter
    private final String name;

    YesNoEnum(Integer code, String name) {
        this.code = code;
        this.name = name;
    }
}

这里的构造函数有点特别,只能是私有的,不能被public修饰,本质上这个构造函数不是给我们开发者调用的,毕竟通过声明枚举常量(这里是YES和NO),已经隐式调用了构造函数。

注意点

  1. 我们的两个自定义字段都是final的,这时不能使用setter方法,我们的业务中也不想动态改变枚举的name属性,这是最佳实践,一般来说,我们不需要可以改变的自定义字段。
  2. java.lang.Enum中已经有name属性,我们也有自定义的name,会不会覆盖呢?实际上不会,看例子:
    @Test
    void getName() {
        YesNoEnum yes = YesNoEnum.YES;
        Assertions.assertEquals("YES", yes.name());
        Assertions.assertEquals("是", yes.getName());
    }

看出有啥区别了吗?

枚举可以定义抽象方法

public enum AbstractWeekDay {
    MONDAY {
        @Override
        public String action() {
            return "星期一我得工作";
        }
    },
    TUESDAY {
        @Override
        public String action() {
            return "星期二我得工作";
        }
    },
    WEDNESDAY {
        @Override
        public String action() {
            return "星期三我得工作";
        }
    },
    THURSDAY {
        @Override
        public String action() {
            return "星期四我得工作";
        }
    },
    FRIDAY {
        @Override
        public String action() {
            return "星期五我得工作";
        }
    },
    SATURDAY {
        @Override
        public String action() {
            return "我要休息";
        }
    },
    SUNDAY {
        @Override
        public String action() {
            return "我要休息";
        }
    },
    ;

    public abstract String action();
}

测试一下

class AbstractWeekDayTest {

    @Test
    void action() {
        AbstractWeekDay monday = AbstractWeekDay.MONDAY;
        Assertions.assertEquals("星期一我得工作", monday.action());

        AbstractWeekDay sunday = AbstractWeekDay.SUNDAY;
        Assertions.assertEquals("我要休息", sunday.action());
    }
}

枚举可以实现接口

不废话,上代码

public enum RegEx implements Predicate<String> {
    UPPER("[A-Z]+"),
    LOWER("[a-z]+"),
    NUMERIC("[+-]?[0-9]+"),
    ;
    private final Pattern pattern;

    RegEx(final String pattern) {
        this.pattern = Pattern.compile(pattern);
    }

    @Override
    public boolean test(final String input) {
        return this.pattern.matcher(input).matches();
    }
}

测试一下:

class RegExTest {

    @Test
    void test1() {
        Assertions.assertEquals(true, RegEx.UPPER.test("ABC"));
        Assertions.assertEquals(false, RegEx.UPPER.test("ABCabc"));
        Assertions.assertEquals(true, RegEx.LOWER.test("abc"));
        Assertions.assertEquals(true, RegEx.NUMERIC.test("-10"));
    }
}

也可以各个成员分别实现

public enum Acceptor implements Predicate<String> {
    NULL {
        @Override
        public boolean test(String s) {
            return s == null;
        }
    },
    EMPTY {
        @Override
        public boolean test(String s) {
            return s.equals("");
        }
    },
    NULL_OR_EMPTY {
        @Override
        public boolean test(String s) {
            return NULL.test(s) || EMPTY.test(s);
        }
    };
}

到这里是不是感觉枚举的代码忽然有点陌生?有点抽象?如果是,建议再深入理解一下枚举的本质。 或者看看class文件反编译的结果:

public abstract enum com/sptan/sbe/enumexample/Acceptor extends java/lang/Enum implements java/util/function/Predicate {


  NESTMEMBER com/sptan/sbe/enumexample/Acceptor$3
  NESTMEMBER com/sptan/sbe/enumexample/Acceptor$2
  NESTMEMBER com/sptan/sbe/enumexample/Acceptor$1
  PERMITTEDSUBCLASS com/sptan/sbe/enumexample/Acceptor$1
  PERMITTEDSUBCLASS com/sptan/sbe/enumexample/Acceptor$2
  PERMITTEDSUBCLASS com/sptan/sbe/enumexample/Acceptor$3

  final enum INNERCLASS com/sptan/sbe/enumexample/Acceptor$1 null null

  final enum INNERCLASS com/sptan/sbe/enumexample/Acceptor$2 null null

  final enum INNERCLASS com/sptan/sbe/enumexample/Acceptor$3 null null


  public final static enum Lcom/sptan/sbe/enumexample/Acceptor; NULL


  public final static enum Lcom/sptan/sbe/enumexample/Acceptor; EMPTY


  public final static enum Lcom/sptan/sbe/enumexample/Acceptor; NULL_OR_EMPTY
......
}

可以看到确实有特殊的地方,由于实现了抽象方法,在虚拟机中每个枚举成员实际上都是内部类的形式。

遍历枚举值

可以使用Enum的values()方法,遍历枚举类的所有常量。 下面代码的fromCode和fromName都使用了values()方法。

public enum YesNoEnum {

     * Yes yes no enum.
     */
    YES(1, "是"),

     * No enum.
     */
    NO(0, "否"),

    ;

    @Getter
    private final Integer code;

    @Getter
    private final String name;

    YesNoEnum(Integer code, String name) {
        this.code = code;
        this.name = name;
    }


     * Gets by code.
     *
     * @param code the code
     * @return the by code
     */
    public static YesNoEnum fromCode(Integer code) {
        for (YesNoEnum value : YesNoEnum.values()) {
            if (value.getCode().equals(code)) {
                return value;
            }
        }
        return null;
    }


     * From name  enum.
     *
     * @param name the name
     * @return the enum
     */
    public static YesNoEnum fromName(String name) {
        for (YesNoEnum value : YesNoEnum.values()) {
            if (value.getName().equals(name)) {
                return value;
            }
        }
        return YesNoEnum.NO;
    }

}

values()方法有点奇怪,在jdk的源码里看不到,在class中能看到,这个是编译器在编译阶段为我们生成的方法,不过不影响我们使用。

利用单元素枚举实现单例模式

上文我们分析到,枚举成员(或者说枚举常量)是静态的、公有的、不可变的。想到什么?没错,就是单例模式。实际上,由于枚举的特性,每个枚举元素都是天然地实现了单例模式。

public enum Single {
    INSTANCE;

    Single() {

        System.out.println("Single!");
    }

    public void done() {
        System.out.println("done!");
    }
}

程序启动的时候,Single.INSTANCE.done()被调用的,以用来完成一些初始化操作。 测试一下:

class SingleTest {

    @Test
    void done() {
        Single.INSTANCE.done();
        Single.INSTANCE.done();
    }
}

输出结果:

Single!
done!
done!

Process finished with exit code 0

怎么样?简单不?要是我们搞一个单例模式,考虑的东西有多少,做过的同学都知道,但是枚举天然的单例属性我们可以直接拿过来用。

添加自定义方法和使用静态代码块

枚举既然是类,肯定可以添加自己的成员函数。

public enum Direction {
    NORTH, SOUTH, EAST, WEST;
    public Direction getOpposite(){
        switch (this){
            case NORTH:
                return SOUTH;
            case SOUTH:
                return NORTH;
            case WEST:
                return EAST;
            case EAST:
                return WEST;
            default: 
                return null;
        }
    }
}

因为枚举的成员都是静态的,也就是都是在编译阶段就都知道结果的,也可以这么写:

public enum Direction {
    NORTH, SOUTH, EAST, WEST;
    private Direction opposite;
    public Direction getOpposite(){
        return opposite;
    }
    static {
        NORTH.opposite = SOUTH;
        SOUTH.opposite = NORTH;
        WEST.opposite = EAST;
        EAST.opposite = WEST;
    }
}

无实例枚举

还是跟单例模式有关,enum可以用作工具类,相当于public final class{}的效果。

enum Util {

    ;

    public static final String echo(String s) {
        return s;
    }
}

枚举作为泛型的限定类型

public class Holder<T extends Enum<T>> {
    public final T value;

    public Holder(T init) {
        this.value = init;
    }
}

这种情况下,T只能是枚举类型。

枚举的多态

先看几段代码 我们的接口

public interface MyInterface {
    String name();
}

我们定义的两个枚举类

public enum DefaultEnum implements MyInterface{
    DEFAULT1,
    DEFAULT2,
    ;
}
public enum ExtendedEnum implements MyInterface{
    EXTENDED3,
    EXTENDED4,
    ;
}

测试结果

    @Test
    void name() {
        MyInterface default1 = DefaultEnum.DEFAULT1;
        Assertions.assertEquals("DEFAULT1", default1.name());
        MyInterface default2 = DefaultEnum.DEFAULT2;
        Assertions.assertEquals("DEFAULT2", default2.name());
        MyInterface extended3 = ExtendedEnum.EXTENDED3;
        Assertions.assertEquals("EXTENDED3", extended3.name());
        MyInterface extended4 = ExtendedEnum.EXTENDED4;
        Assertions.assertEquals("EXTENDED4", extended4.name());
    }

绕这么大弯,我们究竟图啥呢? 是为了API接口的扩展性,举例来说,我们想对各个大平台的oauth2认证进行封装,封装了QQ、微信、码云、GIthub等等一大堆实现,但是总有我们覆盖不到场景,覆盖不到的场景怎么办呢?需要使用我们API的开发者自己去按照我们约定规范来实现。 拿JustAuth作为一个例子,JustAuth封装了很多很多oauth的实现,但是如果是一个私有定制的oauth2认证,JustAuth是绝对不会覆盖到的,只能自己根据约定开发。 JustAuth的AuthSource封装了oauth的来源,他的代码如下:

public interface AuthSource {


     * 授权的api
     *
     * @return url
     */
    String authorize();


     * 获取accessToken的api
     *
     * @return url
     */
    String accessToken();


     * 获取用户信息的api
     *
     * @return url
     */
    String userInfo();


     * 取消授权的api
     *
     * @return url
     */
    default String revoke() {
        throw new AuthException(AuthResponseStatus.UNSUPPORTED);
    }


     * 刷新授权的api
     *
     * @return url
     */
    default String refresh() {
        throw new AuthException(AuthResponseStatus.UNSUPPORTED);
    }


     * 获取Source的字符串名字
     *
     * @return name
     */
    default String getName() {
        if (this instanceof Enum) {
            return String.valueOf(this);
        }
        return this.getClass().getSimpleName();
    }


     * 平台对应的 AuthRequest 实现类,必须继承自 {@link AuthDefaultRequest}
     *
     * @return class
     */
    Class<? extends AuthDefaultRequest> getTargetClass();
}

我们要用自定义的oauth源,就得实现自己的枚举:

public enum AuthShSource implements AuthSource {


     * The Sh  a uat.
     */
    SH("endpoint") {

         * 授权的api
         *
         * @return url
         */
        @Override
        public String authorize() {
            return getEndpoint() + "/auth";
        }


         * 获取accessToken的api
         *
         * @return url
         */
        @Override
        public String accessToken() {
            return getEndpoint() + "/token";
        }


         * 获取用户信息的api
         *
         * @return url
         */
        @Override
        public String userInfo() {
            return getEndpoint() + "/userinfo";
        }


         * 取消授权的api
         *
         * @return url
         */
        @Override
        public String revoke() {
            return super.revoke();
        }


         * 刷新授权的api
         *
         * @return url
         */
        @Override
        public String refresh() {
            return super.refresh();
        }


         * 获取Source的字符串名字
         *
         * @return name
         */
        @Override
        public String getName() {
            return super.getName();
        }


         * 平台对应的 AuthRequest 实现类,必须继承自 {@link AuthDefaultRequest}
         *
         * @return class
         */
        @Override
        public Class<? extends AuthDefaultRequest> getTargetClass() {
            return AuthShRequest.class;
        }

        @Override
        public String getEndpoint() {
            return EnvEndpoint.endpoint;
        }
    };

    @Getter
    private String endpoint;

    AuthShSource(String endpoint) {
        this.endpoint = endpoint;
    }


     * The type Env endpoint.
     */
    @Component
    static class EnvEndpoint {
        private static String endpoint;


         * Init.
         *
         * @param endpoint the endpoint
         */
        @Value("${sh.oauth.endpoint}")
        public void init(String endpoint) {
            EnvEndpoint.endpoint = endpoint;
        }
    }
}

上述代码还有一个知识点,不知道注意到没有? 我实现的oauth认证,是区分环境的,测试环境和生产环境实现逻辑一样,但是端点(认证的URL)不一样,端点在配置文件中,由于枚举常量是静态的,所以没法直接让枚举的字段读取配置文件中的配置项,但是自定义字段(上例中是endpoint)的读取方法又是可以覆盖的,我通过添加的EnvEndpoint这个类倒手了一下,实现了枚举的自定义字段是配置文件中的值。

我写了这么多,你看了这么久,下面result应该是几呢?

    @Test
    void testOrdinal() {
        Season spring = Season.SPRING;
        Season summer = Season.SUMMER;
        int result = spring.compareTo(summer);
        System.out.println(result); 
    }
请登录后发表评论

    没有回复内容