Enum
有时候变量的取值只有在一个有限的集合内。例如服装的尺码只有大、中、小和超大这四种尺寸。针对这种情况就可以自定义枚举类型。枚举类型包含有限个命名的值。
声明
public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE }
这个声明定义的类型是一个类,它定义了四个实例,而且它们都是由public static final
修饰的。
实例化一个枚举对象时,只能初始化为null或者集合中的某个常量值。
因此在比较两个枚举类型的值时,不需要调用equals,而直接使用"=="就可以(直接比较地址)。
上述声明中看到一个关键词enum
这和class
、interface
类似。同样在一个Java源文件中,只允许有一个public修饰的枚举类,而且类名必须和文件名相同。
在声明中,定义了四个实例,他们都继承自java.lang.Enum,并实现了 java.lang.Seriablizable 和 java.lang.Comparable 两个接口。
进阶自定义Enum
如果有需要可以在Enum类中添加成员变量,构造器和方法。此时有以下几点需要注意:
- 在定义新的方法、构造器或者成员变量之前,必须先定义好Enum实例,同时给实例序列末尾加上";"。
- 添加成员变量时,必须注意在预先定义好的实例后添加括号,将成员变量初始化值放进去。它们会通过构造函数来初始化成员变量。
- 添加构造器时一定要用private修饰,私有化,这样一来在外部就不能实例化。构造器只有在构造枚举常量的时候被调用。
public enum Size {
SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
private Size(String abbreviation) {
this.abbreviation = abbreviation;
}
private String abbreviation;
public String getAbbreviation() {
return abbreviation;
}
}
继承自Enum方法
和直接通过public static final
修饰声明常量不同的时,Enum常量为其提供了一系列方法。
toString()
这个实例方法在Enum中默认的实现是能够返回枚举常量名。
例如Size.SMALL.toString();
将返回字符串"SMALL"。
当然可以在自定义枚举类时赋写它。
valueOf()
toString()
的逆方法时静态方法valueOf()
。
例如Size s = Enum.valueOf(Size.class, "SMALL");
将完成枚举类实例化。
这个其实类似Integer等基本类型包装类中的
valueOf()
,内部维护一个数组存储所有预定义的实例,通过valueOf()
来返回堆内存中的地址。
values()
Enum类中有一个静态方法values()
,它将返回一个包含全部枚举常量值的数组,顺序是按照声明的顺序。
例如Size[] values = Size.values();
。
oridinal()
该实例方法返回enum声明中枚举常量的位置,位置从0开始计数,顺序是按照声明的顺序。
例如Size.SMALL.oridinal();
返回0。
name()
该实例方法返回enum声明时的常量名,和toString()
类似。
compareTo()
实际上,Enum类有一个类型参数。实例化枚举类对象Size s = Size.SMALL;
,实际上应该继承自Enum<Size>
。该类型参数主要在compareTo(E other)
方法中的other使用。
比较结果:
- 如果枚举常量出现在other之前,返回一个负值。
- 如果枚举常量==other,返回0。
- 否则返回正值。
也就是默认实现按照enum声明的常量顺序来比较。
Enum使用
常量
可以通过声明一个枚举类型,包含所需的常量值:
public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE }
并且可以使用Enum的方法。
switch-case
jdk1.5之后,switch-case支持使用枚举类型进行判断。
在switch()括号中可以使用枚举类对象,case标签使用枚举类常量值。注意case标签中不需要指明枚举名,由switch()括号中确定。
enum Signal {
GREEN, YELLOW
}
public class TrafficLight {
Signal color = Signal.RED;
public void change() {
switch (color) {
case YELLOW:
color = Signal.RED;
break;
case GREEN:
color = Signal.YELLOW;
break;
}
}
}
自定义枚举类
可以添加构造器,方法或者成员变量,满足上面讨论的几点要求,就可以实现。
代码示例在上面,进阶自定义Enum
覆盖Enum方法
一般默认的toString()方法无法满足平时开发的需求,那么就可以自己去实现逻辑。
@Override
public String toString() {
return "the mobile's name is " + flagshipMachine +
". The mobile's build version is " + buildVersion;
}
上述代码返回的字符串时关于手机信息。
实现接口
由于Java规定不能有多继承,而自定义的枚举类都继承自Enum,所以枚举类不能再继承其他类。那么有什么办法扩展枚举类中实例元素呢?
可以使用接口来进行扩展,实现多态特性。
原理
接口的作用:接口抽象了方法,由子类去实现它。在执行代码中通过调用实例变量的实际类型,来调用匹配方法名的方法。
这是一种编程思想,如果一开始,使用一个具体的类来实现描述一个手机,而此时描述的是塞班系统。日后科技的发展,有了Android、ios、WindowPhone...其他系统,那么只能放弃整个类,去重新实现一个。关键是放弃塞班类之后,需要在执行代码中去修改使用塞班类对象的地方。这个工作量非常大。此时可以使用接口思想来处理。
在一开始设计时,定义一个Phone接口,里面定义了所有关于手机的基本功能抽象方法,然后通过子类去实现它。在执行代码中通过Phone类型变量来调用相关手机基本功能方法。
参考
枚举类实现接口
接口代码:
public interface Phone {
void callPhone();
void sendMessage();
}
枚举类代码:
enum Ios implements Phone {
APPLE(10, "Iphone");
private int buildVersion;
private String flagshipMachine;
private Ios(int buildVersion, String flagshipMachine) {
this.buildVersion = buildVersion;
this.flagshipMachine = flagshipMachine;
}
public void saySiri() {
System.out.println("Hello I'm siri!");
}
@Override
public void callPhone() {
System.out.println("Make a phone with FaceTime");
}
@Override
public void sendMessage() {
System.out.println("Send a message with IMessage");
}
@Override
public String toString() {
return "the mobile's name is " + flagshipMachine +
". The mobile's build version is " + buildVersion;
}
}
当然也可以在每一个预定义的实例后去实现接口抽象方法特定逻辑,但是在enum类中必须实现接口提供的抽象方法,因为enum类implements接口。
enum BlackBerry implements Phone {
BLACKBERRY {
@Override
public void callPhone() {
System.out.println("Make phone with BBM");
}
@Override
public void sendMessage() {
System.out.println("send a message with BBM");
}
};
@Override
public void callPhone() {
}
@Override
public void sendMessage() {
}
}
分组枚举类,返回某一类枚举
以模拟用户选择手机案例来实现。现在市场上主流的职能手机有三个:Android、iOS和WindowPhone。现在将环境抽象,只有这三种系统可以选择,而且每一种系统只有几个固定的手机制造商。
那么有四个枚举类,一是Android系统旗舰机枚举类;二是ios系统旗舰机枚举类;三是WindowPhone系统旗舰机枚举类;最后是供用户选择系统的枚举类。
接口分组枚举类
通过接口,可以将拥有同一共性的枚举类进行分类,同时还可以提供扩展。为什么采用接口进行分组,是因为接口内部的enum在编译时,默认会加上public static
修饰,可以很方便的在接口外部通过接口名访问。
/**
* 用接口组织分组枚举类,同时实现接口共同方法,方便在代码中使用多态特性。
**/
public interface Phone {
//每一个枚举类都有自己的方法
enum Android implements Phone {
GOOGLE(7, "Pixel"), SAMSUNG(6, "Galaxy"), XIAOMI(6, "XIAOMI"), ONEPLUS(6, "ONEPLUS_3");
private int buildVersion;
private String flagshipMachine;
private Android(int buildVersion, String flagshipMachine) {
this.buildVersion = buildVersion;
this.flagshipMachine = flagshipMachine;
}
public void sayOkGoogle() {
System.out.println("What can I help you ?");
}
@Override
public void callPhone() {
System.out.println("Make a phone with Google Hangouts");
}
@Override
public void sendMessage() {
System.out.println("Send a message with Google Hangouts");
}
@Override
public String toString() {
return "the mobile's name is " + flagshipMachine +
". The mobile's build version is " + buildVersion;
}
}
enum Window implements Phone {
NOKIA(10, "Lumia");
private int buildVersion;
private String flagshipMachine;
private Window(int buildVersion, String flagshipMachine) {
this.buildVersion = buildVersion;
this.flagshipMachine = flagshipMachine;
}
public void sayCortana() {
System.out.println("I'm Cortana,what can I Help you ?");
}
@Override
public void callPhone() {
System.out.println("Make a phone with Lumia");
}
@Override
public void sendMessage() {
System.out.println("Send a message with Lumia");
}
@Override
public String toString() {
return "the mobile's name is " + flagshipMachine +
". The mobile's build version is " + buildVersion;
}
}
enum Ios implements Phone {
APPLE(10, "Iphone");
private int buildVersion;
private String flagshipMachine;
private Ios(int buildVersion, String flagshipMachine) {
this.buildVersion = buildVersion;
this.flagshipMachine = flagshipMachine;
}
public void saySiri() {
System.out.println("Hello I'm siri!");
}
@Override
public void callPhone() {
System.out.println("Make a phone with FaceTime");
}
@Override
public void sendMessage() {
System.out.println("Send a message with IMessage");
}
@Override
public String toString() {
return "the mobile's name is " + flagshipMachine +
". The mobile's build version is " + buildVersion;
}
}
void callPhone();
void sendMessage();
}
定义了Phone接口,在里面定义了抽象的打电话,发短信功能的方法,由特定的系统枚举类实现。同时每一个枚举类都拥有各自独特的语音助手方法。最后覆写了toString()
。
枚举元素中的枚举
/**
* 扩展枚举类中的元素为枚举类。同时限定用户只能选择这三种常量。
**/
public enum PhoneUser {
ANDROID(Phone.Android.class),
WINDOW(Phone.Window.class),
IOS(Phone.Ios.class);
private Phone[] values;
private PhoneUser(Class<? extends Phone> kind) {
values = kind.getEnumConstants();
}
public Phone randomSelection() {
return Enums.random(values);
}
}
定义了一个用户选择枚举类,里面的每一个元素包含一个Phone接口的数组,存放对应系统的旗舰机枚举对象。
代码中有用到一个工具类,实现随机分配指定系统类型的一款手机。
public class Enums {
private static Random random = new Random(47);
public static Phone random(Phone[] values) {
return values[random.nextInt(values.length)];
}
}
测试
public class SelectPhone {
public static void main (String[] args) {
Phone mobile = null;
Scanner in = new Scanner(System.in);
System.out.println("What kind of mobile phone do you want?(Android,Windwo,Ios)");
String input = in.next().toUpperCase();
PhoneUser user = null;
while(true) {
try {
user = Enum.valueOf(PhoneUser.class, input);
break;
}catch(IllegalArgumentException e) {
System.out.println("Wrong system name,please input again!");
input = in.next().toUpperCase();
}
}
mobile = user.randomSelection();
if(mobile instanceof Enum) {
System.out.println("The mobile info is " + ((Enum)mobile));
}
System.out.println("Do you want make a phone?(Y/N)");
input = in.next().toUpperCase();
if(input.equals("Y")) {
mobile.callPhone();
}
System.out.println("Do you want send a message?(Y/N)");
input = in.next().toUpperCase();
if(input.equals("Y")) {
mobile.sendMessage();
}
System.out.println("Do you want use voice assistant?(Y/N)");
input = in.next().toUpperCase();
if(input.equals("Y")) {
if(mobile instanceof Phone.Android) {
((Phone.Android)mobile).sayOkGoogle();
}else if(mobile instanceof Phone.Window) {
((Phone.Window)mobile).sayCortana();
}else {
((Phone.Ios)mobile).saySiri();
}
}
}
}
执行输出:
枚举类中的抽象方法
public enum Operation{
PLUS {
@Override
public double calculate(double x, double y) {
return x + y;
}
},
MINUS {
@Override
public double calculate(double x, double y) {
return x - y;
}
},
TIMES {
@Override
public double calculate(double x, double y) {
return x * y;
}
},
DIVIDE {
@Override
public double calculate(double x, double y) {
return x / y;
}
};
//抽象方法,给每一个实例自己实现
public abstract double calculate(double x, double y);
}
针对加减乘除操作枚举类,每一种算法都有自己实现的calculate()
方法。注意,虽然enum类中有抽象方法,但是不能在enum关键词前加上abstract标注为抽象类(因为枚举类在编译时JVM会自动为它加上abstract)。
但是由于枚举类会根据预定义的实例来创建枚举常量值,所以在定义每个实例时都必须实现抽象方法。
问题
enum类中可以有静态变量吗?构造器中可以引用吗?
静态变量
enum中可以声明静态变量,或者静态常量。但是在Java规范中,规定了enum里的构造器、初始化器和初始化块中不得引用该enum中非编译时常量的静态成员域。这就涉及到了enum类加载过程。
enum类加载
将enum类的字节码文件反编译后可以看到:
- Color被final关键词修饰,并且继承自Enum<Color>。
- 分别实现了两个静态方法,
values()
和valuesOf()
。 - 构建了一个静态代码块,实例化预定义的类。
说明预定义的类是在enum类加载初始化阶段,调用构造器创建的,而且是最先执行的。
所以在enum类中构造器可以去引用编译期常量。
非编译期静态常量&静态变量
非编译期静态常量
例如
enum Color {
RED, GREEN, BLUE;
static final Map<String,Color> colorMap = new HashMap<String,Color>();
Color() {
colorMap.put(toString(), this);
}
}
代码中colorMap
就是非编译期常量,而且它不会在类加载连接过程中初始化为null
,只有在声明时初始化,或者静态代码块中初始化。所以没有Java规范(规定了enum里的构造器、初始化器和初始化块中不得引用该enum中非编译时常量的静态成员域),执行会出现尚未初始化报错。
有了Java规范,那么在编译时就无法通过,避免了运行时错误发生。但是可以通过方法来访问colorMap
。
enum Color {
RED, GREEN, BLUE;
static final Map<String,Color> colorMap = new HashMap<String,Color>();
Color() {
//colorMap.put(toString(), this);
registerValue();
}
private void registerValue() {
PowerOfTwo.map.put(value, this);
}
}
这样可以编译通过,但是一旦执行,就会抛出空指针异常,整个程序就会crash。
正确的写法是:
public enum Color{
RED, GREEN, BLUE;
public static final Map<String, Color> colorMap =
new HashMap<>();
static {
for(Color c : Color.values()) {
colorMap.put(c.toString(), c);
}
}
//public static Map<String, Color> colorMap =
//new HashMap<>();
}
在静态代码块中进行Map存储。
这里还有一点需要注意,类加载连接阶段会给静态变量初始化JVM默认值,而引用类型默认是null
。而又有final修饰,并不会赋予默认值。如果像注释代码一样声明,那么编译会报非法向前引用的错误。所以必须在静态代码块前声明并且初始化。
静态变量
例如
public enum Operation{
...
/**
* 创建的实例个数,计数
* 但是无法在构造器中直接引用,编译时会提示“初始化程序中对静态字段的引用不合法”
**/
public static int operationCount = 0;
//抽象方法,给每一个实例自己实现
public abstract double calculate(double x, double y);
private Operation() {
int i = operationCount; //编译时会提示“初始化程序中对静态字段的引用不合法”
addInstantCount();
}
private void addInstantCount() {
operationCount++;
}
}
代码中基本类型静态变量operationCount
,由于Java规范不可以直接在构造器中引用。但是可以通过方法addInstantCount()
进行修改。
这是因为在类加载连接过程中,对基本类型变量会赋予JVM默认值,而且存储在方法区中,且地址唯一,没有副本。那么在实例化预定义类时操作的是同一块内存。所以在本例中operationCount
用于计数实例个数,最终得到的值也是4。
但是引用类型无法以方法的形式在构造器中使用,因为类加载连接阶段初始化为null,运行时会空指针。