Java 枚举

问:Java 枚举类比较用 == 还是 equals,有什么区别?

答:java 枚举值比较用 == 和 equals 方法没啥区别,两个随便用都是一样的效果。因为枚举 Enum 类的 equals 方法默认实现就是通过 == 来比较的;类似的 Enum 的 compareTo 方法比较的是 Enum 的 ordinal 顺序大小;类似的还有 Enum 的 name 方法和 toString 方法一样都返回的是 Enum 的 name 值。

问:简单谈谈你理解的 Java 枚举本质原理?

答:java 枚举的本质原理是通过普通类来实现的,只是编译器为我们进行了加工处理,每个枚举类型编译后的字节码实质都继承自 java.lang.Enum 的枚举类型同名普通类,而每个枚举常量实质是一个枚举类型同名普通类的静态常量对象,所有枚举常量都通过静态代码块进行初始化实例赋值(由于是静态块,所以在类加载期间就初始化了)。为了加深理解可以通过下面的例子说明:

        public enum Status {
            START("a"), RUNNING("b"), STOP();
            public String name;
            private Status() {
                this("def");
            }

            private Status(String name) {
                this.name = name;
            }
        }

我们对如上枚举类型进行 javac 编译后通过 javap -v Status.class 可以查看其编译后字节码如下:


上面例子已经解释的很清楚了,记住枚举的本质是编译器处理成了类,枚举值为类的静态常量属性,其属性在类加载时的静态代码块中被初始化实例赋值。枚举可以有修饰符不大于默认修饰符的构造方法(修饰符可为 private,不可为 public 等)等,枚举只是一种语法糖,被编译器生成了最终的类而已。

所以枚举类型其实和我们自己使用 Java 普通类实现的类似,如下:

        public class Status {
            public static final Status START;
            public static final Status RUNNING;
            public static final Status STOP;
            public String name;
            static {
                START = new Status("a");
                RUNNING = new Status("b");
                STOP = new Status();
            }

            private Status() {
                this("def");
            }

            private Status(String name) {
                this.name = name;
            }

        }

所以从某种意义上可以说 JDK 1.5 后引入的枚举类型是上面枚举常量类的代码封装而已。

问:Java 枚举类与常量的区别有哪些,有啥优缺点?

答:枚举相对于常量类来说定义更简单,其不需要定义枚举值,而常量类中的每个常量必须要手动添加值。枚举作为参数使用时可以在编译时避免弱类型错误,而常量类中的常量作为参数使用时在编译时无法避免弱类型错误(譬如常量类型为 int,参数传递一个常量类中没定义的 int 值)。枚举自动具备内置方法(如 values 方法可以获得所有值的集合来遍历,ordinal 方法可以获得排序值,compareTo 方法可以基于 ordinal 比较),而常量类默认不具备这些方法。枚举的缺点就是不能被继承(编译后生成的类是 final class 的),也不能通过 extends 继承其他类(枚举类编译后实质就是继承了 Enum 类,Java 是单继承机制),但是定义的枚举类可以通过 implements 实现其他接口,枚举值定义完毕后除非修改重构,否则无法做扩展,而常量类可以随意继承。

问:Java 枚举类可以继承其他类(或实现其他接口)或者被其他类继承吗,为什么?

答:枚举类可以实现其他接口但不能继承其他类,因为所有枚举类在编译后的字节码中都继承自 java.lang.Enum(由编译器添加),而 Java 不支持多继承,所以枚举类不可以继承其他类。
枚举类不可以被继承,因为所有枚举类在编译后的字节码中都是继承自 java.lang.Enum(由编译器添加)的 final class 类,final 的类是不允许被派生继承的。(不清楚的可以查看前一篇历史推送枚举原理题)

问:Java switch 为什么能使用枚举类型?

答:Java 1.7 之前 switch 参数可用类型为 short、byte、int、char,枚举类型之所以能使用其实是编译器层面实现的,编译器会将枚举 switch 转换为类似 switch(s.ordinal()) { case Status.START.ordinal() } 形式,所以实质还是 int 参数类型,感兴趣的可以自己写个使用枚举的 switch 代码然后通过 javap -v 去看下字节码就明白了。

此问题延伸出一个新问题就是 JDK 1.7 中 switch 支持 String 类型参数的原理是什么?

实际上 JDK1.7 的 switch 支持 String 也是在编译器层面实现的,在 Java 虚拟机和字节代码层面上依然只支持在 switch 语句中使用与整数类型兼容的类型。我们在 switch 中使用的 String 类型在编译的过程中会将字符串类型转换成与整数类型兼容的格式(譬如基于字符串常量的哈希码等),不同的 Java 编译器可能采用不同的方式和优化策略来完成这个转换。

问:Java 枚举会比静态常量更消耗内存吗?

答:会更消耗,一般场景下不仅编译后的字节码会比静态常量多,而且运行时也会比静态常量需要更多的内存,不过这个多取决于场景和枚举的规模等等,不能明确的定论多多少(一般都至少翻倍以上),此外更不能因为多就一刀切的认为静态常量应该优于枚举使用,枚举有自己的特性和场景,优化也不能过度。每个枚举类中的具体枚举类型都是对应类中的一个静态常量,该常量在 static 块中被初始实例化,此外枚举类还有自己的一些特有方法,而静态常量实质却很简单,所以从对象占用内存大小方面来计算肯定是枚举类比静态常量更加占体积和消耗运行时内存,至于具体怎么算其实很简单,大家可以自己下去搜一下 java 对象占用内存大小即可了解更多,搞清楚特定场合下具体大多少没有什么实际意义,搞清楚为什么大和怎么算出来的本质原因即可。

问:Java 枚举是如何保证线程安全的?

答:因为 Java 类加载与初始化是 JVM 保证线程安全,而 Java enum 枚举在编译器编译后的字节码实质是一个 final 类,每个枚举类型是这个 final 类中的一个静态常量属性,其属性初始化是在该 final 类的 static 块中进行,而 static 的常量属性和代码块都是在类加载时初始化完成的,所以自然就是 JVM 保证了并发安全。

问:不使用 synchronized 和 lock 如何创建一个线程安全的单例?

答:这是一个很 open 的题目,我们平时提到单例并发都是用锁机制,实际抛开锁机制也有几种实现方式可以保证创建单例的并发安全,而且各具特色。

//通过枚举实现单例模式 
/public enum Singleton {
            INSTANCE;

            public void func() {
            }
        }
//通过饿汉模式实现单例 
        public class Singleton {
            private static Singleton instance = new Singleton();

            private Singleton() {
            }

            public static Singleton getInstance() {
                return instance;
            }
        }
//通过静态内部类模式实现单例
        public class Singleton {
            private static class SingletonHolder {
                private static final Singleton INSTANCE = new Singleton();
            }

            private Singleton() {
            }

            public static final Singleton getInstance() {
                return SingletonHolder.INSTANCE;
            }
        } 
//通过 CAS(AtomicReference)实现单例模式 
        public class Singleton {
            private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();

            private Singleton() {
            }

            public static Singleton getInstance() {
                for (; ; ) {
                    Singleton singleton = INSTANCE.get();
                    if (null != singleton) {
                        return singleton;
                    }
                    singleton = new Singleton();
                    if (INSTANCE.compareAndSet(null, singleton)) {
                        return singleton;
                    }
                }
            }

可以看到,上面四种方式都可以不使用 synchronized 或者 lock 来保证了单例创建的并发安全。前面三种都是借助了 JVM 的 ClassLoader 类加载初始化保证并发安全的机制(至于 JVM 底层其实也是使用了 synchronized 或者 lock 的机制),而对于最后一种通过 CAS 机制保证了并发安全(CAS 就是一种非阻塞乐观锁机制,是一种基于忙等待的算法,依赖底层硬件实现,相对于锁其没有线程切换和阻塞的额外消耗,但是如果忙等待一直执行不成功的死循环会对 CPU 造成较大的开销),最后一种才是真正的无锁实现。

问:为什么有人说在一些场景下通过枚举实现的单例是最好的方式,原因是什么?

答:其实这个题目算是一箭双雕,既考察了 Java 枚举的实质特性又考察了单例模式的一些弊端问题。除了枚举实现的单例模式以外的其他实现方式都有一个比较大的问题是一旦实现了 Serializable 接口后就不再是单例了,因为每次调用 readObject() 方法返回的都是一个新创建出来的对象(当然可以通过使用 readResolve() 方法来避免,但是终归麻烦),而 Java 规范中保证了每一个枚举类型及其定义的枚举变量在 JVM 中都是唯一的,在枚举类型的序列化和反序列化上 Java 做了特殊处理,序列化时 Java 仅仅是将枚举对象的 name 属性输出到结果中,反序列化时则是通过 java.lang.Enum 的 valueOf 方法来根据名字查找枚举对象,同时禁用了 writeObject、readObject、readObjectNoData、writeReplace 和 readResolve 等方法。

这个问题也暴露出另一个新问题,Java 枚举序列化有哪些坑?

如果我们枚举被序列化、本地持久化了,那我们就不能删除原来枚举类型中定义的任何枚举对象,否则程序在运行过程中反序列化时 JVM 就会找不到与某个名字对应的枚举对象了,所以我们要尽量避免多枚举对象序列化的使用(当然了,枚举实现的单例枚举对象一般都不会增删改,所以不存在问题)。

问:Java 迭代器和枚举器的区别是什么?

答:主要区别如下。

  1. Enumeration<E> 枚举器接口是 JDK 1.0 提供的,适用于传统类,而 Iterator<E> 迭代器接口是 JDK 1.2 提供的,适用于 Collections。

  2. Enumeration 只有两个方法接口,我们只能读取集合的数据而不能对数据进行修改,而 Iterator 有三个方法接口,除了能读取集合的数据外也能对数据进行删除操作。

  3. Enumeration 不支持 fail-fast 机制,而 Iterator 支持 fail-fast 机制(一种错误检测机制,当多线程对集合进行结构上的改变的操作时就有可能会产生 fail-fast 机制,譬如 ConcurrentModificationException 异常)。

总归现在尽量使用 Iterator 迭代器而不是 Enumeration 枚举器。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 概念 enum 的全称为 enumeration, 是 JDK 1.5 中引入的新特性。 在Java中,被 en...
    静默虚空阅读 8,354评论 1 18
  • 一 Java 枚举7常见种用法DK1.5引入了新的类型——枚举。在 Java 中它虽然算个“小”功能,却给我的开发...
    欢乐时光欢乐你我阅读 804评论 0 6
  • 枚举的特性 枚举的特性,归结起来就是一句话: 除了不能继承,基本上可以将 enum 看做一个常规的类。 但是这句话...
    java部落阅读 305评论 0 2
  • 枚举可以添加方法 在概念章节提到了,枚举值默认为从0开始的有序数值 。那么问题来了:如何为枚举显示的赋值。 Jav...
    java部落阅读 326评论 0 0
  • 枚举可以实现接口 enum 可以像一般类一样实现接口。 同样是实现上一节中的错误码枚举类,通过实现接口,可以约束它...
    java部落阅读 144评论 0 1