Java中的Enum详解与使用

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

Enum的本质

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

/**
 * 简单枚举类
 */
public enum SimpleWeekDay {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY,
    ;
}

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

通过解读源码就能得出以下结论:

  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());
    }

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

Enum的常用使用模式

枚举基础使用

就是类似我们的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 或者public都可以
    private enum Season {
        SPRING,
        SUMMER,
        FALL,
        WINTER,
        ;
    }
}

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

覆盖枚举的toString()方法

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

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 "嗯?";
        }
    }
}

测试一下:

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

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

枚举中可以包含可变字段

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

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(); // Outputs 0
        MutableExample.A.increment();
        MutableExample.A.print(); // Outputs 1 -- we've changed a field
        MutableExample.B.print(); // Outputs 0 -- another instance remains unchanged
    }
}

输出结果为:

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文件反编译的结果:

// 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()方法。

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的高级使用

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

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

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: //This will never happen
                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); // result == ?
    }

  • 9
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值