最近重新阅读《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() && // optimizationself.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圣典吧。

【Java面试】枚举从使用到原理相关推荐

  1. Java面试热点问题,synchronized原理剖析与优化

    前言 观看笔记:https://www.bilibili.com/video/BV1aJ411V763?from=search&seid=6293835933701781647 观看了这个视频 ...

  2. java枚举类型原理_Java枚举类接口实例原理解析

    这篇文章主要介绍了Java枚举类接口实例原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 枚举类可以实现一个或多个接口.与普通类实现接口完全一 ...

  3. 面试大厂不看这两份Java面试核心知识点原理篇+框架篇,有个屁用?食屎啦泥?

    前言 面试在即,Java知识点很凌乱? 别急,有本套书在呢! 除了原理,还有框架! ★ 精细讲解JVM原理.Java基础.并发编程.数据结构和算法.网络与负载均衡 ★ 深入挖掘数据库与分布式事务.分布 ...

  4. 今年Java面试必问的这些技术面,看完这一篇你就懂了

    说明 Java生鲜电商平台中由于采用了微服务架构进行业务的处理,买家,卖家,配送,销售,供应商等进行服务化,但是不可避免存在分布式事务的问题. 业界有很多的解决方案,对此我相信大家都百度一下子就有很多 ...

  5. java面试笔试大汇总(一)

    java面试笔试题大汇总5 JAVA相关基础知识 1.面向对象的特征有哪些方面 1.抽象:2.继承:3.封装:4. 多态性: 2.String是最基本的数据类型吗? 基本数据类型包括byte.int. ...

  6. Java:由浅入深揭开 AOP 实现原理

    点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:干掉 Navicat:这个 IDEA 的兄弟真香!个人原创100W+访问量博客:点击前往,查看更多 作者:马佩 ...

  7. java如何创造一个整数的类_【技术干货】Java 面试宝典:Java 基础部分(1)

    原标题:[技术干货]Java 面试宝典:Java 基础部分(1) Java基础部分: 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语法,集合的语法,io 的 ...

  8. java面试和笔试大全

    2.String是最基本的数据类型吗? 基本数据类型包括byte.int.char.long.float.double.boolean和short. java.lang.String类是final类型 ...

  9. 高级 Java 面试通关知识点整理

    转载自 高级 Java 面试通关知识点整理 1.常用设计模式 单例模式:懒汉式.饿汉式.双重校验锁.静态加载,内部类加载.枚举类加载.保证一个类仅有一个实例,并提供一个访问它的全局访问点. 代理模式: ...

  10. 最全Java面试208题,涵盖大厂必考范围!强烈建议收藏~

    这些题目是去百度.小米.乐视.美团.58.猎豹.360.新浪.搜狐等一线互联网公司面试被问到的题目,熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率. 一.java基础面试知识点 java中= ...

最新文章

  1. Android SlideAndDragListView,一个可排序可滑动item的ListView
  2. Java中Comparable和Comparator区别小结
  3. how is Fiori launchpad host name and port number determine
  4. 贝叶斯 定理_贝叶斯定理实际上是一个直观的分数
  5. 博客,文字的卡拉OK版
  6. 网联下发42号文督促生产测试 银行代扣通道都将关闭 协议支付
  7. 华为 项目管理10大模板 【Word版 (可直接套用)】
  8. python定时任务_Python 定时任务的实现方式
  9. 【OpenGL】实例渲染示例——草地渲染
  10. 基于Modelica的船用大功率电推进系统建模仿真
  11. 打印DPI如何与计算机DPI一致,像素英寸与dpi的那些事儿
  12. 谢烟客---------Linux之权限
  13. 共享租赁汽车,必将重新设计中国汽车产业链游戏规则
  14. C# 中 volatile 关键字的解读
  15. Visual Paradigm使用技能
  16. 虚拟内存、虚拟地址-页-页号、物理地址-页框-页框号
  17. 对PHM铣刀磨损数据进行分析
  18. HTML相对路径的写法
  19. python3 安装urllib3
  20. 华为 android 5.0系统下载地址,华为Mate8 EMUI5.0系统专用官方原版recovery下载和刷入...

热门文章

  1. 音频视频自动提取字幕(extract subtitle from audios and vedios)
  2. 《赵成的运维体系管理课》学习笔记(5)——故障管理
  3. CREATE VIEW
  4. WebGIS中的坐标系
  5. 程序员是不是“后浪”?
  6. 文献阅读 | Tracing the ancestry of modern bread wheats
  7. PowerBI账户免费注册
  8. Angular4 - 共享模块
  9. c语言求偶数的积,动物行为学1
  10. SEO分类:白帽SEO-黑帽SEO-灰帽SEO