扒一扒: Java 中的枚举
@
在 Java 中, 枚举, 也称为枚举类型, 其是一种特殊的数据类型, 它使得变量能够称为一组预定义的常量。 其目的是强制编译时类型安全。
因此, 在 Java 中, enum 是保留的关键字。
1. 枚举的定义
在 Java 是在 JDK 1.4 时决定引入的, 其在 JDK 1.5 发布时正式发布的。
举一个简单的例子:以日常生活中的方向来定义, 因为其名称, 方位等都是确定, 一提到大家就都知道。
1.1 传统的非枚举方法
如果不使用枚举, 我们可能会这样子定义
public class Direction {
public static final int EAST = 0;
public static final int WEST = 1;
public static final int SOUTH = 2;
public static final int NORTH = 3;
}
以上的定义也是可以达到定义的, 我们在使用时
@Test
public void testDirection() {
System.out.println(getDirectionName(Direction.EAST));
System.out.println(getDirectionName(5));// 也可以这样调用
}
public String getDirectionName(int type) {
switch (type) {
case Direction.EAST:
return "EAST";
case Direction.WEST:
return "WEST";
case Direction.SOUTH:
return "SOUTH";
case Direction.NORTH:
return "NORTH";
default:
return "UNKNOW";
}
}
运行起来也没问题。 但是, 我们就如同上面第二种调用方式一样, 其实我们的方向就在 4 种范围之内,但在调用的时候传入不是方向的一个 int 类型的数据, 编译器是不会检查出来的。
1.2 枚举方法
我们使用枚举来实现上面的功能
定义
public enum DirectionEnum {
EAST, WEST, NORTH, SOUTH
}
测试
@Test
public void testDirectionEnum() {
System.out.println(getDirectionName(DirectionEnum.EAST));
// System.out.println(getDirectionName(5));// 编译错误
}
public String getDirectionName(DirectionEnum direction) {
switch (direction) {
case EAST:
return "EAST";
case WEST:
return "WEST";
case SOUTH:
return "SOUTH";
case NORTH:
return "NORTH";
default:
return "UNKNOW";
}
}
以上只是一个举的例子, 其实, 枚举中可以很方便的获取自己的名称。
通过使用枚举, 我们可以很方便的限制了传入的参数, 如果传入的参数不是我们指定的类型, 则就发生错误。
1.3 定义总结
以刚刚的代码为例
public enum DirectionEnum {
EAST, WEST, NORTH, SOUTH
}
- 枚举类型的定义跟类一样, 只是需要将 class 替换为 enum
- 枚举名称与类的名称遵循一样的惯例来定义
- 枚举值由于是常量, 一般推荐全部是大写字母
- 多个枚举值之间使用逗号分隔开
- 最好是在编译或设计时就知道值的所有类型, 比如上面的方向, 当然后面也可以增加
2 枚举的本质
枚举在编译时, 编译器会将其编译为 Java 中 java.lang.Enum
的子类。
我们将上面的 DirectionEnum
进行反编译, 可以获得如下的代码:
// final:无法继承
public final class DirectionEnum extends Enum
{
// 在之前定义的实例
public static final DirectionEnum EAST;
public static final DirectionEnum WEST;
public static final DirectionEnum NORTH;
public static final DirectionEnum SOUTH;
private static final DirectionEnum $VALUES[];
// 编译器添加的 values() 方法
public static DirectionEnum[] values()
{
return (DirectionEnum[])$VALUES.clone();
}
// 编译器添加的 valueOf 方法, 调用父类的 valueOf 方法
public static DirectionEnum valueOf(String name)
{
return (DirectionEnum)Enum.valueOf(cn/homejim/java/lang/DirectionEnum, name);
}
// 私有化构造函数, 正常情况下无法从外部进行初始化
private DirectionEnum(String s, int i)
{
super(s, i);
}
// 静态代码块初始化枚举实例
static
{
EAST = new DirectionEnum("EAST", 0);
WEST = new DirectionEnum("WEST", 1);
NORTH = new DirectionEnum("NORTH", 2);
SOUTH = new DirectionEnum("SOUTH", 3);
$VALUES = (new DirectionEnum[] {
EAST, WEST, NORTH, SOUTH
});
}
}
通过以上反编译的代码, 可以发现以下几个特点
2.1 继承 java.lang.Enum
通过以上的反编译, 我们知道了, java.lang.Enum
是所有枚举类型的基类。查看其定义
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
可以看出来, java.lang.Enum
有如下几个特征
- 抽象类, 无法实例化
- 实现了
Comparable
接口, 可以进行比较 - 实现了
Serializable
接口, 可进行序列化
因此, 相对应的, 枚举类型也可以进行比较和序列化
2.2 final 类型
final 修饰, 说明枚举类型是无法进行继承的
2.3 枚举常量本身就是该类的实例对象
可以看到, 我们定义的常量, 在类内部是以实例对象存在的, 并使用静态代码块进行了实例化。
2.4 构造函数私有化
不能像正常的类一样, 从外部 new 一个对象出来。
2.5 添加了 $values[] 变量及两个方法
- $values[]: 一个类型为枚举类本身的数组, 存储了所有的示例类型
- values() : 获取以上所有实例变量的克隆值
- valueOf(): 通过该方法可以通过名称获得对应的枚举常量
3 枚举的一般使用
枚举默认是有几个方法的
3.1 类本身的方法
从前面我的分析, 我们得出, 类本身有两个方法, 是编译时添加的
3.1.1 values()
先看其源码
public static DirectionEnum[] values() {
return (DirectionEnum[])$VALUES.clone();
}
返回的是枚举常量的克隆数组。
使用示例
@Test
public void testValus() {
DirectionEnum[] values = DirectionEnum.values();
for (DirectionEnum direction:
values) {
System.out.println(direction);
}
}
输出
EAST
WEST
NORTH
SOUTH
3.1.2 valueOf(String)
该方法通过字符串获取对应的枚举常量
@Test
public void testValueOf() {
DirectionEnum east = DirectionEnum.valueOf("EAST");
System.out.println(east.ordinal());// 输出0
}
3.2 继承的方法
因为枚举类型继承于 java.lang.Enum
, 因此除了该类的私有方法, 其他方法都是可以使用的。
3.2.1 ordinal()
该方法返回的是枚举实例的在定义时的顺序, 类似于数组, 第一个实例该方法的返回值为 0。
在基于枚举的复杂数据结构 EnumSet
和EnumMap
中会用到该函数。
@Test
public void testOrdinal() {
System.out.println(DirectionEnum.EAST.ordinal());// 输出 0
System.out.println(DirectionEnum.NORTH.ordinal()); // 输出 2
}
3.2.2 compareTo()
该方法时实现的 Comparable
接口的, 其实现如下
public final int compareTo(E o) {
Enum<?> other = (Enum<?>)o;
Enum<E> self = this;
if (self.getClass() != other.getClass() && // optimization
self.getDeclaringClass() != other.getDeclaringClass())
throw new ClassCastException();
return self.ordinal - other.ordinal;
}
首先, 需要枚举类型是同一种类型, 然后比较他们的 ordinal 来得出大于、小于还是等于。
@Test
public void testCompareTo() {
System.out.println(DirectionEnum.EAST.compareTo(DirectionEnum.EAST) == 0);// true
System.out.println(DirectionEnum.WEST.compareTo(DirectionEnum.EAST) > 0); // true
System.out.println(DirectionEnum.WEST.compareTo(DirectionEnum.SOUTH) < 0); // true
}
3.2.3 name() 和 toString()
该两个方法都是返回枚举常量的名称。 但是, name() 方法时 final 类型, 是不能被覆盖的! 而 toString 可以被覆盖。
3.2.4 getDeclaringClass()
获取对应枚举类型的 Class 对象
@Test
public void testGetDeclaringClass() {
System.out.println(DirectionEnum.WEST.getDeclaringClass());
// 输出 class cn.homejim.java.lang.DirectionEnum
}
2.3.5 equals
判断指定对象与枚举常量是否相同
@Test
public void testEquals() {
System.out.println(DirectionEnum.WEST.equals(DirectionEnum.EAST)); // false
System.out.println(DirectionEnum.WEST.equals(DirectionEnum.WEST)); // true
}
4 枚举类型进阶
枚举类型通过反编译我们知道, 其实也是一个类(只不过这个类比较特殊, 加了一些限制), 那么, 在类上能做的一些事情对其也是可以做的。 但是, 个别的可能会有限制(方向吧, 编译器会提醒我们的)
4.1 自定义构造函数
首先, 定义的构造函数可以是 private, 或不加修饰符
我们给每个方向加上一个角度
public enum DirectionEnum {
EAST(0), WEST(180), NORTH(90), SOUTH(270);
private int angle;
DirectionEnum(int angle) {
this.angle = angle;
}
public int getAngle() {
return angle;
}
}
测试
@Test
public void testConstructor() {
System.out.println(DirectionEnum.WEST.getAngle()); // 180
System.out.println(DirectionEnum.EAST.getAngle()); // 0
}
4.2 添加自定义的方法
以上的 getAngle 就是我们添加的自定义的方法
4.2.1 自定义具体方法
我们在枚举类型内部加入如下具体方法
protected void move() {
System.out.println("You are moving to " + this + " direction");
}
测试
@Test
public void testConcreteMethod() {
DirectionEnum.WEST.move();
DirectionEnum.NORTH.move();
}
输出
You are moving to WEST direction
You are moving to NORTH direction
4.2.2 在枚举中定义抽象方法
在枚举类型中, 也是可以定义 abstract 方法的
我们在DirectinEnum
中定义如下的抽象方法
abstract String onDirection();
定义完之后, 发现编译器报错了, 说我们需要实现这个方法
按要求实现
测试
@Test
public void testAbstractMethod() {
System.out.println(DirectionEnum.EAST.onDirection());
System.out.println(DirectionEnum.SOUTH.onDirection());
}
输出
EAST direction 1
NORTH direction 333
也就是说抽象方法会强制要求每一个枚举常量自己实现该方法。 通过提供不同的实现来达到不同的目的。
4.3 覆盖父类方法
在父类 java.lang.Enum
中, 也就只有 toString() 是没有使用 final 修饰啦, 要覆盖也只能覆盖该方法。 该方法的覆盖相信大家很熟悉, 在此就不做过多的讲解啦
4.4 实现接口
因为Java是单继承的, 因此, Java中的枚举因为已经继承了 java.lang.Enum
, 因此不能再继承其他的类。
但Java是可以实现多个接口的, 因此 Java 中的枚举也可以实现接口。
定义接口
public interface TestInterface {
void doSomeThing();
}
实现接口
public enum DirectionEnum implements TestInterface{
// 其他代码
public void doSomeThing() {
System.out.println("doSomeThing Implement");
}
// 其他代码
}
测试
@Test
public void testImplement() {
DirectionEnum.WEST.doSomeThing(); // 输出 doSomeThing Implement
}
5 使用枚举实现单例
该方法是在 《Effective Java》 提出的
public enum Singlton {
INSTANCE;
public void doOtherThing() {
}
}
使用枚举的方式, 保证了序列化机制, 绝对防止多次序列化问题, 保证了线程的安全, 保证了单例。 同时, 防止了反射的问题。
该方法无论是创建还是调用, 都是很简单。 《Effective Java》 对此的评价:
单元素的枚举类型已经成为实现Singleton的最佳方法。
6 枚举相关的集合类
java.util.EnumSet
和 java.util.EnumMap
, 在此不进行过多的讲述了。