Java 中使用枚举的正确姿势

#Java [字体 ··]

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

1public enum Type {
2  T1,T2,T3
3}

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

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

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

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

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

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

为枚举常量提供更多信息

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

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

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

 1public enum UserEnum {
 2    SUPERVISOR("supervisor", 1, "超级管理员"),
 3    ADMIN("admin", 2, "管理员"),
 4    USER("user", 3, "普通用户");
 5
 6    private final String symbol;
 7    private final int priority;
 8    private final String name;
 9    UserEnum(String symbol, int priority, String name) {
10      this.symbol = symbol;
11      this.priority = priority;
12      this.name = name;
13    }
14    // ~ Getter
15    // ...
16}

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

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

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

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

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

给枚举添加常用的方法

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

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

  1public enum UserEnum {
  2    SUPERVISOR("supervisor", 1, "超级管理员"),
  3    ADMIN("admin", 2, "管理员"),
  4    USER("user", 3, "普通用户");
  5
  6    private final String symbol;
  7    private final int priority;
  8    private final String name;
  9    // 比较器
 10    private static UserEnumComparator comparator;
 11
 12    UserEnum(String symbol, int priority, String name) {
 13        this.symbol = symbol;
 14        this.priority = priority;
 15        this.name = name;
 16    }
 17
 18    /**
 19     * return null if symbol isn't match.
 20     */
 21    public static UserEnum valueOfSymbol(String symbol) {
 22        if (symbol == null || symbol.isEmpty()) {
 23            return null;
 24        }
 25        return Arrays.asList(values()).stream()
 26                .filter(e -> e.getSymbol().equals(symbol))
 27                .findFirst()
 28                .orElse(null);
 29    }
 30
 31    /**
 32     * 判断是不是管理员,这里把 SUPERVISOR、ADMIN 称为管理员。(次步操作相当于进行了二次枚举分类)
 33     * @return
 34     */
 35    public static boolean isSupperUser(UserEnum em) {
 36        if (em == null) {
 37            return false;
 38        }
 39        switch (em) {
 40            case SUPERVISOR:
 41            case ADMIN:
 42                return true;
 43            default:
 44                return false;
 45        }
 46    }
 47
 48    /**
 49     * 我们再重载一个 isSupperUser 方法
 50     */
 51    public static boolean isSupperUser(String symbol) {
 52        return isSupperUser(valueOfSymbol(symbol));
 53    }
 54
 55    /**
 56     * 通过优先权比较权利大小
 57     */
 58    public int compareByPriority(UserEnum e) {
 59        UserEnum other = e;
 60        // 兜底
 61        if (other == null) {
 62            other = USER;
 63        }
 64        if (comparator == null) {
 65            comparator = new UserEnumComparator();
 66        }
 67        return comparator.compare(this, other);
 68    }
 69
 70    /**
 71     * 获取比较器
 72     */
 73    public static Comparator<UserEnum> getComparator() {
 74        if (comparator == null) {
 75            comparator = new UserEnumComparator();
 76        }
 77        return comparator;
 78    }
 79
 80    /**
 81     * 比较器
 82     */
 83    public static class UserEnumComparator implements Comparator<UserEnum> {
 84        @Override
 85        public int compare(UserEnum d1, UserEnum d2) {
 86            UserEnum e1 = d1, e2 = d2;
 87            // 兜底
 88            if (e1 == null) {
 89                e1 = USER;
 90            }
 91            if (e2 == null) {
 92                e2 = USER;
 93            }
 94            return Integer.compare(e1.priority, e2.priority);
 95        }
 96    }
 97
 98    // ~ Getter
 99    // ------------------------------------------------------------
100    public String getSymbol() {
101        return symbol;
102    }
103
104    public int getPriority() {
105        return priority;
106    }
107
108    public String getName() {
109        return name;
110    }
111}

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

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


博客没有评论系统,可以通过 邮件 评论和交流。 Top↑