前言

春节来临之际,祝大家新年快乐哈。整理了Java枚举的相关知识,算是比较基础的,希望大家一起学习进步。

一、枚举类型是什么?

JDK5引入了一种新特性,关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这就是枚举类型。

一个枚举的简单例子

enum
 
SeasonEnum
 
{
    SPRING
,
SUMMER
,
FALL
,
WINTER
;
}

二、 枚举类的常用方法

Enum常用方法有以下几种:

  • name(); 返回enum实例声明时的名字。
  • ordinal(); 返回一个int值,表示enum实例在声明的次序。
  • equals(); 返回布尔值,enum实例判断相等
  • compareTo() 比较enum实例与指定对象的顺序
  • values(); 返回enum实例的数组
  • valueOf(String name) 由名称获取枚举类中定义的常量 直接看例子吧:
enum
 
Shrubbery
 
{
    GROUND
,
CRAWLING
,
 HANGING
}
public
 
class
 
EnumClassTest
 
{
    
public
 
static
 
void
 main
(
String
[]
 args
)
 
{
        
//values 返回enum实例的数组
        
for
 
(
Shrubbery
 temp 
:
 
Shrubbery
.
values
())
 
{
            
// name 返回实例enum声明的名字
            
System
.
out
.
println
(
temp
.
name
()
 
+
 
" ordinal is "
 
+
 temp
.
ordinal
()
 
+
 
" ,equal result is "
 
+
                    
Shrubbery
.
CRAWLING
.
equals
(
temp
)
 
+
 
",compare result is "
 
+
 
Shrubbery
.
CRAWLING
.
compareTo
(
temp
));
        
}
        
//由名称获取枚举类中定义的常量值
        
System
.
out
.
println
(
Shrubbery
.
valueOf
(
"CRAWLING"
));
    
}
}

运行结果:

GROUND ordinal 
is
 
0
 
,
equal result 
is
 
false
,
compare result 
is
 
1
CRAWLING ordinal 
is
 
1
 
,
equal result 
is
 
true
,
compare result 
is
 
0
HANGING ordinal 
is
 
2
 
,
equal result 
is
 
false
,
compare result 
is
 
-
1
CRAWLING

三、枚举类的真面目

枚举类型到底是什么类呢?我们新建一个简单枚举例子,看看它的庐山真面目。如下:

public
 
enum
 
Shrubbery
 
{
    GROUND
,
CRAWLING
,
 HANGING
}

使用javac编译上面的枚举类,可得Shrubbery.class文件。

javac 
Shrubbery
.
java

再用javap命令,反编译得到字节码文件。如:执行 javapShrubbery.class可到以下字节码文件。

Compiled
 
from
 
"Shrubbery.java"
public
 
final
 
class
 enumtest
.
Shrubbery
 
extends
 java
.
lang
.
Enum
<
enumtest
.
Shrubbery
>
 
{
  
public
 
static
 
final
 enumtest
.
Shrubbery
 GROUND
;
  
public
 
static
 
final
 enumtest
.
Shrubbery
 CRAWLING
;
  
public
 
static
 
final
 enumtest
.
Shrubbery
 HANGING
;
  
public
 
static
 enumtest
.
Shrubbery
[]
 values
();
  
public
 
static
 enumtest
.
Shrubbery
 valueOf
(
java
.
lang
.
String
);
  
static
 
{};
}

从字节码文件可以发现:

  • Shrubbery枚举变成了一个final修饰的类,也就是说,它不能被继承啦。
  • Shrubbery是java.lang.Enum的子类。
  • Shrubbery定义的枚举值都是public static final修饰的,即都是静态常量。 为了看得更仔细,javap反编译加多个参数-c,执行如下命令:
javap 
-
c 
Shrubbery
.
class

静态代码块的字节码文件如下:

  
static
 
{};
    
Code
:
       
0
:
 
new
           
#4                  // class enumtest/Shrubbery
       
3
:
 dup
       
4
:
 ldc           
#7                  // String GROUND
       
6
:
 iconst_0
       
7
:
 invokespecial 
#8                  // Method "<init>":(Ljava/lang/String;I)V
      
10
:
 putstatic     
#9                  // Field GROUND:Lenumtest/Shrubbery;
      
13
:
 
new
           
#4                  // class enumtest/Shrubbery
      
16
:
 dup
      
17
:
 ldc           
#10                 // String CRAWLING
      
19
:
 iconst_1
      
20
:
 invokespecial 
#8                  // Method "<init>":(Ljava/lang/String;I)V
      
23
:
 putstatic     
#11                 // Field CRAWLING:Lenumtest/Shrubbery;
      
26
:
 
new
           
#4                  // class enumtest/Shrubbery
      
29
:
 dup
      
30
:
 ldc           
#12                 // String HANGING
      
32
:
 iconst_2
      
33
:
 invokespecial 
#8                  // Method "<init>":(Ljava/lang/String;I)V
      
36
:
 putstatic     
#13                 // Field HANGING:Lenumtest/Shrubbery;
      
39
:
 iconst_3
      
40
:
 anewarray     
#4                  // class enumtest/Shrubbery
      
43
:
 dup
      
44
:
 iconst_0
      
45
:
 getstatic     
#9                  // Field GROUND:Lenumtest/Shrubbery;
      
48
:
 aastore
      
49
:
 dup
      
50
:
 iconst_1
      
51
:
 getstatic     
#11                 // Field CRAWLING:Lenumtest/Shrubbery;
      
54
:
 aastore
      
55
:
 dup
      
56
:
 iconst_2
      
57
:
 getstatic     
#13                 // Field HANGING:Lenumtest/Shrubbery;
      
60
:
 aastore
      
61
:
 putstatic     
#1                  // Field $VALUES:[Lenumtest/Shrubbery;
      
64
:
 
return
}
  • 0-39行实例化了Shrubbery枚举类的GROUND,CRAWLING, HANGING;
  • 40-64为创建Shrubbery[]数组$VALUES,并将上面的三个实例化对象放入数组的操作。
  • 因此,枚举类方法values()返回enum枚举实例的数组是不是豁然开朗啦。

四、枚举类的优点

枚举类有什么优点呢?就是我们为什么要选择使用枚举类呢?因为它可以增强代码的可读性,可维护性,同时,它也具有安全性。

枚举类可以增强可读性、可维护性

假设现在有这样的业务场景:订单完成后,通知买家评论。很容易有以下代码:

//订单已完成
if
(
3
==
orderStatus
){
//do something
}

很显然,这段代码出现了魔法数,如果你没写注释,谁知道3表示订单什么状态呢,不仅阅读起来比较困难,维护起来也很蛋疼?如果使用枚举类呢,如下:

public
 
enum
 
OrderStatusEnum
 
{
    UNPAID
(
0
,
 
"未付款"
),
 PAID
(
1
,
 
"已付款"
),
 SEND
(
2
,
 
"已发货"
),
 FINISH
(
3
,
 
"已完成"
),;

    
private
 
int
 index
;

    
private
 
String
 desc
;

    
public
 
int
 getIndex
()
 
{
        
return
 index
;
    
}

    
public
 
String
 getDesc
()
 
{
        
return
 desc
;
    
}

    
OrderStatusEnum
(
int
 index
,
 
String
 desc
)
 
{
        
this
.
index 
=
 index
;
        
this
.
desc 
=
 desc
;
    
}
}

 
//订单已完成
 
if
(
OrderStatusEnum
.
FINISH
.
getIndex
()==
orderStatus
){
  
//do something
 
}

可见,枚举类让这段代码可读性更强,也比较好维护,后面加个新的订单状态,直接添加多一种枚举状态就可以了。有些朋友认为, publicstaticfinalint这种静态常量也可以实现该功能呀,如下:

public
 
class
 
OrderStatus
 
{
    
//未付款
    
public
 
static
 
final
 
int
 UNPAID 
=
 
0
;
    
public
 
static
 
final
 
int
 PAID 
=
 
1
;
    
public
 
static
 
final
 
int
 SENDED 
=
 
2
;
    
public
 
static
 
final
 
int
 FINISH 
=
 
3
;

}

 
//订单已完成
 
if
(
OrderStatus
.
FINISH
==
orderStatus
){
     
//do something
 
}

当然,静态常量这种方式实现,可读性是没有任何问题的,日常工作中代码这样写也无可厚非。但是,定义int值相同的变量,容易混淆,如你定义 PAID和 SENDED状态都是2,编译器是不会报错的。

因此,枚举类第一个优点就是可读性,可维护性都不错,所以推荐。

枚举类安全性

除了可读性、可维护性外,枚举类还有个巨大的优点,就是安全性。

从上一节枚举类字节码分析,我们知道:

  • 一个枚举类是被final关键字修饰的,不能被继承。
  • 并且它的变量都是public static final修饰的,都是静态变量。

当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。

五、枚举的常见用法

enum组织常量

在JDK5之前,常量定义都是这样,先定义一个类或者接口,属性类型都是public static final...,有了枚举之后,可以把常量组织到枚举类了,如下:

enum
 
SeasonEnum
 
{
    SPRING
,
SUMMER
,
FALL
,
WINTER
,;
}

enum与switch 环环相扣

一般来说,switch-case中只能使用整数值,但是枚举实例天生就具备整数值的次序,因此,在switch语句中是可以使用enum的,如下:

enum
 
OrderStatusEnum
 
{
   UNPAID
,
 PAID
,
 SEND
,
 FINISH
}
public
 
class
 
OrderStatusTest
 
{
    
public
 
static
 
void
 main
(
String
[]
 args
)
 
{
        changeByOrderStatus
(
OrderStatusEnum
.
FINISH
);
    
}

    
static
 
void
 changeByOrderStatus
(
OrderStatusEnum
 orderStatusEnum
)
 
{
        
switch
 
(
orderStatusEnum
)
 
{
            
case
 UNPAID
:
                
System
.
out
.
println
(
"老板,你下单了,赶紧付钱吧"
);
                
break
;
            
case
 PAID
:
                
System
.
out
.
println
(
"我已经付钱啦"
);
                
break
;
            
case
 SENDED
:
                
System
.
out
.
println
(
"已发货"
);
                
break
;
            
case
 FINISH
:
                
System
.
out
.
println
(
"订单完成啦"
);
                
break
;
        
}
    
}
}

在日常开发中,enum与switch一起使用,会让你的代码可读性更好哦。

向枚举中添加新的方法

可以向枚举类添加新方法的,如get方法,普通方法等,以下是日常工作最常用的一种枚举写法:

public
 
enum
 
OrderStatusEnum
 
{
    UNPAID
(
0
,
 
"未付款"
),
 PAID
(
1
,
 
"已付款"
),
 SENDED
(
2
,
 
"已发货"
),
 FINISH
(
3
,
 
"已完成"
),;

    
//成员变量
    
private
 
int
 index
;
    
private
 
String
 desc
;

    
//get方法
    
public
 
int
 getIndex
()
 
{
        
return
 index
;
    
}

    
public
 
String
 getDesc
()
 
{
        
return
 desc
;
    
}

    
//构造器方法
     
OrderStatusEnum
(
int
 index
,
 
String
 desc
)
 
{
        
this
.
index 
=
 index
;
        
this
.
desc 
=
 desc
;
    
}

    
//普通方法
    
public
 
static
 
OrderStatusEnum
 of
(
int
 index
){
        
for
 
(
OrderStatusEnum
 temp 
:
 values
())
 
{
            
if
 
(
temp
.
getIndex
()
 
==
 index
)
 
{
                
return
 temp
;
            
}
        
}
        
return
 
null
;
    
}
}

枚举实现接口

所有枚举类都继承于java.lang.Enum,所以枚举不能再继承其他类了。但是枚举可以实现接口呀,这给枚举增添了不少色彩。如下:

public
 
interface
 
ISeasonBehaviour
 
{

    
void
 showSeasonBeauty
();

    
String
 getSeasonName
();
}
public
 
enum
 
SeasonEnum
 
implements
 
ISeasonBehaviour
 
{
    SPRING
(
1
,
"春天"
),
SUMMER
(
2
,
"夏天"
),
FALL
(
3
,
"秋天"
),
WINTER
(
4
,
"冬天"
),
    
;

    
private
 
int
 index
;
    
private
 
String
 name
;

    
SeasonEnum
(
int
 index
,
 
String
 name
)
 
{
        
this
.
index 
=
 index
;
        
this
.
name 
=
 name
;
    
}

    
public
 
int
 getIndex
()
 
{
        
return
 index
;
    
}
    
public
 
String
 getName
()
 
{
        
return
 name
;
    
}

    
//接口方法
    
@Override
    
public
 
void
 showSeasonBeauty
()
 
{
        
System
.
out
.
println
(
"welcome to "
 
+
 
this
.
name
);
    
}

    
//接口方法
    
@Override
    
public
 
String
 getSeasonName
()
 
{
        
return
 
this
.
name
;
    
}
}

使用接口组织枚举

public
 
interface
 
Food
 
{
    
enum
 
Coffee
 
implements
 
Food
{
  
        BLACK_COFFEE
,
DECAF_COFFEE
,
LATTE
,
CAPPUCCINO
    
}
  
    
enum
 
Dessert
 
implements
 
Food
{
  
        FRUIT
,
 CAKE
,
 GELATO
    
}
  
}

六、枚举类比较是用==还是equals?

先看一个例子,如下:

public
 
class
 
EnumTest
 
{
    
public
 
static
 
void
 main
(
String
[]
 args
)
 
{

        
Shrubbery
 s1 
=
 
Shrubbery
.
CRAWLING
;
        
Shrubbery
 s2 
=
 
Shrubbery
.
GROUND
;
        
Shrubbery
 s3 
=
 
Shrubbery
.
CRAWLING
;

        
System
.
out
.
println
(
"s1==s2,result: "
 
+
 
(
s1 
==
 s2
));
        
System
.
out
.
println
(
"s1==s3,result: "
 
+
 
(
s1 
==
 s3
));
        
System
.
out
.
println
(
"Shrubbery.CRAWLING.equals(s1),result: "
+
Shrubbery
.
CRAWLING
.
equals
(
s1
));
        
System
.
out
.
println
(
"Shrubbery.CRAWLING.equals(s2),result: "
+
Shrubbery
.
CRAWLING
.
equals
(
s2
));

    
}
}

运行结果:

s1
==
s2
,
result
:
 
false
s1
==
s3
,
result
:
 
true
Shrubbery
.
CRAWLING
.
equals
(
s1
),
result
:
 
true
Shrubbery
.
CRAWLING
.
equals
(
s2
),
result
:
 
false

可以发现不管用==还是equals,都是可以的。其实枚举的equals方法,就是用==比较的,如下:

public
 
final
 
boolean
 equals
(
Object
 other
)
 
{
    
return
 
this
==
other
;
}

七、枚举实现的单例

effective java提过,最佳的单例实现模式就是枚举模式。单例模式的实现有好几种方式,为什么是枚举实现的方式最佳呢?

因为枚举实现的单例有以下优点:

  • 枚举单例写法简单
  • 枚举可解决线程安全问题
  • 枚举可解决反序列化会破坏单例的问题 一个枚举单例demo如下:
public
 
class
 
SingletonEnumTest
 
{
   
public
 
enum
 
SingletonEnum
 
{
        INSTANCE
,;
        
private
 
String
 name
;

        
public
 
String
 getName
()
 
{
            
return
 name
;
        
}

        
public
 
void
 setName
(
String
 name
)
 
{
            
this
.
name 
=
 name
;
        
}
    
}

    
public
 
static
 
void
 main
(
String
[]
 args
)
 
{
        
SingletonEnum
.
INSTANCE
.
setName
(
"jay@huaxiao"
);
        
System
.
out
.
println
(
SingletonEnum
.
INSTANCE
.
getName
());
    
}
}

有关于枚举实现单例,想深入了解的朋友可以看Hollis大神这篇文章,写得真心好!为什么我墙裂建议大家使用枚举来实现单例。

八、EnumSet 和EnumMap

EnumSet

先来看看EnumSet的继承体系图

显然,EnumSet也实现了set接口,相比于HashSet,它有以下优点:

  • 消耗较少的内存
  • 效率更高,因为是位向量实现的。
  • 可以预测的遍历顺序(enum常量的声明顺序)
  • 拒绝加null

EnumSet就是set的高性能实现,它的要求就是存放必须是同一枚举类型。EnumSet的常用方法:

  • allof() 创建一个包含指定枚举类里所有枚举值的EnumSet集合
  • range() 获取某个范围的枚举实例
  • of() 创建一个包括参数中所有枚举元素的EnumSet集合
  • complementOf() 初始枚举集合包括指定枚举集合的补集 看实例,最实际:
public
 
class
 
EnumTest
 
{
    
public
 
static
 
void
 main
(
String
[]
 args
)
 
{
        
// Creating a set
        
EnumSet
<
SeasonEnum
>
 set1
,
 set2
,
 set3
,
 set4
;

        
// Adding elements
        set1 
=
 
EnumSet
.
of
(
SeasonEnum
.
SPRING
,
  
SeasonEnum
.
FALL
,
 
SeasonEnum
.
WINTER
);
        set2 
=
 
EnumSet
.
complementOf
(
set1
);
        set3 
=
 
EnumSet
.
allOf
(
SeasonEnum
.
class
);
        set4 
=
 
EnumSet
.
range
(
SeasonEnum
.
SUMMER
,
SeasonEnum
.
WINTER
);
        
System
.
out
.
println
(
"Set 1: "
 
+
 set1
);
        
System
.
out
.
println
(
"Set 2: "
 
+
 set2
);
        
System
.
out
.
println
(
"Set 3: "
 
+
 set3
);
        
System
.
out
.
println
(
"Set 4: "
 
+
 set4
);
    
}
}

输出结果:

Set
 
1
:
 
[
SPRING
,
 FALL
,
 WINTER
]
Set
 
2
:
 
[
SUMMER
]
Set
 
3
:
 
[
SPRING
,
 SUMMER
,
 FALL
,
 WINTER
]
Set
 
4
:
 
[
SUMMER
,
 FALL
,
 WINTER
]

EnumMap

EnumMap的继承体系图如下:

EnumMap也实现了Map接口,相对于HashMap,它也有这些优点:

  • 消耗较少的内存
  • 效率更高
  • 可以预测的遍历顺序
  • 拒绝null

EnumMap就是map的高性能实现。 它的常用方法跟HashMap是一致的,唯一约束是枚举相关。

看实例,最实际:

public
 
class
 
EnumTest
 
{
    
public
 
static
 
void
 main
(
String
[]
 args
)
 
{
        
Map
<
SeasonEnum
,
 
String
>
 map 
=
 
new
 
EnumMap
<>(
SeasonEnum
.
class
);
        map
.
put
(
SeasonEnum
.
SPRING
,
 
"春天"
);
        map
.
put
(
SeasonEnum
.
SUMMER
,
 
"夏天"
);
        map
.
put
(
SeasonEnum
.
FALL
,
 
"秋天"
);
        map
.
put
(
SeasonEnum
.
WINTER
,
 
"冬天"
);
        
System
.
out
.
println
(
map
);
        
System
.
out
.
println
(
map
.
get
(
SeasonEnum
.
SPRING
));
    
}
}

运行结果

{
SPRING
=春天,
 SUMMER
=夏天,
 FALL
=秋天,
 WINTER
=冬天}
春天

九、待更新

有关于枚举关键知识点,亲爱的朋友,你有没有要补充的呢?

参考与感谢

  • 关于Java中枚举Enum的深入剖析
  • 深度分析Java的枚举类型—-枚举的线程安全性及序列化问题
  • 为什么我墙裂建议大家使用枚举来实现单例。
  • 深入理解Java枚举类型(enum)
  • 深入理解 Java 枚举
  • EnumSet in Java
  • Java 枚举(enum) 详解7种常见的用法
  • 《Java编程思想》

个人公众号

  • 如果你是个爱学习的好孩子,可以关注我公众号,一起学习讨论。
  • 如果你觉得本文有哪些不正确的地方,可以评论,也可以关注我公众号,私聊我,大家一起学习进步哈。