【Java面试】枚举从使用到原理
最近重新阅读《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面试】枚举从使用到原理相关推荐
- Java面试热点问题,synchronized原理剖析与优化
前言 观看笔记:https://www.bilibili.com/video/BV1aJ411V763?from=search&seid=6293835933701781647 观看了这个视频 ...
- java枚举类型原理_Java枚举类接口实例原理解析
这篇文章主要介绍了Java枚举类接口实例原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 枚举类可以实现一个或多个接口.与普通类实现接口完全一 ...
- 面试大厂不看这两份Java面试核心知识点原理篇+框架篇,有个屁用?食屎啦泥?
前言 面试在即,Java知识点很凌乱? 别急,有本套书在呢! 除了原理,还有框架! ★ 精细讲解JVM原理.Java基础.并发编程.数据结构和算法.网络与负载均衡 ★ 深入挖掘数据库与分布式事务.分布 ...
- 今年Java面试必问的这些技术面,看完这一篇你就懂了
说明 Java生鲜电商平台中由于采用了微服务架构进行业务的处理,买家,卖家,配送,销售,供应商等进行服务化,但是不可避免存在分布式事务的问题. 业界有很多的解决方案,对此我相信大家都百度一下子就有很多 ...
- java面试笔试大汇总(一)
java面试笔试题大汇总5 JAVA相关基础知识 1.面向对象的特征有哪些方面 1.抽象:2.继承:3.封装:4. 多态性: 2.String是最基本的数据类型吗? 基本数据类型包括byte.int. ...
- Java:由浅入深揭开 AOP 实现原理
点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:干掉 Navicat:这个 IDEA 的兄弟真香!个人原创100W+访问量博客:点击前往,查看更多 作者:马佩 ...
- java如何创造一个整数的类_【技术干货】Java 面试宝典:Java 基础部分(1)
原标题:[技术干货]Java 面试宝典:Java 基础部分(1) Java基础部分: 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语法,集合的语法,io 的 ...
- java面试和笔试大全
2.String是最基本的数据类型吗? 基本数据类型包括byte.int.char.long.float.double.boolean和short. java.lang.String类是final类型 ...
- 高级 Java 面试通关知识点整理
转载自 高级 Java 面试通关知识点整理 1.常用设计模式 单例模式:懒汉式.饿汉式.双重校验锁.静态加载,内部类加载.枚举类加载.保证一个类仅有一个实例,并提供一个访问它的全局访问点. 代理模式: ...
- 最全Java面试208题,涵盖大厂必考范围!强烈建议收藏~
这些题目是去百度.小米.乐视.美团.58.猎豹.360.新浪.搜狐等一线互联网公司面试被问到的题目,熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率. 一.java基础面试知识点 java中= ...
最新文章
- Android SlideAndDragListView,一个可排序可滑动item的ListView
- Java中Comparable和Comparator区别小结
- how is Fiori launchpad host name and port number determine
- 贝叶斯 定理_贝叶斯定理实际上是一个直观的分数
- 博客,文字的卡拉OK版
- 网联下发42号文督促生产测试 银行代扣通道都将关闭 协议支付
- 华为 项目管理10大模板 【Word版 (可直接套用)】
- python定时任务_Python 定时任务的实现方式
- 【OpenGL】实例渲染示例——草地渲染
- 基于Modelica的船用大功率电推进系统建模仿真
- 打印DPI如何与计算机DPI一致,像素英寸与dpi的那些事儿
- 谢烟客---------Linux之权限
- 共享租赁汽车,必将重新设计中国汽车产业链游戏规则
- C# 中 volatile 关键字的解读
- Visual Paradigm使用技能
- 虚拟内存、虚拟地址-页-页号、物理地址-页框-页框号
- 对PHM铣刀磨损数据进行分析
- HTML相对路径的写法
- python3 安装urllib3
- 华为 android 5.0系统下载地址,华为Mate8 EMUI5.0系统专用官方原版recovery下载和刷入...