热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

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

这篇文章主要给大家介绍了一些关于你可能并不了解的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 ");
      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枚举内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!


推荐阅读
  • 目录一、salt-job管理#job存放数据目录#缓存时间设置#Others二、returns模块配置job数据入库#配置returns返回值信息#mysql安全设置#创建模块相关 ... [详细]
  • 本文详细介绍了如何在预装Ubuntu系统的笔记本电脑上安装Windows 7。针对没有光驱的情况,提供了通过USB安装的具体方法,并解决了分区、驱动器无法识别等问题。 ... [详细]
  • Linux环境下C语言实现定时向文件写入当前时间
    本文介绍如何在Linux系统中使用C语言编程,实现在每秒钟向指定文件中写入当前时间戳。通过此示例,读者可以了解基本的文件操作、时间处理以及循环控制。 ... [详细]
  • 本文详细介绍了Hive中用于日期和字符串相互转换的多种函数,包括从时间戳到日期格式的转换、日期到时间戳的转换,以及如何处理不同格式的日期字符串。通过这些函数,用户可以轻松实现日期和字符串之间的灵活转换,满足数据处理中的各种需求。 ... [详细]
  • 从码农到创业者:我的职业转型之路
    在观察了众多同行的职业发展后,我决定分享自己的故事。本文探讨了为什么大多数程序员难以成为架构师,并阐述了我从一家外企离职后投身创业的心路历程。 ... [详细]
  • Python 工具推荐 | PyHubWeekly 第二十一期:提升命令行体验的五大工具
    本期 PyHubWeekly 为大家精选了 GitHub 上五个优秀的 Python 工具,涵盖金融数据可视化、终端美化、国际化支持、图像增强和远程 Shell 环境配置。欢迎关注并参与项目。 ... [详细]
  • 通常情况下,修改my.cnf配置文件后需要重启MySQL服务才能使新参数生效。然而,通过特定命令可以在不重启服务的情况下实现配置的即时更新。本文将详细介绍如何在线调整MySQL配置,并验证其有效性。 ... [详细]
  • Symfony是一个功能强大的PHP框架,以其依赖注入(DI)特性著称。许多流行的PHP框架如Drupal和Laravel的核心组件都基于Symfony构建。本文将详细介绍Symfony的安装方法及其基本使用。 ... [详细]
  • 本文详细介绍了 Python 中的条件语句和循环结构。主要内容包括:1. 分支语句(if...elif...else);2. 循环语句(for, while 及嵌套循环);3. 控制循环的语句(break, continue, else)。通过具体示例,帮助读者更好地理解和应用这些语句。 ... [详细]
  • 2012年7月30日,语言岛团队宣布其智能记单词软件V0.3.4.554版本正式开源。该版本不仅支持跨平台使用,还引入了多项创新功能,旨在帮助用户更高效地记忆单词。 ... [详细]
  • 在编译BSP包过程中,遇到了一个与 'gets' 函数相关的编译错误。该问题通常发生在较新的编译环境中,由于 'gets' 函数已被弃用并视为安全漏洞。本文将详细介绍如何通过修改源代码和配置文件来解决这一问题。 ... [详细]
  • Linux环境下进程间通信:深入解析信号机制
    本文详细探讨了Linux系统中信号的生命周期,从信号生成到处理函数执行完毕的全过程,并介绍了信号编程中的注意事项和常见应用实例。通过分析信号在进程中的注册、注销及处理过程,帮助读者理解如何高效利用信号进行进程间通信。 ... [详细]
  • 本文详细介绍了如何在云服务器上配置Nginx、Tomcat、JDK和MySQL。涵盖从下载、安装到配置的完整步骤,帮助读者快速搭建Java Web开发环境。 ... [详细]
  • 对于许多初学者而言,遇到总线错误(bus error)或段错误(segmentation fault/core dump)是极其令人困扰的。本文详细探讨了这两种错误的成因、表现形式及解决方法,并提供了实用的调试技巧。 ... [详细]
  • 主调|大侠_重温C++ ... [详细]
author-avatar
ryan__bug
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有