最近重新阅读《Java编程思想》与《Java编程逻辑》两本书时,读到了枚举章节,以前一直是使用,大概知道其原理,未进行过深入的总结。今天借这个机会,对枚举的那些事儿,我们详尽的梳理一下。
1. 概念
枚举是什么?被问到这个问题,用自己的大白话来说,就是Java定义的一种特殊的数据(注意:这里不是数据类型,至于为什么?稍后您就理解了)。
枚举的取值是有限的,是可以枚举出来的,那就是固定的那些,例如:一年四季、一周有七天等。
2. 定义与使用
2.1 定义
衣服的尺寸,有大、中、小,那么我们代码中,可以使用枚举定义为:
public enum Size {
SMALL, MEDIUM, LARGE
}
枚举使用enum这个关键字来定义,Size包括三个值,分别表示小、中、大,值一般是大写的字母,多个值之间以逗号分隔。枚举类型可以定义为一个单独的文件,也可以定义在其他类内部。
2.2 基本使用
class Main {
public static void main(String[] args) {
Size size = Size.MEDIUM;
}
}
Size size声明了一个变量size,它的类型是Size, size=Size.MEDIUM将枚举值MEDIUM赋值给size变量。
2.3 枚举本身拥有的方法
大家不知道注意过没有,Java枚举本身已经实现了很多方法,如下
从图中可以看到,除了Object的一些方法依然,枚举常量有compareTo(E o)、valueOf(Class<T> enumType,String name)、equals(Object other)、ordinal()、name()
这些关键方法。接下来,我们一一看一下,这些方法的作用是什么?
写个简单的Demo运行一下:
class Main {
public static void main(String[] args) {
Size size = Size.MEDIUM;
System.out.println("size.compareTo(Size.MEDIUM) = " + size.compareTo(Size.MEDIUM));
System.out.println("size.equals(Size.MEDIUM) = " + size.equals(Size.MEDIUM));
System.out.println("size == Size.MEDIUM = " + (size == Size.MEDIUM));
System.out.println("size.name = " + size.name());
System.out.println("size.ordinal = " + size.ordinal());
}
}
运行结果截图:
可以看到compareTo、equals、==如我们所料,是对比是否相等,name返回的是定义的枚举常量值,ordinal返回的是定义的枚举常量的顺序。
小知识点回顾:equals与-=-的区别与联系?
不知大家是否可以记得,之前我们讲过,equals与\==符合在java中立意不同,前者本身立意是对比两个对象的内容是否相同,后者对比两个对象的内存地址是否相同。
- 对于Java八大基本数据类型来说,equals与==,返回的结果是相同的
- 对于Java引用数据类型来说,立意上,equals对比是两个对象的内容,==对比的是两个对象的内存地址
为了验证大家对于上面小知识点是否已经掌握牢靠,猜猜下面代码的运行结果是什么?(文章末尾有答案哦~)
class Main {
public static void main(String[] args) {
Integer a = 26;
Integer b = 26;
System.out.println(a == b);
System.out.println(a.equals(b));
Integer c = 129;
Integer d = 129;
System.out.println(c == d);
System.out.println(c.equals(d));
}
}
好了,绕远了,我们书归正文,继续讲枚举的equals与==,从上文Demo的运行结果看,枚举的两者运行结果一致。
但是name()与ordinal()
是啥?可能有人就有疑问了,因为枚举定义的时候,我们并未定义size.ordinal = 1,这个东西怎么来的?各位先不要着急,后面原理环节,我们再来揭晓这个答案,我们现在先知道,name是当前枚举定义的时候的值,ordinal 为当前枚举定义的时候的相对顺序。
3.原理
说到原理,其实不如说,我们是想探究枚举是怎么实现的?
接下来使用javac命令进行编译:生成class文件,然后再通过javap反编译
javac Size.java
javap Size.class
得到的源内容为:
public final class Size extends java.lang.Enum<Size> {
public static final Size SMALL;
public static final Size MEDIUM;
public static final Size LARGE;
public static Size[] values();
public static Size valueOf(java.lang.String);
static {};
}
可以看到,枚举类型实际上会被Java编译器转换为一个对应的类,这个类继承了Java API中的java.lang.Enum类。
我们看一下原生的这个类源码
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
/**
* 枚举常量的name
*/
private final String name;
/**
* 枚举常量的顺序
*/
private final int ordinal;
/**
* 重要的是此处的构造方法,从这里可以看出,枚举类默认有构造方法
*/
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
/**
* 返回枚举的name
*/
public String toString() {
return name;
}
/**
* 实现了hashcode与equals方法
*/
public final boolean equals(Object other) {
return this==other;
}
public final int hashCode() {
return super.hashCode();
}
/**
* 实现了compareTo方法,对比枚举常量
*/
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();
//可以看到此处Compareto对比的是ordinal字段
return self.ordinal - other.ordinal;
}
/**
* 根据输入的name,够着返回枚举常量
*/
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
}
Enum类有name和ordinal两个实例变量,在构造方法中需要传递,name()、toString()、ordinal()、compareTo()、equals()方法都是由Enum类根据其实例变量name和ordinal实现的。values和valueOf方法是编译器给每个枚举类型自动添加的。
所以结合上面的Enum父类,我们可以把当前Size的编译出来的类,大概梳理为以下代码:
public final class Size extends Enum<Size> {
public static final Size SMALL = new Size("SMALL",0);
public static final Size MEDIUM = new Size("MEDIUM",1);
public static final Size LARGE = new Size("LARGE",2);
private static Size[] VALUES = new Size[]{SMALL, MEDIUM, LARGE};
private Size(String name, int ordinal){
super(name, ordinal);
}
public static Size[] values(){
Size[] values = new Size[VALUES.length];
System.arraycopy(VALUES, 0, values, 0, VALUES.length);
return values;
}
public static Size valueOf(String name){
return Enum.valueOf(Size.class, name);
}
}
解释几点:
- Size是final的,不能被继承,Enum表示父类,是泛型写法;
- Size有一个私有的构造方法,接受name和ordinal,传递给父类,私有表示不能在外部创建新的实例;
- 三个枚举值实际上是三个静态变量,也是final的,不能被修改;
- values方法是编译器添加的,内部有一个values数组保持所有枚举值;
- valueOf方法调用的是父类的方法,额外传递了参数Size.class,表示类的类型信息,父类实际上是回过头来调用values方法,根据name对比得到对应的枚举值的。
一般枚举变量会被转换为对应的类变量,在switch语句中,枚举值会被转换为其对应的ordinal值。可以看出,枚举类型本质上也是类,但由于编译器自动做了很多事情,因此它的使用更为简洁、安全和方便。
4.总结
4.1 枚举的实际使用场景
上面讲了枚举的定义、基本使用以及原理,接下来,我们梳理一下枚举在实际开发环境中的一些使用场景。
需求栗子背景:客户端与服务端通信,服务端会返回各种错误码与错误状态信息,而这些错误码和错误状态,往往是一一对应的,不管是在客户端还是在服务端,你如何去实现呢?
4.1.1 静态常量
package com.test;
public class ResponseState {
public final static int STATUS_OK = 200;
public final static int STATUS_404 = 404;
public final static String STATUS_OK_STRING = "ok";
public final static String STATUS_404_STRING = "404,not found,客户端请求的资源,服务端无发现";
}
这样实现可以,但是大家发现没有,这种需求场景下,其实需求并没有完全实现
,因为你实现的方案里面,并没有吧状态与状态描述一一对应起来,那么必然后续代码开发的时候,会带来诸多不便,甚至对于不熟悉的开发人员调用的时候,还有可能引入缺陷,任意修改(比如后续有人新增了状态,但是复用了状态描述)。
4.1.2 静态Map
有了上面静态常量的实现方案,可能有人会说,既然没有实现一一对应的需求,那么就想到直接用静态map存储就行了,因为毕竟阅读过小编android源码分析系列文章的人都知道,android系统源码中,多处(例如:ServiceRegister中的系统服务binder注册,遗忘或者由兴趣的小伙伴,可以移步到小编android源码系列文章,复习一下)就是直接静态代码块,初始化的时候,map存储了数据结构信息,从而可以很快的一一查找。
package com.test;
import java.util.HashMap;
public class ResponseState {
public final static int STATUS_OK = 200;
public final static int STATUS_404 = 404;
public final static String STATUS_OK_STRING = "ok";
public final static String STATUS_404_STRING = "404,not found,客户端请求的资源,服务端无发现";
public static final HashMap<Integer, String> map = new HashMap<>();
static {
map.put(STATUS_OK, STATUS_OK_STRING);
map.put(STATUS_404, STATUS_404_STRING);
}
}
估计写到这里,有人很开心了,从代码上来说,的确需求都实现了,但是这时大家还得认真思考一下这个代码的鲁棒性是否满足
。
仔细思考,存在以下弊端,需要解决:
- 可阅读性不高:大家发现尽管添加了一个map,形成了一一对应关系,但是每次知道变量的值,你是不是还得点击查看一下
- 使用上不方便:使用上,外面每处需要调用变量的地方,本来我们最习惯的是自己常量的调用,现在为了一一对应,需要调用map
- 占用内存太高
- 可维护性不高:后续开发人员,如果新增一个状态码、状态描述,需要明确定义清楚值,不要给重复了,而且需要修改map
4.1.3 枚举实现
话不多说,我们直接上代码
/**
* 服务端返回的状态定义
*
* @author itbird
*/
public enum ResponseState {
STATUS_OK(200, "ok"),
STATUS_404(404, "404,not found,客户端请求的资源,服务端无发现");
int status;
String msg;
ResponseState(int status, String msg) {
this.status = status;
this.msg = msg;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
大家是不是感觉清晰很多了,而且维护、调用上也特别简单,是不是呢?
我们对于上面的四个弊端,一一对应来看一下:
- 可阅读性不高:这个一目了然,明显枚举可阅读性高一些
- 使用上不方便:使用上,也是一目了然,枚举既达到了一一对应的效果,也可以像静态常量一下使用
- 可维护性不高:一目了然,这个可维护性,相对于前面两种,肯定更好
这是肯定有人问了,小编你不要骗人,还有一个,弊端里面还有一个内存占用呢?
好吧,既然被机智的你发现了,我也不逃避了,就这点,我还是说明一下吧。
不过为了简单一点(实际上应该找个方案查看实际内存这块占用了多大,去做对比),我们直接对比两种方案实现的class文件的大小吧,编译后的枚举class文件大小为1471字节,静态常量class文件大小为400字节。
经过对比枚举类型文件大小更大一些。
枚举的实现原理就是定义一个类,然后实例化几个由final修饰的这个类的对象,每个实例都带有自己的元信息。而常量相比之下,没有这一层封装,只占用最基本的内存,包括引用,和它的值本身,要简单轻巧很多。如果值可以使用基本类型而不是包装类型,那更不用说了。 不过话又说回来,通常情况下我们没必要在意这种区别。如果用枚举可读性、可扩展性更好,用就是了,枚举占那点内存,沧海一粟。在性能与代码维护性之间,除个别情况,优先选后者。高级编程语言的诞生本身就是硬件提升的背景下,牺牲某些性能来降低开发门槛,提高开发效率的,相对于微小的性能损耗,人力成本更值钱
4.2 枚举的优缺点
优点
- 定义枚举的语法更为简洁。
- 枚举更为安全。一个枚举类型的变量,它的值要么为null,要么为枚举值之一,不可能为其他值,但使用整型变量,它的值就没有办法强制,值可能就是无效的。
- 枚举类型自带很多便利方法(如values、valueOf、toString等),易于使用。
缺点
- 不可继承,无法扩展,但是一般常量在构件时就定义完毕了,不需要扩展。
Demo运行结果
equals与==的小知识点Demo的运行结果截图,各位猜对了吗?猜对并且知道所以然的话,那我恭喜您,之前的文章没有白看,Java基础这块掌握还不错,如果猜错或者只是猜对,不知道所以然,那我建议,赶快读一下文章开头的两本Java圣典吧。