彻底搞清楚Java中的Enum

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

Enum的本质

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

java 复制代码
package com.sptan.sbe.enumexample;

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

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

java 复制代码
// class version 65.0 (65)
// access flags 0x4031
// signature Ljava/lang/Enum<Lcom/sptan/sbe/enumexample/SimpleWeekDay;>;
// declaration: com/sptan/sbe/enumexample/SimpleWeekDay extends java.lang.Enum<com.sptan.sbe.enumexample.SimpleWeekDay>
public final enum com/sptan/sbe/enumexample/SimpleWeekDay extends java/lang/Enum {

  // compiled from: SimpleWeekDay.java

  // access flags 0x4019
  public final static enum Lcom/sptan/sbe/enumexample/SimpleWeekDay; MONDAY

  // access flags 0x4019
  public final static enum Lcom/sptan/sbe/enumexample/SimpleWeekDay; TUESDAY

  // access flags 0x4019
  public final static enum Lcom/sptan/sbe/enumexample/SimpleWeekDay; WEDNESDAY

  // access flags 0x4019
  public final static enum Lcom/sptan/sbe/enumexample/SimpleWeekDay; THURSDAY

  // access flags 0x4019
  public final static enum Lcom/sptan/sbe/enumexample/SimpleWeekDay; FRIDAY

  // access flags 0x4019
  public final static enum Lcom/sptan/sbe/enumexample/SimpleWeekDay; SATURDAY

  // access flags 0x4019
  public final static enum Lcom/sptan/sbe/enumexample/SimpleWeekDay; SUNDAY

  // access flags 0x101A
  private final static synthetic [Lcom/sptan/sbe/enumexample/SimpleWeekDay; $VALUES

  // access flags 0x9
  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

  // access flags 0x9
  public static valueOf(Ljava/lang/String;)Lcom/sptan/sbe/enumexample/SimpleWeekDay;
    // parameter mandated  name
   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

  // access flags 0x2
  // signature ()V
  // declaration: void <init>()
  private <init>(Ljava/lang/String;I)V
    // parameter synthetic  $enum$name
    // parameter synthetic  $enum$ordinal
   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

  // access flags 0x100A
  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

  // access flags 0x8
  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,我们看看它的源码:

java 复制代码
/*
 * 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") // No serialVersionUID needed due to
                            // special-casing of enum classes.
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() {
        // Once initialized, the hash field value does not change.
        // HotSpot's identity hash code generation also never returns zero
        // as the identity hash code. This makes zero a convenient marker
        // for the un-initialized value for both @Stable and the lazy
        // initialization code below.
        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() && // optimization
            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开始递增;
java 复制代码
    @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()方法就做不到了。

Enum的常用使用模式

枚举基础使用

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

java 复制代码
public enum Season {
    SPRING,
    SUMMER,
    FALL,
    WINTER,
    ;
}

也可以把它作为内部类:

java 复制代码
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 或者public都可以
    private enum Season {
        SPRING,
        SUMMER,
        FALL,
        WINTER,
        ;
    }
}

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

覆盖枚举的toString()方法

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

java 复制代码
public enum Season {
    SPRING,
    SUMMER,
    FALL,
    WINTER,
    ;

    @Override
    public String toString() {
        // 注意: 我用的java21,不需要写break,使用低版本java时需要注意
        switch (this) {
            case SPRING:
                return "春天";
            case SUMMER:
                return "夏天";
            case FALL:
                return "秋天";
            case WINTER:
                return "冬天";
            default:
                return "嗯?";
        }
    }
}

测试一下:

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

在switch中进行分支判断

java 复制代码
class SeasonTest {

    @Test
    void testSwitch() {
        enumSwitchExample(Season.SUMMER); // 输出:  It's pretty hot
    }

    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的代码看,枚举的比较就是地址比较,由于枚举成员就是常量,所以一个枚举常量在我们的运行环境中就只有一份。

java 复制代码
Season.FALL == Season.WINTER // false
Season.SPRING == Season.SPRING // true

Season.FALL.equals(Season.FALL); // true
Season.FALL.equals(Season.WINTER); // false
Season.FALL.equals("FALL"); // false and no compiler error

枚举中可以包含可变字段

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

java 复制代码
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);
    }
}

测试一下

java 复制代码
class MutableExampleTest {

    @Test
    void increment() {
        MutableExample.A.print(); // Outputs 0
        MutableExample.A.increment();
        MutableExample.A.print(); // Outputs 1 -- we've changed a field
        MutableExample.B.print(); // Outputs 0 -- another instance remains unchanged
    }
}

输出结果为:

java 复制代码
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类可以有多个构造函数),这种情况用于我们的枚举有自定义字段的情况。

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,会不会覆盖呢?实际上不会,看例子:
java 复制代码
    @Test
    void getName() {
        YesNoEnum yes = YesNoEnum.YES;
        Assertions.assertEquals("YES", yes.name());
        Assertions.assertEquals("是", yes.getName());
    }

看出有啥区别了吗?

枚举可以定义抽象方法

java 复制代码
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();
}

测试一下

java 复制代码
class AbstractWeekDayTest {

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

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

枚举可以实现接口

不废话,上代码

java 复制代码
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();
    }
}

测试一下:

java 复制代码
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"));
    }
}

也可以各个成员分别实现

java 复制代码
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文件反编译的结果:

java 复制代码
// class version 65.0 (65)
// access flags 0x4421
// signature Ljava/lang/Enum<Lcom/sptan/sbe/enumexample/Acceptor;>;Ljava/util/function/Predicate<Ljava/lang/String;>;
// declaration: com/sptan/sbe/enumexample/Acceptor extends java.lang.Enum<com.sptan.sbe.enumexample.Acceptor> implements java.util.function.Predicate<java.lang.String>
public abstract enum com/sptan/sbe/enumexample/Acceptor extends java/lang/Enum implements java/util/function/Predicate {

  // compiled from: Acceptor.java
  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
  // access flags 0x4010
  final enum INNERCLASS com/sptan/sbe/enumexample/Acceptor$1 null null
  // access flags 0x4010
  final enum INNERCLASS com/sptan/sbe/enumexample/Acceptor$2 null null
  // access flags 0x4010
  final enum INNERCLASS com/sptan/sbe/enumexample/Acceptor$3 null null

  // access flags 0x4019
  public final static enum Lcom/sptan/sbe/enumexample/Acceptor; NULL

  // access flags 0x4019
  public final static enum Lcom/sptan/sbe/enumexample/Acceptor; EMPTY

  // access flags 0x4019
  public final static enum Lcom/sptan/sbe/enumexample/Acceptor; NULL_OR_EMPTY
......
}

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

遍历枚举值

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

java 复制代码
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中能看到,这个是编译器在编译阶段为我们生成的方法,不过不影响我们使用。

Enum的高级使用

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

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

java 复制代码
public enum Single {
    INSTANCE;

    Single() {
        // 做一些系统初始化操作
        System.out.println("Single!");
    }

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

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

java 复制代码
class SingleTest {

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

输出结果:

java 复制代码
Single!
done!
done!

Process finished with exit code 0

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

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

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

java 复制代码
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: //This will never happen
                return null;
        }
    }
}

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

java 复制代码
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{}的效果。

java 复制代码
enum Util {
    /*记得要有个分号,用于表示这里是放置枚举实例的地方*/
    ;

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

枚举作为泛型的限定类型

java 复制代码
public class Holder<T extends Enum<T>> {
    public final T value;
    
    public Holder(T init) {
        this.value = init;
    }
}

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

枚举的多态

先看几段代码 我们的接口

java 复制代码
public interface MyInterface {
    String name();
}

我们定义的两个枚举类

java 复制代码
public enum DefaultEnum implements MyInterface{
    DEFAULT1,
    DEFAULT2,
    ;
}
java 复制代码
public enum ExtendedEnum implements MyInterface{
    EXTENDED3,
    EXTENDED4,
    ;
}

测试结果

java 复制代码
    @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的来源,他的代码如下:

java 复制代码
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源,就得实现自己的枚举:

java 复制代码
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应该是几呢?

java 复制代码
    @Test
    void testOrdinal() {
        Season spring = Season.SPRING;
        Season summer = Season.SUMMER;
        int result = spring.compareTo(summer);
        System.out.println(result); // result == ?
    }
相关推荐
新手小袁_J18 分钟前
JDK11下载安装和配置超详细过程
java·spring cloud·jdk·maven·mybatis·jdk11
呆呆小雅19 分钟前
C#关键字volatile
java·redis·c#
Monly2119 分钟前
Java(若依):修改Tomcat的版本
java·开发语言·tomcat
Ttang2322 分钟前
Tomcat原理(6)——tomcat完整实现
java·tomcat
goTsHgo23 分钟前
在 Spring Boot 的 MVC 框架中 路径匹配的实现 详解
spring boot·后端·mvc
钱多多_qdd32 分钟前
spring cache源码解析(四)——从@EnableCaching开始来阅读源码
java·spring boot·spring
waicsdn_haha34 分钟前
Java/JDK下载、安装及环境配置超详细教程【Windows10、macOS和Linux图文详解】
java·运维·服务器·开发语言·windows·后端·jdk
Q_19284999061 小时前
基于Spring Boot的摄影器材租赁回收系统
java·spring boot·后端
Code_流苏1 小时前
VSCode搭建Java开发环境 2024保姆级安装教程(Java环境搭建+VSCode安装+运行测试+背景图设置)
java·ide·vscode·搭建·java开发环境
良许Linux1 小时前
0.96寸OLED显示屏详解
linux·服务器·后端·互联网