计算机程序的思维逻辑 (23) - 枚举的本质

前面系列,我们介绍了Java中表示和操作数据的基本数据类型、类和接口,本节探讨Java中的枚举类型。

所谓枚举,是一种特殊的数据,它的取值是有限的,可以枚举出来的,比如说一年就是有四季、一周有七天,虽然使用类也可以处理这种数据,但枚举类型更为简洁、安全和方便。

下面我们就来介绍枚举的使用,同时介绍其实现原理。

基础

基本用法

定义和使用基本的枚举是比较简单的,我们来看个例子,为表示衣服的尺寸,我们定义一个枚举类型Size,包括三个尺寸,小/中/大,代码如下:

public enum Size {
    SMALL, MEDIUM, LARGE
}

枚举使用enum这个关键字来定义,Size包括三个值,分别表示小、中、大,值一般是大写的字母,多个值之间以逗号分隔。枚举类型可以定义为一个单独的文件,也可以定义在其他类内部。

可以这样使用Size:

Size size = Size.MEDIUM

Size size声明了一个变量size,它的类型是Size,size=Size.MEDIUM将枚举值MEDIUM赋值给size变量。

枚举变量的toString方法返回其字面值,所有枚举类型也都有一个name()方法,返回值与toString()一样,例如:

Size size = Size.SMALL;
System.out.println(size.toString());
System.out.println(size.name());

输出都是SMALL。

枚举变量可以使用equals和==进行比较,结果是一样的,例如:

Size size = Size.SMALL;
System.out.println(size==Size.SMALL);
System.out.println(size.equals(Size.SMALL));
System.out.println(size==Size.MEDIUM);

上面代码的输出结果为三行,分别是true, true, false。

枚举值是有顺序的,可以比较大小。枚举类型都有一个方法int ordinal(),表示枚举值在声明时的顺序,从0开始,例如,如下代码输出为1:

Size size = Size.MEDIUM;
System.out.println(size.ordinal());

另外,枚举类型都实现了Java API中的Comparable接口,都可以通过方法compareTo与其他枚举值进行比较,比较其实就是比较ordinal的大小,例如,如下代码输出为-1,表示SMALL小于MEDIUM:

Size size = Size.SMALL;
System.out.println(size.compareTo(Size.MEDIUM));

枚举变量可以用于和其他类型变量一样的地方,如方法参数、类变量、实例变量等,枚举还可以用于switch语句,代码如下所示:

static void onChosen(Size size){
    switch(size){
    case SMALL:
        System.out.println("chosen small"); break;
    case MEDIUM:
        System.out.println("chosen medium"); break;
    case LARGE:
        System.out.println("chosen large"); break;
    }
}

在switch语句内部,枚举值不能带枚举类型前缀,例如,直接使用SMALL,不能使用Size.SMALL。

枚举类型都有一个静态的valueOf(String)方法,可以返回字符串对应的枚举值,例如,以下代码输出为true:

System.out.println(Size.SMALL==Size.valueOf("SMALL"));

枚举类型也都有一个静态的values方法,返回一个包括所有枚举值的数组,顺序与声明时的顺序一致,例如:

for(Size size : Size.values()){
    System.out.println(size);
}

屏幕输出为三行,分别是SMALL, MEDIUM, LARGE。

枚举的好处

Java是从JDK 5才开始支持枚举的,在此之前,一般是在类中定义静态整形变量来实现类似功能,代码如下所示:

class Size {
    public static final int SMALL = 0;
    public static final int MEDIUM = 1;
    public static final int LARGE = 2;
}

枚举的好处是比较明显的:

  • 定义枚举的语法更为简洁。
  • 枚举更为安全,一个枚举类型的变量,它的值要么为null,要么为枚举值之一,不可能为其他值,但使用整形变量,它的值就没有办法强制,值可能就是无效的。
  • 枚举类型自带很多便利方法(如values, valueOf, toString等),易于使用。

基本实现原理

枚举类型实际上会被Java编译器转换为一个对应的类,这个类继承了Java API中的java.lang.Enum类。

Enum类有两个实例变量name和ordinal,在构造方法中需要传递,name(), toString(), ordinal(), compareTo(), equals()方法都是由Enum类根据其实例变量name和ordinal实现的。

values和valueOf方法是编译器给每个枚举类型自动添加的,上面的枚举类型Size转换后的普通类的代码大概如下所示:

public final class Size extends Enum<Size> {
    public static final Size SMALL = new Size("SMALL",0);
    public static final Size MEDIUM = new Size("MEDIUM",1);
    public static final Size LARGE = new Size("LARGE",2);

    private static Size[] VALUES =
            new Size[]{SMALL,MEDIUM,LARGE};

    private Size(String name, int ordinal){
        super(name, ordinal);
    }

    public static Size[] values(){
        Size[] values = new Size[VALUES.length];
        System.arraycopy(VALUES, 0,
                values, 0, VALUES.length);
        return values;
    }

    public static Size valueOf(String name){
        return Enum.valueOf(Size.class, name);
    }
}

解释几点:

  • Size是final的,不能被继承,Enum<Size>表示父类,<Size>是泛型写法,我们后续文章介绍,此处可以忽略。
  • Size有一个私有的构造方法,接受name和ordinal,传递给父类,私有表示不能在外部创建新的实例。
  • 三个枚举值实际上是三个静态变量,也是final的,不能被修改。
  • values方法是编译器添加的,内部有一个values数组保持所有枚举值。
  • valueOf方法调用的是父类的方法,额外传递了参数Size.class,表示类的类型信息,类型信息我们后续文章介绍,父类实际上是回过头来调用values方法,根据name对比得到对应的枚举值的。

一般枚举变量会被转换为对应的类变量,在switch语句中,枚举值会被转换为其对应的ordinal值。

可以看出,枚举类型本质上也是类,但由于编译器自动做了很多事情,它的使用也就更为简洁、安全和方便。

典型场景

用法

以上枚举用法是最简单的,实际中枚举经常会有关联的实例变量和方法,比如说,上面的Size例子,每个枚举值可能有关联的缩写和中文名称,可能需要静态方法根据缩写返回对应的枚举值,修改后的Size代码如下所示:

public enum Size {
    SMALL("S","小号"),
    MEDIUM("M","中号"),
    LARGE("L","大号");

    private String abbr;
    private String title;

    private Size(String abbr, String title){
        this.abbr = abbr;
        this.title = title;
    }

    public String getAbbr() {
        return abbr;
    }

    public String getTitle() {
        return title;
    }

    public static Size fromAbbr(String abbr){
        for(Size size : Size.values()){
            if(size.getAbbr().equals(abbr)){
                return size;
            }
        }
        return null;
    }
}

以上代码定义了两个实例变量abbr和title,以及对应的get方法,分别表示缩写和中文名称,定义了一个私有构造方法,接受缩写和中文名称,每个枚举值在定义的时候都传递了对应的值,同时定义了一个静态方法fromAbbr根据缩写返回对应的枚举值。

需要说明的是,枚举值的定义需要放在最上面,枚举值写完之后,要以分号(;)结尾,然后才能写其他代码。

这个枚举定义的使用与其他类类似,比如说:

Size s = Size.MEDIUM;
System.out.println(s.getAbbr());

s = Size.fromAbbr("L");
System.out.println(s.getTitle());

以上代码分别输出: M, 大号

实现原理

加了实例变量和方法后,枚举转换后的类与上面的类似,只是增加了对应的变量和方法,修改了构造方法,代码不同之处大概如下所示:

public final class Size extends Enum<Size> {
  public static final Size SMALL =
          new Size("SMALL",0, "S", "小号");
  public static final Size MEDIUM =
          new Size("MEDIUM",1,"M","中号");
  public static final Size LARGE =
          new Size("LARGE",2,"L","大号");       

  private String abbr;
  private String title;                   

  private Size(String name, int ordinal,
          String abbr, String title){
      super(name, ordinal);
      this.abbr = abbr;
      this.title = title;
  }
  //... 其他代码
}  

说明

每个枚举值经常有一个关联的标示(id),通常用int整数表示,使用整数可以节约存储空间,减少网络传输。一个自然的想法是使用枚举中自带的ordinal值,但ordinal并不是一个好的选择。

为什么呢?因为ordinal的值会随着枚举值在定义中的位置变化而变化,但一般来说,我们希望id值和枚举值的关系保持不变,尤其是表示枚举值的id已经保存在了很多地方的时候。

比如说,上面的Size例子,Size.SMALL的ordinal的值为0,我们希望0表示的就是Size.SMALL的,但如果我们增加一个表示超小的值XSMALL呢?

public enum Size {
    XSMALL, SMALL, MEDIUM, LARGE
}

这时,0就表示XSMALL了。

所以,一般是增加一个实例变量表示id,使用实例变量的另一个好处是,id可以自己定义。比如说,Size例子可以写为:

public enum Size {
    XSMALL(10), SMALL(20), MEDIUM(30), LARGE(40);

    private int id;
    private Size(int id){
        this.id = id;
    }
    public int getId() {
        return id;
    }
}

高级用法

枚举还有一些高级用法,比如说,每个枚举值可以有关联的类定义体,枚举类型可以声明抽象方法,每个枚举值中可以实现该方法,也可以重写枚举类型的其他方法。

比如说,我们看改后的Size代码(这个代码实际意义不大,主要展示语法):

public enum Size {
    SMALL {
        @Override
        public void onChosen() {
            System.out.println("chosen small");
        }
    },MEDIUM {
        @Override
        public void onChosen() {
            System.out.println("chosen medium");
        }
    },LARGE {
        @Override
        public void onChosen() {
            System.out.println("chosen large");
        }
    };

    public abstract void onChosen();
} 

Size枚举类型定义了onChosen抽象方法,表示选择了该尺寸后执行的代码,每个枚举值后面都有一个类定义体{},都重写了onChosen方法。

这种写法有什么好处呢?如果每个或部分枚举值有一些特定的行为,使用这种写法比较简洁。对于这个例子,上面我们介绍了其对应的switch语句,在switch语句中根据size的值执行不同的代码。

switch的缺陷是,定义swich的代码和定义枚举类型的代码可能不在一起,如果新增了枚举值,应该需要同样修改switch代码,但可能会忘记,而如果使用抽象方法,则不可能忘记,在定义枚举值的同时,编译器会强迫同时定义相关行为代码。所以,如果行为代码和枚举值是密切相关的,使用以上写法可以更为简洁、安全、容易维护。

这种写法内部是怎么实现的呢?每个枚举值都会生成一个类,这个类继承了枚举类型对应的类,然后再加上值特定的类定义体代码,枚举值会变成这个子类的对象,具体代码我们就不赘述了。

枚举还有一些其他高级用法,比如说,枚举可以实现接口,也可以在接口中定义枚举,使用相对较少,本文就不介绍了。

小结

本节介绍了枚举类型,介绍了基础用法、典型场景及高级用法,不仅介绍了如何使用,还介绍了实现原理,对于枚举类型的数据,虽然直接使用类也可以处理,但枚举类型更为简洁、安全和方便。

我们之前提到过异常,但并未深入讨论,让我们下节来探讨。

----------------

未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心写作,原创文章,保留所有版权。

-----------

更多相关原创文章

计算机程序的思维逻辑 (13) - 类

计算机程序的思维逻辑 (14) - 类的组合

计算机程序的思维逻辑 (15) - 初识继承和多态

计算机程序的思维逻辑 (16) - 继承的细节

计算机程序的思维逻辑 (17) - 继承实现的基本原理

计算机程序的思维逻辑 (18) - 为什么说继承是把双刃剑

计算机程序的思维逻辑 (19) - 接口的本质

计算机程序的思维逻辑 (20) - 为什么要有抽象类?

计算机程序的思维逻辑 (21) - 内部类的本质

时间: 2024-12-11 09:58:24

计算机程序的思维逻辑 (23) - 枚举的本质的相关文章

计算机程序的思维逻辑 (21) - 内部类的本质

内部类 之前我们所说的类都对应于一个独立的Java源文件,但一个类还可以放在另一个类的内部,称之为内部类,相对而言,包含它的类称之为外部类. 为什么要放到别的类内部呢?一般而言,内部类与包含它的外部类有比较密切的关系,而与其他类关系不大,定义在类内部,可以实现对外部完全隐藏,可以有更好的封装性,代码实现上也往往更为简洁. 不过,内部类只是Java编译器的概念,对于Java虚拟机而言,它是不知道内部类这回事的, 每个内部类最后都会被编译为一个独立的类,生成一个独立的字节码文件. 也就是说,每个内部

计算机程序的思维逻辑 (25) - 异常 (下)

上节我们介绍了异常的基本概念和异常类,本节我们进一步介绍对异常的处理,我们先来看Java语言对异常处理的支持,然后探讨在实际中到底应该如何处理异常. 异常处理 catch匹配 上节简单介绍了使用try/catch捕获异常,其中catch只有一条,其实,catch还可以有多条,每条对应一个异常类型,比如说: try{ //可能触发异常的代码 }catch(NumberFormatException e){ System.out.println("not valid number"); }

计算机程序的思维逻辑 (22) - 代码的组织机制

使用任何语言进行编程都有一个类似的问题,那就是如何组织代码,具体来说,如何避免命名冲突?如何合理组织各种源文件?如何使用第三方库?各种代码和依赖库如何编译连接为一个完整的程序? 本节就来讨论Java中的解决机制,具体包括包.jar包.程序的编译与连接,从包开始. 包的概念 使用任何语言进行编程都有一个相同的问题,就是命名冲突,程序一般不全是一个人写的,会调用系统提供的代码.第三方库中的代码.项目中其他人写的代码等,不同的人就不同的目的可能定义同样的类名/接口名,Java中解决这个问题的方法就是包

计算机程序的思维逻辑 (28) - 剖析包装类 (下)

本节探讨Character类,它的基本用法我们在包装类第一节已经介绍了,本节不再赘述.Character类除了封装了一个char外,还有什么可介绍的呢?它有很多静态方法,封装了Unicode字符级别的各种操作,是Java文本处理的基础,注意不是char级别,Unicode字符并不等同于char,本节详细介绍这些方法以及相关的Unicode知识. 在介绍这些方法之前,我们需要回顾一下字符在Java中的表示方法,我们在第六节.第七节.第八节介绍过编码.Unicode.char等知识,我们先简要回顾一

计算机程序的思维逻辑 (29) - 剖析String

上节介绍了单个字符的封装类Character,本节介绍字符串类.字符串操作大概是计算机程序中最常见的操作了,Java中表示字符串的类是String,本节就来详细介绍String. 字符串的基本使用是比较简单直接的,我们来看下. 基本用法 可以通过常量定义String变量 String name = "老马说编程"; 也可以通过new创建String String name = new String("老马说编程"); String可以直接使用+和+=运算符,如: S

计算机程序的思维逻辑 (9) - 条件执行的本质【转】

条件执行 前面几节我们介绍了如何定义数据和进行基本运算,为了对数据有透彻的理解,我们介绍了各种类型数据的二进制表示. 现在,让我们回顾程序本身,只进行基本操作是不够的,为了进行有现实意义的操作,我们需要对操作的过程进行流程控制.流程控制中最基本的就是条件执行,也就 是说,某些操作只能在某些条件满足的情况下才执行,在一些条件下执行某种操作,在另外一些条件下执行另外某种操作.这与交通控制中的红灯停.绿灯行条件执行是类似的. Java中表达这种流程控制的基本语法是If语句. if if的语法为: if

计算机程序的思维逻辑 (9) - 条件执行的本质

条件执行 前面几节我们介绍了如何定义数据和进行基本运算,为了对数据有透彻的理解,我们介绍了各种类型数据的二进制表示. 现在,让我们回顾程序本身,只进行基本操作是不够的,为了进行有现实意义的操作,我们需要对操作的过程进行流程控制.流程控制中最基本的就是条件执行,也就 是说,某些操作只能在某些条件满足的情况下才执行,在一些条件下执行某种操作,在另外一些条件下执行另外某种操作.这与交通控制中的红灯停.绿灯行条件执行是类似的. Java中表达这种流程控制的基本语法是If语句. if if的语法为: if

计算机程序的思维逻辑 (51) - 剖析EnumSet

上节介绍了EnumMap,本节介绍同样针对枚举类型的Set接口的实现类EnumSet.与EnumMap类似,之所以会有一个专门的针对枚举类型的实现类,主要是因为它可以非常高效的实现Set接口. 之前介绍的Set接口的实现类HashSet/TreeSet,它们内部都是用对应的HashMap/TreeMap实现的,但EnumSet不是,它的实现与EnumMap没有任何关系,而是用极为精简和高效的位向量实现的,位向量是计算机程序中解决问题的一种常用方式,我们有必要理解和掌握. 除了实现机制,EnumS

计算机程序的思维逻辑 (14) - 类的组合【转】

正所谓,道生一,一生二,二生三,三生万物,如果将二进制表示和运算看做一,将基本数据类型看做二,基本数据类型形成的类看做三,那么,类的组合以及下节介绍的继承则使得三生万物. 上节我们通过类Point介绍了类的一些基本概念和语法,类Point中只有基本数据类型,但类中的成员变量的类型也可以是别的类,通过类的组合可以表达更为复杂的概念. 程序是用来解决现实问题的,将现实中的概念映射为程序中的概念,是初学编程过程中的一步跨越.本节通过一些例子来演示,如何将一些现实概念和问题,通过类以及类的组合来表示和处