Java之枚举
目录
枚举
引入
定义
代码示例
常用方法
代码示例
枚举的优缺点
枚举和反射
面试题
枚举
引入
枚举是在JDK1.5以后引入的。主要用途是:将一组常量组织起来,在这之前表示一组常量通常使用定义常量的方式:
publicstaticintfinalRED=1;
publicstaticintfinalGREEN=2;publicstaticintfinalBLACK=3;
但是常量举例有不好的地方,例如:可能碰巧有个数字1,但是他有可能误会为是RED,现在我们可以直接用枚举来进行组织,这样一来,就拥有了类型,枚举类型。而不是普通的整形1。
定义
在Java中,枚举类型是通过关键字enum来定义的。
枚举的定义类似于类的定义,但它使用enum关键字而不是class关键字。枚举可以包含字段、方法和构造函数,但构造函数默认是私有的,以防止外部代码创建枚举的实例。
本质:是 java.lang.Enum的子类,也就是说,自己写的枚举类,就算没有显示继承 Enum,但是它默认继承了这个类。
publicenumTestEnum{
RED,BLACK,GREEN;
}
代码示例
public enum TestEnum {
//枚举对象
RED,WHITE,GREEN;
public static void main(String[] args) {
TestEnum testEnum = TestEnum.RED;
switch (testEnum) {
case RED:
System.out.println("红色");
break;
case GREEN:
System.out.println("绿色");
break;
case WHITE:
System.out.println("白色");
break;
default:
break;
}
}
}
运行结果:
常用方法
方法名称 | 描述 |
values() | 以数组形式返回枚举类型的所有成员 |
ordinal() | 获取枚举成员的索引位置 |
valueOf() | 将普通字符串转换为枚举类型 |
compareTo() | 比较两个枚举类型成员在定义时的顺序 |
当枚举对象有参数后,需要提供相应的构造函数,枚举的构造函数默认是私有的。
代码示例
public enum TestEnum {
// 枚举对象
RED(1,"RED"),
WHITE(2,"WHITE"),
GREEN(3,"GREEN");
public String color;
public int ordinal;
private TestEnum(int ordinal,String color) {
this.ordinal = ordinal;
this.color = color;
}
public static void main(String[] args) {
TestEnum[] testEnums = TestEnum.values();
for (int i = 0; i < testEnums.length; i++) {
System.out.println(testEnums[i]+" "+testEnums[i].ordinal());
}
System.out.println("=====");
TestEnum testEnum = TestEnum.valueOf("RED");
System.out.println(testEnum);
System.out.println(RED.compareTo(WHITE));
}
}
运行结果:
枚举的优缺点
优点
1.类型安全:枚举类型是编译时常量,它们比使用整数值或字符串常量更加安全。
2.自动封装:枚举类型提供了编译时的类型检查,确保了只有声明在枚举中的值才能被赋值给枚举类型的变量。
3.可以包含字段和方法:枚举类型可以拥有字段、方法和构造函数。
4.构造器限制:枚举的构造函数默认是私有的,防止外部代码实例化枚举。
5.实现接口:枚举类型可以实现一个或多个接口。
缺点:
1.不可变性限制:Java中的枚举实例默认是不可变的(即枚举值一旦创建,其状态就不能改变)。这种不可变性在大多数情况下是一个优点,因为它有助于保证线程安全和简化代码逻辑。然而,在某些情况下,你可能希望枚举值能够改变其状态,但这在Java枚举中是不被允许的。虽然你可以通过枚举中的方法改变枚举关联的其他对象的状态,但这通常不是最佳实践。
2.继承限制:Java中的枚举类型隐式地继承自java.lang.Enum类,并且Java不支持多重继承。因此,枚举类型不能继承自除java.lang.Enum之外的任何其他类。这限制了枚举的灵活性,并可能导致一些设计上的折衷。
枚举和反射
枚举是否能通过反射拿到实例对象呢?
代码示例
public enum TestEnum {
// 枚举对象
RED(1,"RED"),
WHITE(2,"WHITE"),
GREEN(3,"GREEN");
public String color;
public int ordinal;
private TestEnum(int ordinal,String color) {
this.ordinal = ordinal;
this.color = color;
}
public static void main(String[] args) throws ClassNotFoundException {
Class<?> c = Class.forName("enumDemo.TestEnum");
try {
Constructor<?> constructor
= c.getDeclaredConstructor(int.class,String.class);
constructor.setAccessible(true);
TestEnum testEnum
= (TestEnum)constructor.newInstance(99,"hello");
System.out.println(testEnum);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
运行结果:
我们注意到,异常信息是:java.lang.NoSuchMethodException: enumDemo.TestEnum.<init>(int, java.lang.String)。
这是什么意思呢?就是没有对应的构造方法,但是我们明明已经提供了枚举的构造方法且两个参数分别是 int 和 String,那么问题出在哪里呢?
前面我们提到,所有的枚举类都默认继承于 java.lang.Enum,继承了父类除构造函数之外的所有内容,并且子类要帮助父类进行构造,而我们写的类并没有帮助父类构造,那是否意味着,我们要在实现的枚举类中提供 super 呢?
并不是,枚举类比较特殊,虽然我们实现的构造函数是两个参数,但是它默认还添加了两个参数,那添加的两个参数是什么呢?下面我们看一下Enum类的部分源码:
也就是说,我们自己的构造函数有两个参数一个是int一个是String,同时他默认在此前面还会给两个参数,一个是String一个是int。也就是说,这里我们正确给的是4个参数,修改后的代码:
public static void main(String[] args) throws ClassNotFoundException {
Class<?> c = Class.forName("enumDemo.TestEnum");
try {
Constructor<?> constructor
= c.getDeclaredConstructor(String.class,int.class,int.class,String.class);
constructor.setAccessible(true);
TestEnum testEnum
= (TestEnum)constructor.newInstance("ceshi",999,99,"hello");
System.out.println(testEnum);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
运行结果:
为什么此时在newInstance()方法会抛出异常: java.lang.IllegalArgumentException: Cannot reflectively create enum objects。
下面我们看看 newInstance() 方法的源码:
解释:
在这段代码中,if ((clazz.getModifiers() & Modifier.ENUM) != 0) 这一行是用来检查给定的类(clazz)是否是一个枚举(Enum)类型。
这里使用了位运算和位掩码的概念来检查类的修饰符中是否包含了枚举(Enum)的修饰符。在Java中,每个类都可以有一组修饰符,这些修饰符定义了类的性质,比如是否是公开的(public)、私有的(private)、受保护的(protected)、抽象的(abstract)、最终的(final)等,以及是否是枚举(enum)。这些修饰符在内部是通过整数(通常是int类型)的位模式来表示的,每个修饰符都对应一个特定的位位置。
Modifier.ENUM 是一个在 java.lang.reflect.Modifier 类中定义的常量,它代表了枚举类型的位掩码。这个常量是一个整数,其位模式中的某一位(或几位,但在这个上下文中通常只是一位)被设置为1,以表示枚举类型。
clazz.getModifiers() 方法返回了一个整数,这个整数包含了clazz类的所有修饰符的位模式。
& 是按位与(AND)运算符,它对两个整数进行操作,并返回一个新的整数,这个整数的每一位都是原来两个整数对应位进行AND操作的结果。如果两个整数在某一位上都是1,则结果在该位上也是1;否则,结果在该位上是0。
因此,clazz.getModifiers() & Modifier.ENUM 的结果是一个整数,它只在clazz的修饰符中包含枚举类型修饰符时才不为0。如果结果不为0,说明clazz是一个枚举类型;如果结果为0,说明clazz不是一个枚举类型。
所以,if ((clazz.getModifiers() & Modifier.ENUM) != 0) 这行代码的意思是:“如果clazz是一个枚举类型,则执行接下来的代码块(在这个例子中,是抛出一个IllegalArgumentException异常)”。这是因为在Java中,你不能通过反射来实例化枚举类型的对象,因为枚举的实例通常是通过枚举类型本身定义的常量来访问的,而不是通过构造函数创建的。
面试题
为什么枚举实现单例是线程安全的?
1.自动线程安全:Java的枚举类型是自动支持线程安全的。因为枚举类型本质上是通过类的静态字段来实现的,并且JVM保证了每个枚举实例在JVM中是唯一的。这意味着,在并发环境下,枚举实例的创建和访问都是线程安全的,无需额外的同步措施。
2.防止反射攻击:Java的枚举还有一个重要的特性,那就是它们默认是final的,并且枚举的构造函数默认也是私有的。这意味着枚举的实例不能被继承,也不能通过反射来调用枚举的构造函数来创建新的实例(尽管技术上可以通过反射调用私有构造函数,但Java平台禁止通过反射实例化枚举,如果尝试这样做,会抛出IllegalArgumentException或IllegalAccessException异常)。这种限制确保了枚举实例的唯一性和不可变性,从而增强了单例模式的安全性。
3.自动序列化机制:枚举类型还提供了自动的序列化机制。当枚举实例被序列化时,Java序列化机制仅仅是将枚举的name属性(即枚举常量的名称)输出到序列化流中,而不是像普通对象那样序列化其状态。反序列化时,Java通过枚举的name来查找枚举类型中对应的枚举常量,从而恢复枚举实例。这个机制确保了枚举实例在序列化和反序列化过程中仍然保持单例。
4.简单性和易用性:使用枚举实现单例模式非常简单,只需要定义一个枚举类型,并在其中定义一个枚举常量即可。这种实现方式既简洁又直观,易于理解和维护。
综上所述,枚举实现单例模式之所以被认为是安全的,主要是因为它提供了自动的线程安全、防止了反射攻击、支持自动序列化机制,并且实现简单、易用。这些特性使得枚举成为实现单例模式的理想选择之一。