Java枚举细节

2023-10-16 21:38:50 浏览数 (1)

枚举的简单使用

在java中,我们可以使用enum关键字来定义枚举:

代码语言:javascript复制
public enum Color {
    RED, GREEN, BLUE;
}

就像上面一样,我们定义了一个名为Color的枚举类,包含了RED、GREEN、BLUE三个常量。当我们使用枚举类的时候,直接通过枚举类名.枚举常量即可。就像如下的形式:

代码语言:javascript复制
void f(Color c) {
    switch (c) {
        case RED: System.out.println("It's RED"); break;
        case GREEN: System.out.println("It's GREEN"); break;
        default: System.out.println("It's BLUE");
    }
}
// 调用
f(Color.RED);
// 输出 "It's RED"
枚举的本质

虽然Java提供枚举类的定义,但是实际上他并不是Java中一个新的对象类型,我们通过对Color枚举类进行反编译,得到如下的反编译结果:

代码语言:javascript复制
$ javap Color.class 
// 反编译结果
Compiled from "Color.java"
public final class EnumerationAndAnnotation.Color extends java.lang.Enum<EnumerationAndAnnotation.Color> {
  public static final EnumerationAndAnnotation.Color RED;
  public static final EnumerationAndAnnotation.Color GREEN;
  public static final EnumerationAndAnnotation.Color BLUE;
  public static EnumerationAndAnnotation.Color[] values();
  public static EnumerationAndAnnotation.Color valueOf(java.lang.String);
  static {};
}

我们可以看到,枚举类实际上在编译的过程中,被编译器进行调整,它并不是一个新的类型,本质上依然是一个类(Color),这个类继承了java.lang.Enum,而对于每一个枚举常量,实际上是public static final修饰的枚举类的静态实例对象。

同时注意,编译器会为我们添加两个新的static方法:values() 和 valueOf(java.lang.String),其实分别作用是返回枚举类中定义的所有的枚举常量,以及根据枚举名来获取枚举常量(注意,这里就是定义枚举常量的枚举名)。

当然,由于每一个枚举常量实际上是实现了java.lang.Enum的枚举类的一个静态实例对象,而这个过程是编译器为我们进行的,所以,自然,我们可以在枚举类中定义任何的方法、变量,以及构造函数的定义:

代码语言:javascript复制
public enum Color {
    RED("RED"), GREEN("GREEN"), BLUE("BLUE");
    
    // 定义类中的实例变量
    private String colorName;
    // 定义构造函数,注意上面的枚举常量必须满足这种构造方式
    Color(String colorName) {
        this.colorName = colorName;
    }
    // 定义实例对象的方法
    public void printColorName() {
        System.out.println("Color name is "   colorName);
    }
    // 重写toString方法
    @Override
    public String toString() {
        return colorName;
    }
}

自然,我们可以枚举常量当作一个枚举的实例化对象,调用枚举类中的方法:

代码语言:javascript复制
public static void main(String[] args) {
    System.out.println(Color.RED); // RED
    Color.RED.printColorName();	// Color name is RED
}
为什么枚举构造器不能访问枚举的静态域

查看下面这段代码:

代码语言:javascript复制
public enum Color {
    RED, GREEN, BLUE;
    private static int value = 0;
    Color() {
        System.out.println(value); // 编译错误!构造器无法访问静态变量
    }
}

我们可以看到这里编译不通过,提示我们枚举构造器不能够访问枚举的静态域(以及静态变量)。我们知道,一般的类中,静态域以及静态变量是优于实例对象的变量、方法的初始化的。这里简要复习一下类中变量的加载机制:

代码语言:javascript复制
// 辅助静态变量的初始化
class Init {
    public Init(String init) {
        System.out.println("Init()"   init);
    }
}
class Fa {
    // 父类静态变量
    static final Init initFa = new Init("Fa's static");
    // 父类实例变量
    Init initFa2 = new Init("Fa's no static");
    // 父类静态域
    static {
        System.out.println("Fa's static");
    }
    // 父类构造方法
    Fa() {
        System.out.println("Fa()");
    }
}
class Su extends Fa {
    // 子类静态域
    static {
        System.out.println("Su's static");
    }
    // 子类静态变量
    static final Init initSu = new Init("Su's static");
    // 子类实例变量
    Init initSu2 = new Init("Su's no static");
    // 子类构造函数
    Su() {
        System.out.println("Su()");
    }
}

接下来,我们new出Su实例对象,并观察输出结果:

代码语言:javascript复制
public static void main(String[] args) {
    new Su();
}
/* 输出
Init()Fa's static 	// 1
Fa's static			// 2
Su's static			// 3
Init()Su's static	// 4
Init()Fa's no static// 5
Fa()				// 6
Init()Su's no static// 7
Su()				// 8
*/

我们可以看到,有static修饰的始终优于实例对象的相关的初始化的,在输出中 1 - 4 是static修饰部分,5 - 8是实例域部分。此外,在继承情形下,父类由于子,输出中 1- 2 是父类static域的初始化,3 - 4 是子类static域的初始化。在static域加载完成之后,才开始加载父类非static域,最后加载子类的非static域。注意,都为static修饰的情况下,加载顺序根绝定义时候的顺序而来,1、2与3、4就可以看出。

看到这里,也许会有点疑问,既然静态域加载优于实例域(包含构造函数),那为什么在枚举类中就不行呢?让我们回到前面对枚举类的反编译,其实答案就出来了。反编译的过程我们可以看到,我们的枚举常量实际上是我们枚举类的静态实例化对象,在编译器的修改下,我们运行加载枚举类的过程中,枚举常量是static修饰的,其他静态域也是static修饰的,枚举常量又排在其他静态域的前面,按照上面的额初始化顺序,首先就会调用构造器实例化枚举常量对象,此时,枚举类中的其他静态域都还没来得及初始化,自然在构造函数中不能访问静态域了。有人可能想说,那我静态域放到枚举常量前面,让他先加载怎么样?很遗憾,Java不允许这样做:

代码语言:javascript复制
public enum Color { // 编译不通过!!!
    private static int value = 0;
    RED, GREEN, BLUE;
}
使用抽象函数为枚举统一方法

上面的内容探讨了枚举类的一些基础,这里提一些关于使用枚举的代码策略设计。 有的时候,我们想要给枚举常量定义某一些通用的方法,同时,针对不同的枚举,该通用方法呈现不同的具体内容。例如,我现在有一个如下的Color枚举类,当针对不同的Color常量的时候,能有一个方式给我返回该颜色的十六进制颜色码。也许你会如下来实现

代码语言:javascript复制
public enum Color {
    RED, GREEN, BLUE;
    public String getHexCode() {
        switch (this) {
            case RED:
                return "#FF0000";
            case GREEN:
                return "#00FF00";
            default:
                return "#0000FF";
        }
    }
}

以上的方式较为简洁与易读,但是存在问题:枚举常量越来越多,case会不断增长,如果编写代码的过程由于不注意,增加颜色常量的时候,忘记了增加对应的case,那么编译是不会有任何的问题的,但是却隐含的将增加的颜色常量也返回的是BLUE的十六进制颜色码。

鉴于上述的问题,我们需要某种方式来防止我们犯错,能够想到的,就是通过编译器来告诉我们。于是,我们在枚举类中定一个抽象方法getHexCode,于是乎,对于每一个枚举常量,编译器会提示我们实现具体实例的getHexCode:

代码语言:javascript复制
public enum Color {
    RED {
        @Override
        public String getHexCode() {
            return "#FF0000";
        }
    }, GREEN {
        @Override
        public String getHexCode() {
            return "#00FF00";
        }
    }, BLUE {
        @Override
        public String getHexCode() {
            return "#0000FF";
        }
    };

    abstract public String getHexCode();
}

在这样的定义下,代码牺牲了一定的简洁性,但是易读性丝毫不输于最开始的方式。针对Color枚举类,我们定义了抽象方法,表明了对于Color中的每一个枚举常量,都应该有getHexCode方法,返回自己的十六进制颜色码。如果我们新添加了枚举常量,而没有实现该方法,编译器会报错警告我们。

0 人点赞