热门标签 | 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枚举内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!


推荐阅读
  • Linux CentOS 7 安装PostgreSQL 9.5.17 (源码编译)
    近日需要将PostgreSQL数据库从Windows中迁移到Linux中,LinuxCentOS7安装PostgreSQL9.5.17安装过程特此记录。安装环境&#x ... [详细]
  • 字符串学习时间:1.5W(“W”周,下同)知识点checkliststrlen()函数的返回值是什么类型的?字 ... [详细]
  • 基于Linux开源VOIP系统LinPhone[四]
    ****************************************************************************************** ... [详细]
  • 近期在研究逆向工程,因此尝试了一些CTF题目。通过合天网络安全实验室的CTF实战演练平台(http://www.hetianlab.com/CTFrace.html),我对Linux逆向工程的掌握还不够深入,因此暂时跳过了RE300题目。首先从逆向100开始,将文件后缀名修改为.apk进行初步分析。这一过程不仅帮助我熟悉了基本的逆向技巧,还加深了对Android应用结构的理解。 ... [详细]
  • 如何在虚拟机中实现Linux与Windows主机之间的文件夹共享
    为了在虚拟机中实现Linux与Windows主机之间的文件夹共享,首先需要确保Linux系统已安装VMware Tools。如果尚未安装,可以通过虚拟机软件提供的“安装VMware Tools”选项进行安装。安装完成后,通过配置共享文件夹设置,即可实现主机与虚拟机之间的文件互传。此外,建议检查虚拟机网络设置,确保网络连接正常,以提高文件传输的稳定性和速度。 ... [详细]
  • Linux核心目录解析及其功能概述 ... [详细]
  • 如何在Linux服务器上配置MySQL和Tomcat的开机自动启动
    在Linux服务器上部署Web项目时,通常需要确保MySQL和Tomcat服务能够随系统启动而自动运行。本文将详细介绍如何在Linux环境中配置MySQL和Tomcat的开机自启动,以确保服务的稳定性和可靠性。通过合理的配置,可以有效避免因服务未启动而导致的项目故障。 ... [详细]
  • Crontab 是 Linux 系统中用于设置定时任务的强大工具。为了高效地管理和使用 Crontab,首先需要编写相应的 Shell 脚本来定义具体的任务逻辑。此外,还需要对 Crontab 进行适当的配置,以确保任务能够按时准确地执行。本文将详细介绍如何编写和管理 Crontab 定时任务,包括常见的配置选项和最佳实践,帮助用户提高任务调度的效率和可靠性。 ... [详细]
  • Shell脚本编译器的全面解析与应用指南 ... [详细]
  • Linux系统中默认安装目录有哪些?Tomcat在Linux下的默认安装路径是什么?
    在Linux系统中,默认安装目录通常包括 `/usr`, `/opt`, 和 `/var` 等。对于Tomcat而言,在Linux下的默认安装路径通常是 `/opt/tomcat` 或者 `/usr/local/tomcat`。具体路径可能会因不同的发行版和配置而有所差异。例如,在Ubuntu Server中,Tomcat的默认安装路径通常是 `/opt/tomcat`。这些目录的选择旨在确保系统的整洁性和可维护性。 ... [详细]
  • CentOS 7 中 iptables 过滤表实例与 NAT 表应用详解
    在 CentOS 7 系统中,iptables 的过滤表和 NAT 表具有重要的应用价值。本文通过具体实例详细介绍了如何配置 iptables 的过滤表,包括编写脚本文件 `/usr/local/sbin/iptables.sh`,并使用 `iptables -F` 清空现有规则。此外,还深入探讨了 NAT 表的配置方法,帮助读者更好地理解和应用这些网络防火墙技术。 ... [详细]
  • 在CentOS 7环境中安装配置Redis及使用Redis Desktop Manager连接时的注意事项与技巧
    在 CentOS 7 环境中安装和配置 Redis 时,需要注意一些关键步骤和最佳实践。本文详细介绍了从安装 Redis 到配置其基本参数的全过程,并提供了使用 Redis Desktop Manager 连接 Redis 服务器的技巧和注意事项。此外,还探讨了如何优化性能和确保数据安全,帮助用户在生产环境中高效地管理和使用 Redis。 ... [详细]
  • Linux系统中权限修改命令详解:chmod使用方法与技巧
    在Linux系统中,`chmod`命令用于修改文件和目录的访问权限。文件和目录的访问控制由其所有权和权限设置决定。本文将详细介绍`chmod`命令的使用方法和技巧,帮助用户更好地管理和控制文件系统的安全性。 ... [详细]
  • 本报告对2018年湘潭大学程序设计竞赛在牛客网上的时间数据进行了详细分析。通过统计参赛者在各个时间段的活跃情况,揭示了比赛期间的编程频率和时间分布特点。此外,报告还探讨了选手在准备过程中面临的挑战,如保持编程手感、学习逆向工程和PWN技术,以及熟悉Linux环境等。这些发现为未来的竞赛组织和培训提供了 valuable 的参考。 ... [详细]
  • 您的数据库配置是否安全?DBSAT工具助您一臂之力!
    本文探讨了Oracle提供的免费工具DBSAT,该工具能够有效协助用户检测和优化数据库配置的安全性。通过全面的分析和报告,DBSAT帮助用户识别潜在的安全漏洞,并提供针对性的改进建议,确保数据库系统的稳定性和安全性。 ... [详细]
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社区 版权所有