在平时的开发中,枚举几乎是一个人人都会用的工具。如果某类业务变量是某些限定死的固定值,我们往往会使用枚举来表示。 看上去,枚举既直观又简单,利用它还能避免一些异常值扰乱我们的业务;给我们的印象是枚举非常简单,至少学习它的时候,甚至没有把它当做一个专门的知识点来应对。但是时间长了就能发现:即使看上去极其简单的东西也有一些弯弯绕是我们之前没有想过的。就好比武功中的太祖长拳,萧峰用起来能打死老虎,我学了太祖长拳却连一条狗都干不过。 有必要审视一下看似极其简单的枚举,下面我会根据我在项目中的经验,由简入繁地介绍一下这个看似简单的工具。
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
}
代码很长,捡干货:
- SimpleWeekDay这个枚举类型是一个java类,被final修饰,所以不能再被继承;
- SimpleWeekDay继承自java.lang.Enum;
- SimpleWeekDay中的成员(比如MONDAY),是SimpleWeekDay类型的常量,之所以说是常量,因为这些成员类型都是SimpleWeekDay,并且公有的、不可修改的、静态的;从这些修饰符来看,这不就是常量嘛,所以枚举值的命名我们约定都使用常量的命名方式:大写字母加下划线这种,而不是使用驼峰式;
- 字节码中有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());
}
}
}
简单地看一下源码就能得出以下结论:
- java.lang.Enum是个抽象类,所以不能直接用来示例化;
- 这个类实现了几个接口:Constable, Comparable, Serializable,就能猜出枚举是不可变的(典型的不可变类是String、BigDecimal这些)、可比较的,可序列化的;
- 构造方法是protected,意味着只能被它的子类(我们开发者定义的枚举类)调用,实际上在我们自定义的枚举中,这个构造函数就变成private了;
- 它有两个成员变量,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());
}
- hash值私有的,被@Stable修饰,明显也是不可变的,所以hashCode()不可被覆盖也是理所当然的了;
- 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),已经隐式调用了构造函数。
注意点
- 我们的两个自定义字段都是final的,这时不能使用setter方法,我们的业务中也不想动态改变枚举的name属性,这是最佳实践,一般来说,我们不需要可以改变的自定义字段。
- 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 == ?
}