Java 中使用枚举的正确姿势

#Java [字体···]

在项目开发中我们经常用到枚举定义常量,例如定义系统中用户的身份类型(ADMIN、USER…)、定义项目启动环境的类型(DEV,PROD…)、某个字段的值的枚举等。往往我们发现在项目里定义枚举类时只是有枚举成员,像下面这样:

public enum Type {
  T1,T2,T3
}

不能说这样不好,只是这样差不多又回到了我们使用类定义静态常量常量。Java 中除了有类(class)这种类型,又实现了枚举(enum)这种类型,说明肯定它独特的用处,不妨我们先回忆下使用类成员定义静态常量和枚举定义常量的区别。

  • 使用类的静态成员定义常量,常量只能有一个固定的值,通常是一个简单的字符串或一个数值。
  • 使用枚举定义常量,实例化枚举后不可变,枚举可以携带多个值,这为常量提供了更多信息。(补充,虽说枚举实例化后不可变,但它的成员变量如果不用 final 修饰,是可变的!)
  • 如果你熟悉枚举的原理,你可以通过类(class)模拟出一个枚举(enum),但通常你需要两个类来完成这个工作,一个类来定义枚举和其成员,另一类完成初始化。而枚举(enum)通过编译消除了这些复杂性,可以轻松实例化对象和定义方法。

通过它们之间的区别,可以看出枚举能为我们的常量提供更多的信息和更加便捷的定义、实例化。那么如何利用好枚举这些特性,如何定义好用(便捷)的枚举呢?

先解决选择的问题,静态常量、枚举常量二者如何选择?

我们要使用常量存储一些值,如果是这个常量只是一个简单的字符串或数值,例如 Redis key 常量、登录用户 Token 过期时间,这些常量不需要额外的操作和相互转换,选择类定义静态常量就足够了。

反之,如果我们的常量比较复杂,例如系统里的用户类型常量,它不仅是一个标志,有时候还需要比较不同用户类型的权利大小,甚至需要进行二次分类等,这时用类定义静态常量已经满足不了需求了,而枚举是更好的选择。

为枚举常量提供更多信息

为什么提供更多信息?提供信息有什么作用?

当枚举拥有了这些信息(常量的元数据)其实就能实现更多的功能,是为了加强常量。

以一个系统中的用户类型为例,用户可有超级管理员、管理员、用户,每种类型的用户都有一个标志,除了这些一般还应该有权利大小或用户的描述信息。这里描述的枚举大致如下:

public enum UserEnum {
    SUPERVISOR("supervisor", 1, "超级管理员"),
    ADMIN("admin", 2, "管理员"),
    USER("user", 3, "普通用户");

    private final String symbol;
    private final int priority;
    private final String name;
    UserEnum(String symbol, int priority, String name) {
      this.symbol = symbol;
      this.priority = priority;
      this.name = name;
    }
    // ~ Getter
    // ...
}

这种定义的常量一眼看上去就能获得很多信息,这些信息既是描述信息的一部分又是代码的一部分。

这里说明下这个枚举,symbol 成员可能是我们存在数据库中的用户的标志,也可以不用这个标志直接用枚举变量的名字,例如 ADMIN(它通过 UserEnum.ADMIN.name() 获取), 如果我们的用户是从其他系统(例如 CAS)同步过来的,两种用户的类型标志不一样,这时候也可以通过枚举成员变量抹平这种差异,例如我们用枚举的名字做数据库中用户的标志,而从 CAS 拿到的用户类型标志记做 symbol 枚举成员变量;priority 是用户表示不同类型用户的优先权,如果枚举只有 symbol 标志是无法比较用户类型的大小的;name 保存了不同用户类型的名称,可以统一系统中不同用户类型的称呼。

关于枚举成员变量命名。因为枚举是特殊的 class,其中默认定义了一下成员变量和方法,如果命名做不好会产生方法冲突,需要知道枚举默认的成员和已实现的方法,避免用与其相近成员变量名和方法名,否则容易产生混淆。

通常变量名通常要避免以 name、value、ordinal 命名,尽量也不包含这些单词。

到这里我们让枚举常量拥有了更多信息,想要发挥这些数据的作用需要给枚举实现方法,实现了方法的枚举才更加强大。

给枚举添加常用的方法

一般来说,我们的枚举需要根据根据某个标志转换为枚举(例如通过 symbol 转换为枚举);如果需要比较枚举常量的大小(不是比较枚举相等)还可以实现比较方法,例如比较超级管理员管理员谁的大。

我们还以 UserEnum 为例展示可以为枚举添加的方法:

public enum UserEnum {
    SUPERVISOR("supervisor", 1, "超级管理员"),
    ADMIN("admin", 2, "管理员"),
    USER("user", 3, "普通用户");

    private final String symbol;
    private final int priority;
    private final String name;
    // 比较器
    private static UserEnumComparator comparator;

    UserEnum(String symbol, int priority, String name) {
        this.symbol = symbol;
        this.priority = priority;
        this.name = name;
    }

    /**
     * return null if symbol isn't match.
     */
    public static UserEnum valueOfSymbol(String symbol) {
        if (symbol == null || symbol.isEmpty()) {
            return null;
        }
        return Arrays.asList(values()).stream()
                .filter(e -> e.getSymbol().equals(symbol))
                .findFirst()
                .orElse(null);
    }

    /**
     * 判断是不是管理员,这里把 SUPERVISOR、ADMIN 称为管理员。(次步操作相当于进行了二次枚举分类)
     * @return
     */
    public static boolean isSupperUser(UserEnum em) {
        if (em == null) {
            return false;
        }
        switch (em) {
            case SUPERVISOR:
            case ADMIN:
                return true;
            default:
                return false;
        }
    }

    /**
     * 我们再重载一个 isSupperUser 方法
     */
    public static boolean isSupperUser(String symbol) {
        return isSupperUser(valueOfSymbol(symbol));
    }

    /**
     * 通过优先权比较权利大小
     */
    public int compareByPriority(UserEnum e) {
        UserEnum other = e;
        // 兜底
        if (other == null) {
            other = USER;
        }
        if (comparator == null) {
            comparator = new UserEnumComparator();
        }
        return comparator.compare(this, other);
    }

    /**
     * 获取比较器
     */
    public static Comparator<UserEnum> getComparator() {
        if (comparator == null) {
            comparator = new UserEnumComparator();
        }
        return comparator;
    }

    /**
     * 比较器
     */
    public static class UserEnumComparator implements Comparator<UserEnum> {
        @Override
        public int compare(UserEnum d1, UserEnum d2) {
            UserEnum e1 = d1, e2 = d2;
            // 兜底
            if (e1 == null) {
                e1 = USER;
            }
            if (e2 == null) {
                e2 = USER;
            }
            return Integer.compare(e1.priority, e2.priority);
        }
    }

    // ~ Getter
    // ------------------------------------------------------------
    public String getSymbol() {
        return symbol;
    }

    public int getPriority() {
        return priority;
    }

    public String getName() {
        return name;
    }
}

现在,这个枚举提供了转换、分类、比较等功能,对比类静态成员的实现方式更加强大。

我们进一步抽象这个枚举,例如可以把这个枚举想象成一个数据库的关系表,枚举的成员变量是表的字段。SQL 对单表可以进行查询、排序、分组等功能强大的操作,我们的枚举也具备这种基础要素,因此几乎所有单表 SQL 操作完成的功能在枚举上都是可以实现的,这可能就是 Java 枚举存在的意义吧。


Top↑