1. 枚举类介绍
1.1 什么是枚举类?
- 枚举类,是一种特殊的类。
- java中使用enum定义枚举类;就像使用class定义普通类一样;就像使用interface定义接口一样;就像使用@interface定义注解一样。
- 枚举类是jdk 1.5之后引入的新的引用类型;和class、interface、array等一样,都是用来定义新类型的。
- 枚举类定以后,java编译器默认让该类继承了java.lang.Enum类;就像java编译器默认让一个普通的class继承Object类一样;基于此,意味着任何一个枚举类不能再继承其他类。
- 同时,java编译器默认对任何一个枚举类都修饰为final,意味着任何一个枚举类是不能被其他类继承的。
- 枚举类的构造方法默认是private的,即构造方法私有化;可以不显式编写,编译器自动为我们修饰为private的;如果要显式编写构造器的权限符,也只能编写为private的,否则编译器报错。
- 因为枚举类的构造方法必须是private的,所以它的作用就很明显了;即限制外部去构造枚举实例,而是交由枚举类自己去负责实例化自己的枚举对象;枚举对象也叫做枚举常量。
- 枚举类建议一定要实例化枚举对象;尽管枚举类可以什么都不编写,但是如果连枚举对象都没有,枚举将没有任何意义;如果声明枚举对象的话,则枚举对象的实例化声明必须在枚举类的首行,不能放在其他位置;如果是无参构造则只能通过无参构造创建实例;如果显式编写了有参构造,则可通过有参构造创建对应枚举实例;多个枚举对象通过逗号分隔,如果后续还有其他内容,则枚举对象结尾使用分号。
- 枚举实例对象不能显式添加任何修饰符,java编译器编译后为自动为任何一个枚举实例对象添加修饰符 public static final ,意味着任何一个枚举实例对象实际上就是一个枚举常量;因此建议枚举对象命名全部为大写,即符合常量的命名方式。
- 枚举类也可以定义自己的成员变量,定义方式和普通的class完全一致。
- 枚举类也允许像普通类一样定义自己的静态块和构造块;静态块随着枚举类的加载只执行一次,构造块的调用随着JVM自动调用枚举类的构造方法一起执行的。
1.2 通过案例验证上述规则
定义枚举类EnumType
package zeh.test.demo.com.enum1;
// 2021-04-09
public enum EnumType {
// 枚举对象通过指定构造方法去创建,只能声明在枚举类的首行
// 如果编写在枚举类其他地方,编译器将报错
// 实例化枚举对象时不能加入任何其他修饰符
// 多个枚举对象通过逗号分隔,最后使用分号结尾
ZHANGSAN("zhangsan", 22), LISI("lisi", 18);
// 枚举类也可以定义自己的成员变量
private String name;
private int age;
private static String school;
private final String city = "西北工业大学";
{
// 定义枚举类自己的构造块
System.out.println("枚举类自己的构造块");
}
static{
System.out.println("枚举类自己的静态块");
}
// 显示编写空的枚举构造
EnumType() {
}
// 枚举类的构造器默认是private的,可以不显式编写;如果执意要显式编写,也必须是private的,否则编译报错
private EnumType(String name, int age) {
this.setName(name);
this.setAge(age);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void testMyEnum() {
System.out.println("枚举类自己提供的业务方法");
}
}
主程序
package zeh.test.demo.com.enum1;
// 2021-04-09
public class EnumMain {
public static void main(String[] args) {
// 反射获取枚举类 EnumType 的父类,结果是 Enum
Class<?> clazz = EnumType.class.getSuperclass();
System.out.println(clazz);
// 反射获取 Enum 类的父类,结果是 Object
clazz = Enum.class.getSuperclass();
System.out.println(clazz);
// 使用values()方法获取枚举类的所有枚举常量
EnumType[] enumTypes = EnumType.values();
for(EnumType enumType : enumTypes){
enumType.testMyEnum();
System.out.println(enumType);
}
EnumType enumType = EnumType.valueOf("ZHANGSAN");
System.out.println(enumType);
}
}
执行结果
class java.lang.Enum
class java.lang.Object
枚举类自己的构造块
枚举类自己的构造块
枚举类自己的静态块
枚举类自己提供的业务方法
ZHANGSAN
枚举类自己提供的业务方法
LISI
ZHANGSAN
Process finished with exit code 0
使用javap -v 反汇编EnumType.class
public final class zeh.test.demo.com.enum1.EnumType extends java.lang.Enum<zeh.test.demo.com.enum1.EnumType>
public static final zeh.test.demo.com.enum1.EnumType ZHANGSAN; //枚举常量
public static final zeh.test.demo.com.enum1.EnumType LISI; //枚举常量
private java.lang.String name;
private int age;
private static java.lang.String school;
private final java.lang.String city;
private static final zeh.test.demo.com.enum1.EnumType[] $VALUES; //枚举常量对象对应的属性数组
public static zeh.test.demo.com.enum1.EnumType[] values(); //枚举类的values方法
public static zeh.test.demo.com.enum1.EnumType valueOf(java.lang.String); //枚举类的valueOf方法
private zeh.test.demo.com.enum1.EnumType(); // 私有的空构造
private zeh.test.demo.com.enum1.EnumType(java.lang.String, int); // 私有的有参构造
public java.lang.String getName(); // 自己定义的业务方法
public void setName(java.lang.String); // 自己定义的业务方法
public int getAge(); // 自己定义的业务方法
public void setAge(int); // 自己定义的业务方法
public void testMyEnum(); // 自己定义的业务方法
static {}; // 静态块
使用第三方工具jad反编译EnumType.class
javap是jdk自带的一款反编译工具,但是它反编译后的文件不是很友好,我们一般使用它去反汇编(javap同时提供了反汇编即javap -c,或者javap -v),能够看到编译器传送到JVM的原始汇编指令,更好的了解JVM指令的底层。
因此,我们需要借助第三方工具jad去反编译字节码文件,jad反编译后的源文件是最接近原始指令的源文件,并且很友好(类似的jd反编译出来的源文件就不接近底层)。
使用jad反编译后的文件如下:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumType.java
package zeh.test.demo.com.enum1;
import java.io.PrintStream;
// java编译器编译后的枚举类自动继承了Enum抽象类
public final class EnumType extends Enum
{
// 编译器生成了values()方法,通过克隆枚举对象数组返回所有枚举常量
public static EnumType[] values()
{
return (EnumType[])$VALUES.clone();
}
// 编译器生成了valueOf(String)方法,底层实际上调用的就是Enum类的valueOf,只不过第一个参数写死了。
public static EnumType valueOf(String name)
{
return (EnumType)Enum.valueOf(zeh/test/demo/com/enum1/EnumType, name);
}
// 编译后的枚举的空构造方法实际上会接收两个参数,第一个参数表示该枚举常量的字面量,第二个参数表示该枚举常量的定义序号(序号从0开始)
// 实际上当前枚举类的构造方法首先调用了父类Enum类的构造
// 并且编译器优化了构造块,构造块的逻辑会合并到所有的构造方法中去。
private EnumType(String s, int i)
{
super(s, i);
System.out.println("\u679A\u4E3E\u7C7B\u81EA\u5DF1\u7684\u6784\u9020\u5757");
}
// 同理,对于有参构造,原理同上;在自己显式编写的构造参数前默认加上name和ordinal参数。
private EnumType(String s, int i, String name, int age)
{
super(s, i);
System.out.println("\u679A\u4E3E\u7C7B\u81EA\u5DF1\u7684\u6784\u9020\u5757");
setName(name);
setAge(age);
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public int getAge()
{
return age;
}
public void setAge(int age)
{
this.age = age;
}
public void testMyEnum()
{
System.out.println("\u679A\u4E3E\u7C7B\u81EA\u5DF1\u63D0\u4F9B\u7684\u4E1A\u52A1\u65B9\u6CD5");
}
public static final EnumType ZHANGSAN;
public static final EnumType LISI;
private String name;
private int age;
private static String school;
private final String city = "\u897F\u5317\u5DE5\u4E1A\u5927\u5B66";
// 编译器默认生成一个当前枚举类型的数组,用来缓存当前枚举类的所有枚举常量
private static final EnumType $VALUES[];
// 编译器生成了静态块,用来按照声明顺序(顺序从0开始)实例化当前枚举对象和枚举对象数组 $VALUES
// 并且编译器对静态块做了优化,将所有的静态块逻辑合并到同一个静态块中执行
static
{
ZHANGSAN = new EnumType("ZHANGSAN", 0, "zhangsan", 22);
LISI = new EnumType("LISI", 1, "lisi", 18);
$VALUES = (new EnumType[] {
ZHANGSAN, LISI
});
System.out.println("\u679A\u4E3E\u7C7B\u81EA\u5DF1\u7684\u9759\u6001\u5757");
}
}
结果分析
- 枚举类编译后继承了父类 Enum,则不能再继承其他类(因为java是单继承);Enum类继承Object类。
- 枚举类编译后使用final修饰,即枚举类不能被其他类继承。
- 枚举对象编译后使用了public static final修饰,即所有枚举对象都是常量。
- 枚举类编译后提供了static方法 values(),返回一个当前枚举类型数组,表示当前枚举类的所有枚举对象;这个方法是java编译器为当前枚举类自动生成的,不是继承Enum类的;该方法通过克隆枚举对象数据$VALUES而来。
- 枚举类编译后提供了static方法 valueOf(),返回一个当前枚举类的指定枚举对象;这个方法是java编译器为当前枚举类自动生成的,不是继承Enum类的,但实际上调用的依旧是Enum中的valueOf()方法。
- 枚举对象的产生实际上并不高深,是java编译器直接为我们生成了一个静态块,然后调用枚举类的私有构造器产生的枚举常量(java编译器会合并所有的静态块逻辑到一个静态块中)。
- 编译后构造块实际上是合并到所有的构造方法中去执行的,因此构造块就是一个语法糖而已。
- 编译器优化了当前枚举类的构造方法,每个构造方法都调用了父类Enum中的构造器,并且Enum中的构造器本身是protected的,它只能由编译器调用。
- 至此,应该明白,实际上枚举并不高深,只不过java编译器替我们做了很多工作而已。
1.3 枚举和单例
我们先来回顾下单例模式:
- 单例模式中,构造方法必须是显式编写为private的,这样才能阻止外部实例化该对象。
- 单例类自己负责实例化对象,因此需要定义一个当前类自己的成员变量,该成员必须是static的,只有这样才能随着目标类加载一次;一般建议定义为private的,防止外部直接访问成员;专门提供一个公共的方法返回该成员。
上面就是单例模式的基本要求,俗称“双私一公”;
是不是发现,枚举和单例的要求很像。
枚举实际上比单例模式具有更丰富的功能,它是java本身提供给我们的更加简约的一种单例应用。
所以,我们可以认为,枚举就是单例模式简单化。
也就是说,在java的发展中,逐渐发现单例模式用的很多,因此jdk直接提供了枚举:
构造方法私有化–》单例模式/多例模式;
单例模式/多例模式–》枚举类。
2. 探究枚举类的底层
2.1 如何获取枚举对象?
前面通过javap -v反汇编class字节码后,知道java编译器默认为每个枚举类提供了两个方法:
public static zeh.test.demo.com.enum1.EnumType[] values();
public static zeh.test.demo.com.enum1.EnumType valueOf(java.lang.String);
这两个方法都是用来获取枚举类的枚举常量的。
并且枚举类中定义的枚举对象默认被修饰成了public static final;因此可以直接通过当前枚举类获取对应的枚举常量。
其次,我们知道任何一个枚举类编译后都默认继承Enum类,我们在Enum的API中同样找到了一个方法(该方法是static,因此可以被子类继承但是不能覆盖):
public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name);
最后,观察Class类提供了一个 public T[] getEnumConstants()方法,这个方法可以返回当前枚举类的所有枚举常量。
所以,现在知道了获取一个目标枚举对象共有5种常见方式:
(1) 枚举类.枚举常量 直接获取;
(2) 通过当前枚举类中的values()方法获取所有枚举常量,然后遍历;
(3) 通过当前枚举类中的valueOf()方法获取指定名称的枚举常量;
(4) 通过Enum.valueOf()方法来获取指定枚举类型的指定名称的枚举常量。
(5) 通过Class对象的getEnumConstants()来反射获取枚举常量。
下面我们通过案例来验证前4种方式,反射获取枚举常量的方式后面再学习(EnumType枚举类和上面案例中保持一样,只修改主程序):
package zeh.test.demo.com.enum1;
// 获取目标枚举对象的方式
// 2021-04-09
public class EnumMain {
public static void main(String[] args) {
// 方式1:枚举类.枚举常量 直接获取
EnumType lisi = EnumType.LISI;
// 通过获取后的枚举对象调用其方法
lisi.testMyEnum();
System.out.println("lisi is :" + lisi);
// 方式2:通过当前枚举类自带的values方法返回所有枚举常量进行遍历
EnumType[] enumTypes = EnumType.values();
for(EnumType enumType : enumTypes){
// 通过获取后的枚举对象调用其方法
enumType.testMyEnum();
System.out.println("enumType is :" + enumType);
}
// 方式3:通过当前枚举类自带的valueOf方法获取当前枚举类指定名称的枚举常量
EnumType zhangsan = EnumType.valueOf("ZHANGSAN");
// 通过获取后的枚举对象调用其方法
zhangsan.testMyEnum();
System.out.println("zhangsan is :" + zhangsan);
// 方式4:通过Enum类提供的valueOf(Class,String)方法获取指定枚举类中的指定名称的枚举对象
EnumType enumType = Enum.valueOf(EnumType.class,"LISI");
enumType.testMyEnum();
System.out.println("enumType is :" + enumType);
// 因此valueOf(Class,String)方法是Enum类的且是public static的,因此子类EnumType也继承了它,所以可以直接使用(static方法可以被继承但是不能被覆盖)
enumType = EnumType.valueOf(EnumType.class,"ZHANGSAN");
enumType.testMyEnum();
System.out.println("enumType is :" + enumType);
}
}
2.2 枚举常量
我们知道,枚举对象实际上就是枚举常量,因为编译器默认将它们优化为public static final。
接下来我们深入了解下枚举常量:
- 枚举常量本质是当前枚举类的public、static、final的实例化对象;因为它是final的,因此编译器禁止重置它的引用值;因为它是static的,因此随着JVM加载当前枚举类,它会首先被初始化到方法区的静态区中(静静非构构初始化方法);
- 直接输出枚举常量,输出的是枚举常量的字面量字符串,而不是枚举对象的引用地址。因为枚举类继承了Enum类,而Enum覆盖了toString()方法,覆盖后的toString方法返回的就是对应枚举常量的字面量值;
- 在声明枚举常量时,实际上调用的是当前枚举类对应的构造方法。如果是有参构造,则枚举常量声明时需要传递实际参数。
2.3 Enum类提供的API
前面已经清楚,任何一个使用enum定义的枚举类,在编译后默认继承Enum类。
那么,任何一个枚举对象都可以使用Enum提供的方法。
- Enum类中提供的方法大多数都是final的,意味着这些方法不允许子类去覆盖。
- Enum提供的clone方法直接抛出异常并且是protected,这意味着哪怕使用反射去调用clone方法,也无法创建一个枚举对象的副本,保证了枚举对象在整个JVM中的单例特性。
- Enum提供的finalize方法是final的,意味着不允许覆盖;且默认是空实现,说明枚举类不能有finalize方法。
- Enum是抽象类。
- Enum本身有构造方法 protected Enum(String name, int ordinal);该方法是protected的,只能由java编译器调用。
- Enum类本身实现了Comparable 和 Serializable接口。
下面通过案例测试下Enum类的API:
package zeh.test.demo.com.enum1;
// 获取目标枚举对象的方式
// 2021-04-09
public class EnumMain {
public static void main(String[] args) {
EnumType enumType = EnumType.LISI;
// 比较两个枚举对象;如果当前枚举对象大于目标枚举对象,则返回1;等于则返回0;小于则返回-1。比较的顺序就是枚举类中声明枚举常量的顺序。
int result = enumType.compareTo(EnumType.ZHANGSAN);
System.out.println(result);
// 判断两个枚举对象是否是同一个对象,即引用地址是否相同
boolean isEqual = enumType.equals(EnumType.LISI);
System.out.println(isEqual);
// 返回描述此枚举对象的class对象
Class<EnumType> enumTypeClass = enumType.getDeclaringClass();
System.out.println(enumTypeClass);
// 返回此枚举常量的hashCode
int hashCode = enumType.hashCode();
System.out.println(hashCode);
// 返回当前枚举常量的名称,就是枚举常量的字面量含义。其实Enum覆盖的toString方法也是返回的枚举中的name
String name = enumType.name();
System.out.println(name);
name = enumType.toString();
System.out.println(name);
// 返回当前枚举常量的序数,即当前枚举常量在枚举中的声明位置,序数从0开始
int ordinal = enumType.ordinal();
System.out.println(ordinal);
// Enum提供的静态方法,返回指定枚举类型中的指定名称的枚举常量
EnumType zhangsan = Enum.valueOf(EnumType.class, "ZHANGSAN");
System.out.println(zhangsan);
}
}
3. 枚举类常见注意点
3.1 equals方法
- 任何一个枚举类都继承Enum类的equals(),Enum类对该方法的实现就是直接使用==比较两个枚举对象的引用是否相同;因此通过枚举常量调用equals()方法时一定要注意它比较的是地址,即使用Enum的equals()方法和直接使用==是完全相同的。
- 任何一个字符串覆盖的equals()方法,是比较两个字符串的内容是否相同的;因此如果使用字符串的equals()去和一个枚举常量进行比较结果返回的是false。
- 在枚举的使用中,我们很少直接使用枚举的equals去比较两个枚举对象是否相同;而是直接通过枚举对象获取其中的属性,再去比较属性是否相同(因为枚举的属性大多数都是基本数据类型和String)。
备注:
因为枚举常量本身是常量,而且直接输出的话返回的是该枚举常量的字面量字符串;所以很多时候我们会潜意识的认为枚举常量本质是个字符串,从而使用枚举常量的equals()方法区比较,这是错误的!
枚举常量直接toString后返回的是其字面量字符串,但它本质仍旧是个java对象,并不是字符串。
下面通过案例去测试:
```java package zeh.test.demo.com.enum1;
// 获取目标枚举对象的方式 // 2021-04-09 public class EnumMain { public static void main(String[] args) { EnumType enumType = EnumType.LISI; // 比较两个枚举对象,返回true boolean result = enumType.equals(EnumType.LISI); System.out.println(result); // 使用==和使用枚举的equals()方法作用完全相同,返回true result = enumType == EnumType.LISI; System.out.println(result); // 使用字符串的equals比较枚举对象,返回false result = “LISI”.equals(enumType); System.out.println(result); // 获取枚举对象的name属性值,然后再比较,返回true result = “lisi”.equals(enumType.getName()); System.out.println(result); } }
## 3.2 编译器为枚举类生成的valueOf方法和values方法
1. valueOf(String)方法和values()方法是编译器为枚举类自动生成的方法;这两个方法是static的。
2. 因为是static的,所以由当前枚举类名直接调用。
3. 当前枚举类向上转型为Enum后,无法调用这两个方法,因为Enum类中没有这两个方法。
下面通过案例验证:
```java
package zeh.test.demo.com.enum1;
// 获取目标枚举对象的方式
// 2021-04-09
public class EnumMain {
public static void main(String[] args) {
EnumType enumType = EnumType.LISI;
Enum e = enumType;
// 向上转型后无法调用values()方法和valueOf()方法(valueOf方法是一个参数的那个方法);因为Enum类中并没有这两个方法
e.values();
e.valueOf();
}
}
3.3 通过反射也可以获取枚举类的所有枚举对象
Class类中提供了两个枚举相关的方法:
public boolean isEnum():返回当前class对象是否表示的是枚举类型。
public T[] getEnumConstants():如果当前class表示枚举类型,则返回枚举类的所有枚举常量,否则返回null。
所以,我们也可以通过class对象来获取枚举类的所有枚举对象。
下面通过案例验证:
package zeh.test.demo.com.enum1;
import java.util.Arrays;
// 获取目标枚举对象的方式
// 2021-04-09
public class EnumMain {
public static void main(String[] args) {
Class>?< clazz = EnumType.class;
if(clazz.isEnum()){
EnumType[] enumTypes = (EnumType[])clazz.getEnumConstants();
System.out.println("enumTypes is :" + Arrays.toString(enumTypes));
}
}
}
4. 枚举对象的单例特性
4.1 枚举对象不能被克隆
前面说明过,Enum类中的clone方法是final、protected的,而且实现是直接抛出了异常。这意味着一个枚举对象无法clone出另外一个枚举对象。
案例
package zeh.test.demo.com.enum1;
public enum EnumType {
ZHOUJIELUN, WANGLIHONG, LINJUNJIE;
public static void main(String[] args) throws CloneNotSupportedException {
// Enum类中的clone()方法是protected的,因此只能由Enum的子类调用;其直接抛出异常,表示枚举对象不支持clone
ZHOUJIELUN.clone();
}
}
测试结果
Exception in thread "main" java.lang.CloneNotSupportedException
at java.lang.Enum.clone(Enum.java:163)
at zeh.test.demo.com.enum1.EnumType.main(EnumType.java:14)
Process finished with exit code 1
4.2 不能反射出一个枚举对象
通过反射创建枚举对象,直接抛出异常。即Class的newInstance()方法不允许反射枚举对象。
下面通过案例演示:
定义枚举类EnumType
package zeh.test.demo.com.enum1;
public enum EnumType {
ZHANGSAN("zhangsan", 22), LISI("lisi", 18);
private String name;
private int age;
// 枚举空构造,编译后编译器默认为编译为两个参数的构造即:EnumType(String name,int ordinal)
EnumType() {
}
// 枚举两参构造,编译后将成为:EnumType(String name,int ordinal,String name, int age)
EnumType(String name, int age) {
this.setName(name);
this.setAge(age);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
通过Class的newInstance()方法反射创建枚举对象
package zeh.test.demo.com.enum1;
// 获取目标枚举对象的方式
public class EnumMain {
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
Class<?> clazz = EnumType.class;
// 方式1:直接使用Class类的newInstance()方法去创建当前类的对象;该方法要求当前类必须具备public的无参构造方法
// 但是我们知道枚举类编译后,其实是没有无参构造的,哪怕有也是private的,因此该方法始终报错,即找不到指定的构造方法
clazz.newInstance();
}
}
测试结果:
Exception in thread "main" java.lang.InstantiationException: zeh.test.demo.com.enum1.EnumType
at java.lang.Class.newInstance(Class.java:427)
at zeh.test.demo.com.enum1.EnumMain.main(EnumMain.java:14)
Caused by: java.lang.NoSuchMethodException: zeh.test.demo.com.enum1.EnumType.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.newInstance(Class.java:412)
... 1 more
Process finished with exit code 1
原因分析:
枚举类编译后实际上并没有无参构造方法,因此Class类的newInstance()方法永远无法创建枚举对象。
通过Constructor反射指定参数的构造器去创建枚举对象
package zeh.test.demo.com.enum1;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
// 获取目标枚举对象的方式
public class EnumMain {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class<?> clazz = EnumType.class;
// 方式2:只能使用Constructor的newInstance()方法去获取指定参数的构造器。现在获取EnumType中两参构造器,所以得传入4个参数。
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class, String.class, int.class);
constructor.setAccessible(true);
EnumType enumType = (EnumType) constructor.newInstance("zhangsan", 12);
System.out.println(enumType);
}
}
测试结果:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at zeh.test.demo.com.enum1.EnumMain.main(EnumMain.java:18)
Process finished with exit code 1
原因分析:
我们可以观察Class类提供的newInstance()方法的源代码:
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
也就是说,源码内部限制了去反射创建枚举对象。
4.3 枚举对象不允许序列化和反序列化
5. 像使用普通class一样使用枚举
通过前面知道:我们通过enum关键字定义的一个枚举类,java编译器优化后会继承Enum类、使构造方法私有并且默认调用Enum的构造器、通过静态块初始化枚举实例等。
因此,除了这些区别之外,枚举类实际上和普通类没有其他区别了。
因此我们可以把枚举类当做普通类,实现接口、添加内部类、添加成员和方法、甚至是添加main方法。
5.1 向枚举中添加普通成员和方法等
EnumType
package zeh.test.demo.com.enum1;
public enum EnumType {
ZHANGSAN("zhangsan", 22), LISI("lisi", 18);
private String name;
private int age;
public static String school;
private final String city = "西北工业大学";
{
System.out.println("枚举类自己的构造块");
}
static {
System.out.println("枚举类自己的静态块");
}
EnumType(String name, int age) {
this.setName(name);
this.setAge(age);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void testMyEnum() {
System.out.println("枚举类自己提供的业务方法,city = " + this.city + ";school = " + EnumType.school);
}
public static void main(String[] args) {
EnumType enumTypes[] = EnumType.values();
System.out.println("enumTypes is :" + Arrays.toString(enumTypes));
EnumType.school = "西南石油大学";
EnumType enumType = EnumType.valueOf("ZHANGSAN");
enumType.testMyEnum();
}
}
执行结果
枚举类自己的构造块
枚举类自己的构造块
枚举类自己的静态块
enumTypes is :[ZHANGSAN, LISI]
枚举类自己提供的业务方法,city = 西北工业大学;school = 西南石油大学
Process finished with exit code 0
5.2 枚举类覆盖方法
既然枚举类继承Enum类,那么枚举类就可以覆盖Enum中的方法。
很遗憾,Enum类中的实例方法只有一个不是final的,那就是toString()方法。
言外之意,允许我们自定义枚举对象的输出格式。
5.3 枚举类中定义抽象方法
- 普通类定义了抽象方法后,则该类必须使用abstract进行修饰;并且需要子类去实现该抽象方法。
- 枚举和普通类类似,也可以定义抽象方法,只不过枚举对象只能由自己产生,因此枚举的抽象方法需要枚举类自己负责实现;既然必须枚举自己负责实现抽象方法,那么枚举类势必不能再使用abstract去修饰。
- 枚举内部定义抽象方法,表现出了一定的多态特性。
```java package zeh.test.demo.com.enum1;
public enum EnumType { TEST1 { @Override public void testEnum() { System.out.println(“TEST1的实现”); } }, TEST2 { @Override public void testEnum() { System.out.println(“TEST2的实现”); } }; public abstract void testEnum(); // 枚举类中直接定义main方法 public static void main(String[] args) { TEST1.testEnum(); TEST2.testEnum(); } }
## 5.4 枚举和接口
1. 枚举类只是不能继承其他类,但它可以实现多个接口。
2. 一般通过接口来组织多种类型的枚举,使得枚举看起来优雅。
枚举实现多个接口
```java
package zeh.test.demo.com.enum1;
interface IEat {
void eat();
}
interface IDrink {
void drink();
}
public enum MyEnum implements IEat, IDrink {
EAT, DRINK;
@Override
public void drink() {
System.out.println("drink");
}
@Override
public void eat() {
System.out.println("eat");
}
public static void main(String[] args) {
EAT.drink();
DRINK.eat();
}
}
通过接口组织枚举
package zeh.test.demo.com.enum1;
// 通过接口中定义内部枚举类并实现接口,来组织枚举分类并实现一定的多态性
public interface Food {
enum Mianshi implements Food {
YOUPO, DAOXIAO, SAOZOMIAN;
}
enum Mifan implements Food {
HUANGMENJI, GAIJIAOFAN, ZHUTIFAN;
}
enum Roushi implements Food {
JITUI, DAROU, HONGSHAOROU, PAIGU;
}
static void main(String[] args) {
Food food = Mianshi.DAOXIAO;
food = Mifan.GAIJIAOFAN;
food = Roushi.HONGSHAOROU;
}
}
5.5 枚举和switch case
- switch case语句用于选择结构中的一种,当匹配条件很多时使用switch case比较方便。
- switch后面的条件参数一般是int和char,jdk的后续版本支持了枚举和String。
- case后面的匹配值使用枚举时,不能使用枚举类型去引用枚举对象,直接使用枚举对象即可。
枚举结合switch case使用
package zeh.test.demo.com.enum1;
enum Color {
RED, GREEN, YELLOW;
}
public class Switch {
public static void printColor(Color color) {
switch (color) {
case RED: // 无须使用Color引用
System.out.println("红色");
break;
case YELLOW:
System.out.println("黄色");
break;
case GREEN:
System.out.println("绿色");
break;
default:
System.out.println("没有匹配的颜色");
}
}
public static void main(String[] args) {
printColor(Color.GREEN);
}
}
6. 枚举实现单例
6.1 枚举类本身作为单例
通过上面的学习,我们已经知道,jdk底层对于枚举有诸多的限制。
总之一句话,枚举对象只能是有限的,且不能通过克隆、序列化、反射等方式创建。
枚举对象是static的,其只随着目标类的装载而实例化一次。
JVM保证了枚举对象在整个JVM生命周期中都是唯一的。
因此,通过枚举实现的单例模式是最经典的。
最简单的枚举实现单例,我们需要的单例就是枚举本身。
枚举实现单例1
package zeh.test.demo.com.enum1;
public enum SingletonEnum {
// 需要使用时,直接通过SingletonEnum.INSTANCE获取该单例对象。
// 该对象只有在第一次引用时才被装载,因此是懒加载的。
INSTANCE;
}
有时候我们想加上一个static方法专门用来返回枚举单例,这更符合我们的编程习惯。
枚举实现单例2
package zeh.test.demo.com.enum1;
public enum SingletonEnum {
INSTANCE;
public static SingletonEnum getInstance() {
return INSTANCE;
}
}
往往我们需要的单例对象是比较复杂的,不会是个空对象,意味着需要的单例对象是属性和方法的。
枚举实现单例3
package zeh.test.demo.com.enum1;
public enum SingletonEnum {
INSTANCE("单例枚举对象");
public static SingletonEnum getInstance() {
return INSTANCE;
}
// 定义单例枚举自己的属性
private String name;
// 编写带参构造
SingletonEnum(String name) {
this.name = name;
}
// 枚举单例提供的业务方法
public void invoke() {
System.out.println("name = " + name);
}
}
6.2 借助枚举改造已有类
实际上很多时候,我们需要的单例对象是独立的一个Class,可能这个类已经编写好久了,我们不想把这个类修改成枚举,这时我们可以通过枚举来改造这个类。
我们将现有的MySingleton类改造为枚举单例:
package zeh.test.demo.com.enum1;
public class MySingleton {
private MySingleton() {
}
public enum SingletonEnum {
SINGLETON_ENUM;
private MySingleton instance;
SingletonEnum() {
instance = new MySingleton();
}
public MySingleton getInstance() {
return instance;
}
}
}
这种借助枚举改造已有类为单例的方式存在严重问题:
package zeh.test.demo.com.enum1;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class EnumMain {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
MySingleton mySingleton1 = MySingleton.SingletonEnum.SINGLETON_ENUM.getInstance();
MySingleton mySingleton2 = MySingleton.SingletonEnum.SINGLETON_ENUM.getInstance();
// 返回true
System.out.println("mySingleton1 == mySingleton2 ?" + (mySingleton1 == mySingleton2));
Constructor<?> constructor = MySingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
MySingleton mySingleton3 = (MySingleton) constructor.newInstance();
// 返回false
System.out.println("mySingleton1 == mySingleton3 ? " + (mySingleton1 == mySingleton3));
}
}
分析:
类似这种,借助枚举类的外壳,包一个普通类的对象。
然后利用枚举类的各种特性去保证该枚举类的对象是唯一的,既然枚举对象都是唯一的,那么枚举中包装的那个普通类的对象也是唯一的了?
这块实际上和借助静态内部类去创建单例的思想是相同的,但实际上这种并没有真正保证单例特性,因为毕竟枚举包装的类是个普通类,尽管它的构造已经私有,然而我们依旧无法避免通过反射去创建它的一个新对象。
6.3 枚举实现单例的总结
使用枚举实现单例,就直接将单例对象所在的类改造成枚举。 只有枚举对象本身才是真正的单例,其他借助枚举特性去将普通类改造成单例的方式,是存在问题的。
7. 枚举和单例的关系
- 枚举常量本身被编译器修饰为:public static final的;而单例模式中的单例一般是 private static 的即可。
- 枚举常量直接输出是枚举常量的字面量含义,因为Enum实际上覆盖了toString方法,返回的就是枚举常量的字面量名称;而单例模式中直接输出单例,输出的是单例对象的引用地址。
- 枚举类可以创建多个有限的枚举实例(枚举枚举,就是有限个数的意思,可以枚举出来的实例);而单例模式中只有一个单实例。
- 枚举本身可以实现单例模式。
8. 枚举规范
- 枚举类通常当做常量类使用,因为枚举对象直接toString后就表示其字面量,即常量字符串。
- 枚举常量命名规范:全部大写,单词之间用_分隔。
9. 枚举集合
文档信息
- 本文作者:Marshall