1. 优享JAVA首页
  2. 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();
   
 private  Status()  {

 this("def");

 }
 
 private  Status(String name)  {

 this.name = name;

 }
 
 public  String name;

}

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

......

public  final  class  Status  extends java.lang.Enum<Status>

 ......

{

 //枚举类型值都成了Status类型类的静态常量成员属性

 public  static  final  Status START;

 public  static  final  Status RUNNING;

 public  static  final  Status STOP;

 public java.lang.String name;


 public  static  Status[] values();

 ......

 public  static  Status valueOf(java.lang.String);

......

 //静态代码块,类加载时执行

 static  {};

 flags: ACC_STATIC

 Code:

 stack=5, locals=0, args_size=0

 //创建一个Status对象通过参数为字符串常量"a"的构造方法初始化赋值给START

  0:  new  #4 // class Status

  3: dup

 4: ldc #10 // String START

 6: iconst_0

  7: ldc #11 // String a

  9: invokespecial #7 // Method "":(Ljava/lang/String;ILjava/lang/String;)V

 12: putstatic #12 // Field START:LStatus;

 ......

}

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

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

public  class  Status  {

 public  static  final  Status START;

 public  static  final  Status RUNNING;

 public  static  final  Status STOP;


 static  {

 START =  new  Status("a");

 RUNNING =  new  Status("b");

 STOP =  new  Status();

 }


 private  Status()  {

 this("def");

 }


 private  Status(String name)  {

 this.name = name;

 }

 public  String 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 保证了并发安全。(不清楚 enum 编译后为啥是静态常量的可以查看历史推送了解更多)

不使用 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 我们后面并发相关每日一题会再详细推送讨论的,这里先记住 CAS 就是一种非阻塞乐观锁机制,是一种基于忙等待的算法,依赖底层硬件实现,相对于锁其没有线程切换和阻塞的额外消耗,但是如果忙等待一直执行不成功的死循环会对 CPU 造成较大的开销),最后一种才是真正的无锁实现。

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

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

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

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

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

主要区别如下。

  • Enumeration 枚举器接口是 JDK 1.0 提供的,适用于传统类,而 Iterator 迭代器接口是 JDK 1.2 提供的,适用于 Collections。
  • Enumeration 只有两个方法接口,我们只能读取集合的数据而不能对数据进行修改,而 Iterator 有三个方法接口,除了能读取集合的数据外也能对数据进行删除操作。
  • Enumeration 不支持 fail-fast 机制,而 Iterator 支持 fail-fast 机制(一种错误检测机制,当多线程对集合进行结构上的改变的操作时就有可能会产生 fail-fast 机制,譬如 ConcurrentModificationException 异常)。

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

原创文章,作者:Craig,如若转载,请注明出处:https://www.goodlymoon.com/archives/695.html

发表评论

电子邮件地址不会被公开。 必填项已用*标注

QR code