【Java面试】枚举从使用到原理

简介: 【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() && // 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圣典吧。
在这里插入图片描述

目录
相关文章
|
19天前
|
消息中间件 存储 缓存
大厂面试高频:Kafka 工作原理 ( 详细图解 )
本文详细解析了 Kafka 的核心架构和实现原理,消息中间件是亿级互联网架构的基石,大厂面试高频,非常重要,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:Kafka 工作原理 ( 详细图解 )
|
16天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
45 2
|
5天前
|
Java 程序员
Java社招面试题:& 和 && 的区别,HR的套路险些让我翻车!
小米,29岁程序员,分享了一次面试经历,详细解析了Java中&和&&的区别及应用场景,展示了扎实的基础知识和良好的应变能力,最终成功获得Offer。
29 14
|
21天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
21天前
|
存储 安全 Java
面试高频:Synchronized 原理,建议收藏备用 !
本文详解Synchronized原理,包括其作用、使用方式、底层实现及锁升级机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
面试高频:Synchronized 原理,建议收藏备用 !
|
21天前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
23天前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
22天前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
46 4
|
SQL 缓存 安全
Java高频面试题目
面试时面试官最常问的问题总结归纳!
146 0
JAVA高频面试题目集锦(6)
JAVA高频面试题目集锦(6)
142 0
JAVA高频面试题目集锦(6)