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