谈谈你可能并不了解的java枚举

前言

枚举在java里也算个老生长谈的内容了,每当遇到一组需要类举的数据时我们都会自然而然地使用枚举类型:

public enum Color {
  RED, GREEN, BLUE, YELLOW;

  public static void main(String[] args) {
    Color red = Color.RED;
    Color redAnother = Color.RED;
    Color blue = Color.BLUE;

    System.out.println(red.equals(redAnother)); // true
    System.out.println(red.equals(blue)); // false
  }
}

当然今天我们要探讨的并非是java中enum的基础语法,本次的主题将会深入enum的本质,并探讨部分高阶用法。本文基于Oracle JDK 14.0.2和jad v1.5.8e(由于jad已经很久未进行更新,对于新版本的jdk支持不是很完善,但单纯分析enum和interface已经足够)。

自定义枚举值背后的秘密

枚举默认的值是从0开始递增的数值,通常来说这完全够用了。不过java中还允许我们对枚举的值做个性化定制,例如:

// 我们不仅想用英语的方位,同时还想取得对应的本地化名称(这里是中文)
enum Direction {
  EAST("东"),
  WEST("西"),
  NORTH("北"),
  SOUTH("南");

  private final String name;

  // 注意是private
  private Direction(String name) {
    this.name = name;
  }

  public String getName() {
    return this.name;
  }
}

public class Test {
  public static void main(String[] args) {
    for (var v : Direction.values()) {
      System.out.println(v.toString() + "-->" + v.getName());
    }
  }
}

编译并运行程序,你将会得到下面这样的结果:

EAST-->东
WEST-->西
NORTH-->北
SOUTH-->南

很多教程到此就结束了,点到为止,对于枚举值后面的圆括号有什么作用,为什么构造函数需要private修饰都一笔带过甚至连解释说明都没给出。然而理解这些却是我们进一步学习枚举的高阶用法的前提。

不过没关系,我们可以自己动手一探究竟,比如看看反编译后的代码,从编译器处理枚举类型的方法中一探究竟。这里我们将会利用jad,具体的使用教程参考园内其他优秀文章,本文不进行赘述,我们直接看反编译后的结果:

final class Direction extends Enum
{

  /* 省略部分无关紧要的方法 */

  private Direction(String s, int i, String s1)
  {
    super(s, i);
    name = s1;
  }

  public String getName() // 这是我们自定义的getter
  {
    return name;
  }

  public static final Direction EAST;
  public static final Direction WEST;
  public static final Direction NORTH;
  public static final Direction SOUTH;
  private final String name;
  // 省略不重要的部分字段

  static
  {
    EAST = new Direction("EAST", 0, "\u4E1C");
    WEST = new Direction("WEST", 1, "\u897F");
    NORTH = new Direction("NORTH", 2, "\u5317");
    SOUTH = new Direction("SOUTH", 3, "\u5357");
    // 省略部分字段的初始化
  }
}

首先看到我们的enum是一个类,其次它继承自java.lang.Enum(这意味着enum是无法显式指定基类的),而我们在Direction的构造函数中调用了其父类的构造函数,通过阅读文档可知,java.lang.Enum的构造函数是protected修饰的,也就是说对于java.lang包以外的使用者无法调用这个构造函数。同时文档也指出,该构造函数是由编译器自动调用的。因此我们自己定义的enum的构造函数也是无法正常调用的,只能由编译器用来初始化enum的枚举成员。既然本身无法被用户调用那么java干脆直接不允许protected和public(default和private允许)修饰自定义enum类型的构造函数以免造成误用。

另外我们的自定义构造函数其实是被编译器进行了合成,除了自定义参数之外还有枚举成员的字符串名称以及一个从0开始的序号(可用ordinal方法获取),前两个参数编译器会自动为我们添加,而自定义参数则是根据在我们给定的枚举成员后的圆括号里的值传递给构造函数,简单说就是:

EAST("东"),
WEST("西"),
NORTH("北"),
SOUTH("南");

// 转换为(unicode字符被转码)
EAST = new Direction("EAST", 0, "\u4E1C");
WEST = new Direction("WEST", 1, "\u897F");
NORTH = new Direction("NORTH", 2, "\u5317");
SOUTH = new Direction("SOUTH", 3, "\u5357");

如果我需要更多字段,只需要像这样:

public enum Planet {
  // 带有两个自定义数值
  MERCURY (3.303e+23, 2.4397e6),
  VENUS  (4.869e+24, 6.0518e6),
  EARTH  (5.976e+24, 6.37814e6),
  MARS  (6.421e+23, 3.3972e6),
  JUPITER (1.9e+27,  7.1492e7),
  SATURN (5.688e+26, 6.0268e7),
  URANUS (8.686e+25, 2.5559e7),
  NEPTUNE (1.024e+26, 2.4746e7);

  // 保存自定义值的字段,不使用final也可以,但枚举值一般不应该发生改变
  private final double mass;  // in kilograms
  private final double radius; // in meters
  // 在这里使用default的权限控制,即package-private
  Planet(double mass, double radius) {
    this.mass = mass;
    this.radius = radius;
  }
  public double mass() { return mass; }
  public double radius() { return radius; }
}

这就是自定义枚举值背后的秘密。

至此我们的疑问几乎都得到了解答,然而细心观察就会发现,我们的枚举成员都是Direction的_静态字段_!因此我们不能把这些枚举成员当作类型来使用:

public void work(Direction.EAST e) {
  // 这是无法通过编译的
}

静态字段很好理解,因为我们需要通过类名+枚举成员名Direction.WEST直接引用,但为什么字段类型要是Direction的呢?

别着急,下一节答案就将揭晓。

为枚举添加抽象方法

这一节看起来很荒谬,抽象方法似乎和枚举八杆子打不到一块儿去。可是仔细想一想,在上一节中我们已经为枚举添加了getter成员方法,这说明我们还可以为枚举添加其他的方法从而定制枚举类型的行为,以上一节的Planet为例,我们可以添加计算任意物体在某个行星表面所受重力和质量的大小:

public enum Planet {

  /* 定义枚举成员和初始化的相关重复代码,此处不再重复 */

  private double mass() { return mass; }
  private double radius() { return radius; }

  // universal gravitational constant (m3 kg-1 s-2)
  public static final double G = 6.67300E-11;

  double surfaceGravity() {
    return G * mass / (radius * radius);
  }
  double surfaceWeight(double otherMass) {
    return otherMass * surfaceGravity();
  }
  public static void main(String[] args) {
    if (args.length != 1) {
      System.err.println("Usage: java Planet <earth_weight>");
      System.exit(-1);
    }
    double earthWeight = Double.parseDouble(args[0]);
    double mass = earthWeight/EARTH.surfaceGravity();
    for (Planet p : Planet.values())
      System.out.printf("Your weight on %s is %f%n",
               p, p.surfaceWeight(mass));
  }
}

运行结果如下:

$ java Planet.java 70

Your weight on MERCURY is 26.443033
Your weight on VENUS is 63.349937
Your weight on EARTH is 70.000000
Your weight on MARS is 26.511603
Your weight on JUPITER is 177.139027
Your weight on SATURN is 74.621088
Your weight on URANUS is 63.358904
Your weight on NEPTUNE is 79.682965

既然能定制整个enum的行为,那是否意味着我们可以单独定义枚举成员的行为呢,毕竟方法最终还是从枚举成员值身上进行调用的。

答案是肯定的,还记得在上一节最后部分编译器是怎么处理枚举成员的吗?

EAST = new Direction("EAST", 0, "\u4E1C");
WEST = new Direction("WEST", 1, "\u897F");
NORTH = new Direction("NORTH", 2, "\u5317");
SOUTH = new Direction("SOUTH", 3, "\u5357");

没错,枚举成员本身也是enum对象的一个实例!而且这些枚举成员虽然是Direction类型的,但实际上还可以引用Direction的派生类型。

假设我们有一个Color类型的枚举,对于每个枚举成员我们都一个定制的print方法用于打印不同的信息:

enum Color {
  RED{
    // 先不用管这是什么语法,后面会解释
    @Override
    public void print() {
      // Linux上输出彩色字符串
      System.out.println("\u001B[1;31m This is red text \u001B[0m");
    }
  },
  BLUE{
    @Override
    public void print() {
      System.out.println("\u001B[1;34m This is blue text \u001B[0m");
    }
  },
  GREEN{
    @Override
    public void print() {
      System.out.println("\u001B[1;32m This is green text \u001B[0m");
    }
  };

  // 枚举成员必须要覆写的抽象方法
  public abstract void print();
}

public class Test {
  public static void main(String[] args) {
    for (var v : Color.values()) {
      v.print();
    }
  }
}

运行结果如下:

要想知道原理,我们还是得借助jad,这是Color.class经过处理后的内容:

// 变成了抽象类
abstract class Color extends Enum
{
  // 构造函数
  private Color(String s, int i)
  {
    super(s, i);
  }

  public abstract void print();

  public static final Color RED;
  public static final Color BLUE;
  public static final Color GREEN;

  static
  {
    // 重点从这开始
    RED = new Color("RED", 0) {

      public void print()
      {
        System.out.println("\033[1;31m This is red text \033[0m");
      }

    };
    BLUE = new Color("BLUE", 1) {

      public void print()
      {
        System.out.println("\033[1;34m This is blue text \033[0m");
      }

    };
    GREEN = new Color("GREEN", 2) {

      public void print()
      {
        System.out.println("\033[1;32m This is green text \033[0m");
      }

    };
  }
}

细心的读者大概已经发现了,这不就是_匿名内部类_么?说对了,我们的enum类型这次实际上变成了抽象类,而枚举成员则是继承自Color的匿名内部类并实现了抽象方法。所以最开始我们用注释标记的大括号其实可以理解成匿名类的类体。不过需要注意的是,虽然这里显式使用了new来创建了匿名内部类,但构造函数仍然是编译器代为调用的。

如果想增加自定义的枚举数据呢?可以这样做:

enum Color {
  RED(31){
    @Override
    public void print() {
      System.out.println("\u001B[1;31m This is red text \u001B[0m");
    }
  },
  BLUE(34){
    @Override
    public void print() {
      System.out.println("\u001B[1;34m This is blue text \u001B[0m");
    }
  },
  GREEN(32){
    @Override
    public void print() {
      System.out.println("\u001B[1;32m This is green text \u001B[0m");
    }
  };

  // color code
  private final int colorCode;
  private Color(int code) {
    colorCode = code;
  }
  public int getColorCode() {
    return colorCode;
  }

  public abstract void print();
}

我们看看编译后的代码,限于篇幅,我只保留了重要的部分:

abstract class Color extends Enum
{

  /* 大量省略代码 */

  private Color(String s, int i, int j)
  {
    super(s, i);
    colorCode = j;
  }

  public abstract void print();

  public static final Color RED;
  public static final Color BLUE;
  public static final Color GREEN;
  private final int colorCode;

  static
  {
    // 参数传递给了构造函数
    RED = new Color("RED", 0, 31) {

      public void print()
      {
        System.out.println("\033[1;31m This is red text \033[0m");
      }

    };
    BLUE = new Color("BLUE", 1, 34) {

      public void print()
      {
        System.out.println("\033[1;34m This is blue text \033[0m");
      }

    };
    GREEN = new Color("GREEN", 2, 32) {

      public void print()
      {
        System.out.println("\033[1;32m This is green text \033[0m");
      }

    };
  }
}

总结一下,对于一个enum类型来说,通常会有如下格式:

[public] enum NAME [implements XXX, ...] {
  VALUE1 [(自定义数据,格式和自定义构造函数函数的参数列表相同)]
  [{
    // 可以override或是追加新的method
  }],
  ...,
  VALUEN [(...)]
  [{
    // overrides or methods
  }];

  [存储各种自定义数据的字段,最好用final修饰]
  [
    // 自定义构造函数
    [private] NAME(和枚举成员中给出的圆括号内的内容一致) { /* 设置数据字段 */ }
  ]

  [定义抽象方法或者重写object/Enum的方法或是添加普通类方法]
}

给出的格式中用[]框住的部分都是可以省略的。

枚举和接口

在上一节的最后,我们看到enum其实还可以实现interface(毕竟本质上还是个class),所以上一节的例子可以这么写:

interface Printer {
  void print();
}

enum Color implements Printer {
  RED{
    @Override
    public void print() {
      System.out.println("\u001B[1;31m This is red text \u001B[0m");
    }
  },
  BLUE{
    @Override
    public void print() {
      System.out.println("\u001B[1;34m This is blue text \u001B[0m");
    }
  },
  GREEN{
    @Override
    public void print() {
      System.out.println("\u001B[1;32m This is green text \u001B[0m");
    }
  };
}

我个人更倾向于第二种方法,因为enum主要是数据的集合,而对于数据表现出的行为/模式尽量使用interface进行描述。

除此之外,enum还可以定义在iinterface中。假设我们有一个枚举表示从周一到周日,同时给定一个方法isRestDay判断当前日期是否可以休息(比如有的人双休有的人单休还有的人在周一或周五休息),不同类型的人对于周几该休息将会产生不同的答案,因此将它抽象成接口再合适不过了:

interface Relaxable {
  enum Weekly {
    Mon, Tue, Wed, Thu, Fri, Sat, Sun
  }

  boolean isRestDay(Relaxable.Weekly day);
}

class PersonA implements Relaxable {
  @Override
  public boolean isRestDay(Relaxable.Weekly day) {
    return day.equals(Relaxable.Weekly.Sat) || day.equals(Relaxable.Weekly.Sun);
  }
}

class PersonB implements Relaxable {
  @Override
  public boolean isRestDay(Relaxable.Weekly day) {
    return day.equals(Relaxable.Weekly.Sun);
  }
}

public class Relax {
  public static void main(String[] args) {
    var a = new PersonA();
    var b = new PersonB();
    var day = Relaxable.Weekly.Sat;
    System.out.println(a.isRestDay(day)); // true
    System.out.println(b.isRestDay(day)); // false
  }
}

PersonA拥有一个美好的双休,而可怜的PersonB却要在周六加班!使用jad查看生产的代码:

interface Relaxable
{
  public static final class Weekly extends Enum
  {

    /* 省略了部分代码 */

    public static final Weekly Mon;
    public static final Weekly Tue;
    public static final Weekly Wed;
    public static final Weekly Thu;
    public static final Weekly Fri;
    public static final Weekly Sat;
    public static final Weekly Sun;

    static
    {
      Mon = new Weekly("Mon", 0);
      Tue = new Weekly("Tue", 1);
      Wed = new Weekly("Wed", 2);
      Thu = new Weekly("Thu", 3);
      Fri = new Weekly("Fri", 4);
      Sat = new Weekly("Sat", 5);
      Sun = new Weekly("Sun", 6);
    }

    private Weekly(String s, int i)
    {
      super(s, i);
    }
  }

  public abstract boolean isRestDay(Weekly weekly);
}

可以看出此时的enum仅仅只是interface中的一个静态内部类而已。使用类似的方法可以借由interface来组织多个不同但有弱关联性的枚举类型,从而构成类似其他语言中namespace的组织结构。

当然,通常我们并不推荐用接口来组织多种不同的类型或是构成namespace,接口通常的作用是抽象出一组类的共同特性,或是让不同的类之间可以遵守相同的协议从而简化开发工作,主体应该是接口提供的方法以及这些方法所依赖的共通的一小部分数据类型(例如上例,虽然例子不是很好);而final class则更适合组织不同数据类型和静态常量,更进一步的理由超过了本文的探讨范畴,你可以在园内搜索相关文章进一步学习。

总结

在本文中,我们学到了:

  1. 如何添加枚举的自定义数据
  2. 为枚举添加构造函数和方法
  3. 用枚举实现接口
  4. 将枚举和接口组合使用

当然,依靠反编译的代码来学习语言特性并不是一个值得推荐的选择,但确实最直观的最容易让人理解底层原理的办法。

到此这篇关于你可能并不了解的java枚举的文章就介绍到这了,更多相关你不了解的java枚举内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 三分钟快速掌握Java中枚举(enum)

    什么是枚举? 枚举是JDK5引入的新特性.在某些情况下,一个类的对象是固定的,就可以定义为枚举.在实际使用中,枚举类型也可以作为一种规范,保障程序参数安全.枚举有以下特点: Java中枚举和类.接口的级别相同. 枚举和类一样,都有自己的属性.方法.构造方法,不同点是:枚举的构造方法只能是private修饰,也就无法从外部构造对象.构造方法只在构造枚举值时调用. 使用enum关键字声明一个枚举类型时,就默认继承自Java中的 java.lang.Enum类,并实现了java.lang.Seriab

  • 全面解读Java中的枚举类型enum的使用

    关于枚举 大多数地方写的枚举都是给一个枚举然后例子就开始switch,可是我想说,我代码里头来源的数据不太可能就是枚举,通常是字符串或数字,比如一个SQL我解析后首先判定SQL类型,通过截取SQL的token,截取出来可能是SELECT.DELETE.UPDATE.INSERT.ALTER等等,但是都是字符串,此时我想用枚举就不行了,我要将字符串转换成枚举怎么转呢,类似的情况还有从数据库取出数据根据一些类型做判定,从页面传入数据,根据不同的类型做不同的操作,但是都是字符串,不是枚举,悲剧的是我很

  • 浅析Java编程中枚举类型的定义与使用

    定义枚举类型时本质上就是在定义一个类,只不过很多细节由编译器帮您补齐了,所以某些程度上,enum关键字的 作用就像是class或interface. 当您使用"enum"定义枚举类型时,实质上您定义出来的类型继承自 java.lang.Enum 类,而每个枚举的成员其实就是您定义的枚举类型的一个实例(Instance),它们都被默认为 final,所以您无法改变它们,它们也是 static 成员,所以您可以透过类型名称直接使用它们,当然最重要的,它们都 是公开的(public). 举个

  • Java的枚举类型使用方法详解

    1.背景 在java语言中还没有引入枚举类型之前,表示枚举类型的常用模式是声明一组具有int常量.之前我们通常利用public final static 方法定义的代码如下,分别用1 表示春天,2表示夏天,3表示秋天,4表示冬天. public class Season { public static final int SPRING = 1; public static final int SUMMER = 2; public static final int AUTUMN = 3; publ

  • Java(enum)枚举用法详解

    概念 enum的全称为 enumeration, 是 JDK 1.5 中引入的新特性. 在Java中,被 enum 关键字修饰的类型就是枚举类型.形式如下: enum Color { RED, GREEN, BLUE } 如果枚举不添加任何方法,枚举值默认为从0开始的有序数值.以 Color 枚举类型举例,它的枚举常量依次为RED:0,GREEN:1,BLUE:2 枚举的好处:可以将常量组织起来,统一进行管理. 枚举的典型应用场景:错误码.状态机等. 枚举类型的本质 尽管enum 看起来像是一种

  • java中枚举的详细使用介绍

    枚举特点 1.用enum定义枚举类默认继承了java.lang.Enum类而不是继承了Object类.其中java.lang.Enum类实现了java.lang.Serializable和java.lang.Comparable两个接口 2.枚举类的构造函数只能使用private访问修饰符,如果省略了其构造器的访问控制符,则默认使用private修饰: 3.枚举类的所有实例必须在枚举类中显式列出,否则这个枚举类将永远都不能产生实例.列出这些实例时,系统会自动添加public static fin

  • java 中枚举类enum的values()方法的详解

    java 中枚举类enum的values()方法的详解 前言: 关于枚举,相信使用的已经很普遍了,现在主要写的是枚举中的一个特殊方法,values(), 为什么说特殊呢,因为在Enum 的 API 文档中也找不到这个方法.接下来就看看具体的使用. 理论上此方法可以将枚举类转变为一个枚举类型的数组,因为枚举中没有下标,我们没有办法通过下标来快速找到需要的枚举类,这时候,转变为数组之后,我们就可以通过数组的下标,来找到我们需要的枚举类.接下来就展示代码了. 首先是我们自己的枚举类. public e

  • java中的枚举类型详细介绍

    枚举中有values方法用于按照枚举定义的顺序生成一个数组,可以用来历遍.我们自定义的枚举类都是继承自java.lang.Enum,拥有一下实例中的功能: 复制代码 代码如下: //: enumerated/EnumClass.java // Capabilities of the Enum class import static net.mindview.util.Print.*; enum Shrubbery { GROUND, CRAWLING, HANGING } public clas

  • Java枚举类型enum的详解及使用

     Java枚举类型enum的详解及使用 最近跟同事讨论问题的时候,突然同事提到我们为什么Java 中定义的常量值不采用enmu 枚举类型,而采用public final static 类型来定义呢?以前我们都是采用这种方式定义的,很少采用enum 定义,所以也都没有注意过,面对突入起来的问题,还真有点不太清楚为什么有这样的定义.既然不明白就抽时间研究下吧. Java 中的枚举类型采用关键字enum 来定义,从jdk1.5才有的新类型,所有的枚举类型都是继承自Enum 类型.要了解枚举类型,建议大

  • Java枚举类用法实例

    本文实例讲述了Java枚举类用法.分享给大家供大家参考.具体如下: package com.school.stereotype; /** * 活动枚举类型 * @author QiXuan.Chen */ public enum EventStatus { /** * 未发布. */ DRAFT("DRAFT", "未发布"), /** * 已发布. */ PUBLISHED("PUBLISHED", "已发布"); /**

随机推荐