扒一扒 Java 中的枚举类型

时间:2022-09-20 22:21:03

前言

在 java 中, 枚举, 也称为枚举类型, 其是一种特殊的数据类型, 它使得变量能够称为一组预定义的常量。 其目的是强制编译时类型安全。

枚举类更加直观,类型安全。使用常量会有以下几个缺陷:

1. 类型不安全。若一个方法中要求传入季节这个参数,用常量的话,形参就是int类型,开发者传入任意类型的int类型值就行,但是如果是枚举类型的话,就只能传入枚举类中包含的对象。

2. 没有命名空间。开发者要在命名的时候以season_开头,这样另外一个开发者再看这段代码的时候,才知道这四个常量分别代表季节。

因此, 在 java 中, enum 是保留的关键字。

扒一扒 Java 中的枚举类型

1. 枚举的定义

在 java 是在 jdk 1.4 时决定引入的, 其在 jdk 1.5 发布时正式发布的。

举一个简单的例子:以日常生活中的方向来定义, 因为其名称, 方位等都是确定, 一提到大家就都知道。

1.1 传统的非枚举方法

如果不使用枚举, 我们可能会这样子定义

?
1
2
3
4
5
6
7
8
9
10
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;
 
}

以上的定义也是可以达到定义的, 我们在使用时

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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 枚举方法

我们使用枚举来实现上面的功能

定义

?
1
2
3
public enum directionenum {
 east, west, north, south
}

测试

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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 定义总结

以刚刚的代码为例

?
1
2
3
public enum directionenum {
 east, west, north, south
}
  • 枚举类型的定义跟类一样, 只是需要将 class 替换为 enum
  • 枚举名称与类的名称遵循一样的惯例来定义
  • 枚举值由于是常量, 一般推荐全部是大写字母
  • 多个枚举值之间使用逗号分隔开
  • 最好是在编译或设计时就知道值的所有类型, 比如上面的方向, 当然后面也可以增加

2 枚举的本质

枚举在编译时, 编译器会将其编译为 java 中 java.lang.enum 的子类。

我们将上面的 directionenum 进行反编译, 可以获得如下的代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 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 是所有枚举类型的基类。查看其定义

?
1
2
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()

先看其源码

?
1
2
3
public static directionenum[] values() {
return (directionenum[])$values.clone();
}

返回的是枚举常量的克隆数组。

使用示例

?
1
2
3
4
5
6
7
8
@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)

该方法通过字符串获取对应的枚举常量

?
1
2
3
4
5
@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 中会用到该函数。

?
1
2
3
4
5
@test
public void testordinal() {
system.out.println(directionenum.east.ordinal());// 输出 0
system.out.println(directionenum.north.ordinal()); // 输出 2
}

3.2.2 compareto()

该方法时实现的 comparable 接口的, 其实现如下

?
1
2
3
4
5
6
7
8
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 来得出大于、小于还是等于。

?
1
2
3
4
5
6
@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 对象

?
1
2
3
4
5
@test
public void testgetdeclaringclass() {
 system.out.println(directionenum.west.getdeclaringclass());
 // 输出 class cn.homejim.java.lang.directionenum
}

2.3.5 equals

判断指定对象与枚举常量是否相同

?
1
2
3
4
5
@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, 或不加修饰符

扒一扒 Java 中的枚举类型

自定义构造函数

我们给每个方向加上一个角度

?
1
2
3
4
5
6
7
8
9
10
11
12
13
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;
 }
}

测试

?
1
2
3
4
5
@test
public void testconstructor() {
system.out.println(directionenum.west.getangle()); // 180
system.out.println(directionenum.east.getangle()); // 0
}

4.2 添加自定义的方法

以上的 getangle 就是我们添加的自定义的方法

4.2.1 自定义具体方法

我们在枚举类型内部加入如下具体方法

?
1
2
3
protected void move() {
system.out.println("you are moving to " + this + " direction");
}

测试

?
1
2
3
4
5
@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中定义如下的抽象方法

?
1
abstract string ondirection();

定义完之后, 发现编译器报错了, 说我们需要实现这个方法

扒一扒 Java 中的枚举类型

按要求实现

扒一扒 Java 中的枚举类型

测试

?
1
2
3
4
5
@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 中的枚举也可以实现接口。

定义接口

?
1
2
3
public interface testinterface {
 void dosomething();
}

实现接口

?
1
2
3
4
5
6
7
public enum directionenum implements testinterface{
 // 其他代码
 public void dosomething() {
  system.out.println("dosomething implement");
 }
 // 其他代码
}

测试

?
1
2
3
4
@test
public void testimplement() {
 directionenum.west.dosomething(); // 输出 dosomething implement
}

5 使用枚举实现单例

该方法是在 《effective java》 提出的

?
1
2
3
4
5
6
7
public enum singlton {
 instance;
 
 public void dootherthing() {
  
 }
}

使用枚举的方式, 保证了序列化机制, 绝对防止多次序列化问题, 保证了线程的安全, 保证了单例。 同时, 防止了反射的问题。

该方法无论是创建还是调用, 都是很简单。 《effective java》 对此的评价:

单元素的枚举类型已经成为实现singleton的最佳方法。

6 枚举相关的集合类

java.util.enumset 和 java.util.enummap, 在此不进行过多的讲述了。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对服务器之家的支持。

原文链接:https://www.cnblogs.com/homejim/p/10056701.html