Java 枚举(enum)
1、枚举类概述
Java 枚举是一个特殊的类,一般表示一组常量,比如一年的 4 个季节,一个年的 12 个月份,一个星期的 7 天,方向有东南西北等。像这种类的对象只有有限个,确定的,强烈建议使用枚举类,我们之前在多线程编程中提到的线程状态state就是一个枚举类
public enum State {
NEW, //初始状态、开始状态
RUNNABLE, //就绪状态、可运行状态。
BLOCKED, //阻塞状态
WAITING, //等待状态
TIMED_WAITING, //超时等待状态
TERMINATED; //终止状态、结束状态
}
JDK1.5之前需要自定义枚举类,JDK 1.5 及之后Java 枚举类使用 enum 关键字来定义,各个常量使用逗号,来分割。例如定义一个颜色的枚举类。
enum Color {
RED, GREEN, BLUE;
}
枚举类对象的调用
enum Color{
RED, GREEN, BLUE;
}
public class Test{
public static void main(String[] args){
Color c1 = Color.RED;//枚举类对象的调用
System.out.println(c1);//RED
}
}
3、自定义枚举类
步骤
- 提供类的属性,声明为private final
- 声明为
final的属性,在私有构造器中初始化 - 提供公共方法可以获取枚举类的属性
- 创建共有的枚举类的多个对象
public class Season {
//提供类的属性,声明为常量 private final
private final String seasonName;
private final String seasonDesc;
//声明为final的属性,私有化构造器中初始化
private Season(String seasonName, String seasonDesc) {
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
//创建当前枚举类的多个对象
//public static final的,static可以方便类调用。由于不使用get方法。所以用public便于访问
public static final Season SPRING = new Season("春天", "春暖花开");
public static final Season SUNNER = new Season("夏天", "夏日炎炎");
public static final Season ATAUM = new Season("秋天", "秋高气爽");
public static final Season WINTER = new Season("冬天", "白雪皑皑");
@Override
public String toString() {
return "Season [seasonName=" + seasonName + ", seasonDesc=" + seasonDesc + "]";
}
//提供公共方法获取枚举类的属性
public String getSeasonName() {
return seasonName;
}
//提供公共方法获取枚举类的属性
public String getSeasonDesc() {
return seasonDesc;
}
}
测试
//使用:调用枚举类对象
public static void main(String[] args) {
Season spring = Season.SPRING; //类名.属性方式调用
System.out.println(spring);//Season [seasonName=春天, seasonDesc=春暖花开]
}
4、使用Enum关键字定义枚举类
- 枚举类对象的属性不应允许被改动,所以应该使用private final修饰
- 枚举类的使用private final修饰的属性应该在构造器中为其赋值
- 若枚举类显式的定义了带参数的构造器,则在列出枚举值时也必须对应的传入参数
- 必须在枚举类的第一行声明枚举类对象
enum Season1 {
//1.提供当前枚举类的对象,多个对象之间用","隔开,末尾对象";"结束
//会自动调用有参构造器
SPRING("春天", "春暖花开"),
SUMMER("夏天", "夏日炎炎"),
AUTUMN("秋天", "秋高气爽"),
WINTER("冬天", "冰天雪地");
//2.声明Season对象的属性:private final 修饰
private final String seasonName;
private final String seasonDesc;
//3.私有化类的构造器,并给对象属性赋值
private Season1(String seasonName, String seasonDesc) {
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
//4.其他诉求1:获取枚举类对象的属性
public String getSeasonName() {
return seasonName;
}
public String getSeasonDesc() {
return seasonDesc;
}
}
测试:
不建议在Enum枚举类里面重写tostring方法,因为我们使用Enum关键字声明的枚举类继承于Enum类,而Enum类中重写了继承自Object类的toString()方法,Enum类中重写的toString()方法输出的就是枚举类对象所对应的常量名 ,那么我们打印枚举类对象,打印是什么呢,是地址值嘛?答:对象名
//使用:调用枚举类对象
public static void main(String[] args) {
SeasonEnum spring = SeasonEnum.SPRING; //调用枚举类对象
System.out.println(spring);//SPRING
//toString:返回枚举类型对象的名称
System.out.println(spring.toString());//SPRING
System.out.println(SeasonEnum.class.getSuperclass());//父类Enum,class java.lang.Enum
}
5、Enum枚举类成员
枚举跟普通类一样可以用自己的变量、方法和构造函数,构造函数只能使用 private 访问修饰符,所以外部无法调用。枚举既可以包含具体方法,也可以包含抽象方法。 如果枚举类具有抽象方法,则枚举类的每个实例都必须实现它。
public enum Color {
RED, GREEN, BLUE; //三个对象就会自动调用私有化构造函数三次
// 私有化构造函数
private Color() {
System.out.println("Constructor called for : " + this.toString());
}
public void colorInfo() {
System.out.println("Universal Color");
}
public static void main(String[] args) {
Color c1 = Color.RED;
System.out.println(c1);
c1.colorInfo();
}
}
执行以上代码输出结果为:
Constructor called for : RED
Constructor called for : GREEN
Constructor called for : BLUE
RED
Universal Color
6、Enum类的常用方法
enum 定义的枚举类默认继承了 java.lang.Enum 类,并实现了 java.lang.Seriablizable 和 java.lang.Comparable 两个接口。values(), ordinal() 和 valueOf() 方法位于 java.lang.Enum 类中:
可以看到,枚举也提供了序列化机制。某些情况,比如我们要通过网络传输一个数据库连接的句柄,会提供很多帮助。
6.1、values()
- 通过枚举类直接调用此方法返回枚举类的对象数组,也就是将枚举类的对象以一个一个数组元素合并成一个数组返回
- 这个方法是一个静态方法
- 该方法可以很方便地遍历所有枚举值
SeasonEnum [] values = SeasonEnum.values();
for(int i = 0; i < values.length ; i++){
System.out.println(values[i]);
}
在学习多线程中我们曾经学到Thread类中有一个内部类State,State类就是通过enum关键字定义的枚举类,在State类中定义了线程运行的几种状态
Thread.State [] values = Thread.State.values();
for(int i = 0; i < values.length ; i++){
System.out.println(values[i])
}
6.2、valueof(String objName)
- 使用枚举类调用此方法,可以把一个字符串转换为对应的同名的枚举类对象
- 这个方法是一个静态方法
- 要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:
IllegalArgumentException非法的参数异常
/*
通过SeasonEnum类调用valueof()方法创建了一个WINTER对象并赋给了SeasonEnum类型的引用winter
*/
SeasonEnum winter = SeasonEnum.valueof("WINTER");
//这里输出的自然就是winter引用对应的枚举类对象的对象名 : WINTER
System.out.println(winter);
6.3、ordinal()方法
- ordinal()方法可以找到每个枚举常量的索引,就像数组索引一样。
enum Color{
RED, GREEN, BLUE;
}
public class Test{
public static void main(String[] args){
// 调用 values()
Color[] arr = Color.values();
// 迭代枚举
for (Color col : arr){
// 查看索引
System.out.println(col + " at index " + col.ordinal());
}
// 使用 valueOf() 返回枚举常量,不存在的会报错 IllegalArgumentException
System.out.println(Color.valueOf("RED"));
// System.out.println(Color.valueOf("WHITE"));
}
}
执行以上代码输出结果为:
RED at index 0
GREEN at index 1
BLUE at index 2
RED
7、实现接口的枚举类
- 和普通 Java 类一样,枚举类可以实现一个或多个接口
- 若每个枚举对象在调用实现的接口方法呈现相同的行为方式,则只要统一实现该方法即可。
- 若需要每个枚举对象在调用实现的接口方法呈现出不同的行为方式, 则可以让每个枚举对象分别来实现该方法
(1)实视接口,在enum类中实现抽象方法
interface Info{
void show();
}
enum Season1 implements Info {
//1.提供当前枚举类的对象,多个对象之间用","隔开,末尾对象";"结束
SPRING("春天", "春暖花开"),
SUMMER("夏天", "夏日炎炎"),
AUTUMN("秋天", "秋高气爽"),
WINTER("冬天", "冰天雪地");
//2.声明Season对象的属性:private final 修饰
private final String seasonName;
private final String seasonDesc;
//3.私有化类的构造器,并给对象属性赋值
private Season1(String seasonName, String seasonDesc) {
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
//4.其他诉求1:获取枚举类对象的属性
public String getSeasonName() {
return seasonName;
}
public String getSeasonDesc() {
return seasonDesc;
}
@Override
public void show() {
System.out.println("这是一个季节");
}
}
(2)让枚举类的对象分别实现接口中的抽象方法
interface Info {
void show();
}
enum Season1 implements Info {
//1.提供当前枚举类的对象,多个对象之间用","隔开,末尾对象";"结束
SPRING("春天", "春暖花开") {
@Override
public void show() {
System.out.println("这是一个季节");
}
},
SUMMER("夏天", "夏日炎炎") {
@Override
public void show() {
System.out.println("这是一个季节");
}
},
AUTUMN("秋天", "秋高气爽") {
@Override
public void show() {
System.out.println("这是一个季节");
}
},
WINTER("冬天", "冰天雪地") {
@Override
public void show() {
System.out.println("这是一个季节");
}
};
//2.声明Season对象的属性:private final 修饰
private final String seasonName;
private final String seasonDesc;
//3.私有化类的构造器,并给对象属性赋值
private Season1(String seasonName, String seasonDesc) {
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
//4.其他诉求1:获取枚举类对象的属性
public String getSeasonName() {
return seasonName;
}
public String getSeasonDesc() {
return seasonDesc;
}
}
测试:
public static void main(String[] args) {
Season1.SPRING.show();//枚举类.对象.方法
}
结果
这是一个季节
8、内部类中使用枚举
枚举类也可以声明在内部类中:
public class Test{
enum Color{
RED, GREEN, BLUE;
}
// 执行输出结果
public static void main(String[] args){
Color c1 = Color.RED;
System.out.println(c1); //RED
}
}
9、在 switch 中使用枚举类
JDK 1.5 中可以在 switch 表达式中使用Enum定义的枚举类的对象作为表达式,case 子句可以直接使用枚举值的名字, 无需添加枚举类作为限定。 枚举类常应用于 switch 语句中:
enum Color{
RED, GREEN, BLUE;
}
public class MyClass {
public static void main(String[] args) {
Color myVar = Color.BLUE;
switch(myVar) {
case RED:
System.out.println("红色");
break;
case GREEN:
System.out.println("绿色");
break;
case BLUE:
System.out.println("蓝色");
break;
}
}
}
执行以上代码输出结果为:
蓝色
由于 switch 是要在有限的可能值集合中进行选择,因此它和 enum 是绝佳的组合。一般来说,在 switch 中使用整数值,而枚举实例天生就是具备整数值的次序,并且可以通过 ordinal() 方法取得其次序(现在显然是编译器帮我们做了类似的工作),因此我们可以再 switch 语句中使用 enum。
10、利用枚举实现策略模式
public enum CalculationEnum {
/**
* 加法运算
*/
ADD{
@Override
public double exec(double num1, double num2) {
return num1 + num2;
}
},
/**
* 减法运算
*/
SUB{
@Override
public double exec(double num1, double num2) {
return num1 - num2;
}
};
public abstract double exec(double num1, double num2);
public static void main(String[] args) {
double addNum = CalculationEnum.ADD.exec(1, 2.2);
System.out.println(addNum);
double subNum = CalculationEnum.SUB.exec(3.2, 1.1);
System.out.println(subNum);
}
}
11、利用枚举实现单例模式
传统的方式:
饿汉式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
饿汉式的缺点就是,可能在还不需要此实例的时候就已经把实例创建出来了,没起到lazy loading的效果。优点就是实现简单,而且安全可靠
懒加载模式:lazy loaded
public class SingletonSafe {
private static volatile SingletonSafe singleton;
private SingletonSafe() {
}
public static SingletonSafe getSingleton() {
if (singleton == null) {
synchronized (SingletonSafe.class) {
if (singleton == null) {
singleton = new SingletonSafe();
}
}
}
return singleton;
}
}
这就是用double checked locking 双重校验 方法实现的单例,这里的getInstance()方法要检查两次,确保是否实例INSTANCE是否为null或者已经实例化了,通过加锁 ,对懒汉式单例模式做了线程安全处理,可以保证同时只有一个线程走到第二个判空代码中去,这样保证了只创建 一个实例。这里还用到了volatile关键字来修饰singleton,其最关键的作用是防止指令重排。
枚举实现:
注意: 若枚举只有一个对象,则可以作为一种单例模式的实现方式。
上面这种模式,即使有DCL(double checked locking) 也可能会创建不止一个实例,尽管在Java5这个问题修复了(jdk1.5在内存模型上做了大量的改善,提供了volatile关键字来修饰变量),但是仍然对新手来说还是比较棘手
选择枚举作为Singleton的实现方式,相对于其他方式尤其是类似的饿汉模式主要有以下优点:
- 代码简单
- 自由序列化
默认枚举实例的创建是线程安全的,但是在枚举中的其他任何实例方法由程序员自己负责。
public enum Singleton {
INSTANCE; //创建实例的时候会调用构造方法
public void doSomething() {
System.out.println("doSomething");
}
}
public class Main {
public static void main(String[] args) {
Singleton.INSTANCE.doSomething(); //通过Singleton.INSTANCE来调用实例对象
}
}
传统单例模式通过调用getInstance()方法 获得对象,而枚举类直接通过Singleton.INSTANCE来获得实例对象。从创建一个单例模式来看,枚举模式可以全部在一行内完成,并且枚举创建的单例在JVM层面上也能保证实例是线程安全的。
单元素的枚举类型已经成为实现Singleton的最佳方法。
单例是如何被保证的:
首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举对象实例时会自动执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。 也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。
12、枚举是如何保证线程安全的
从下面代码出发
public enum Color {
RED, GREEN, BLUE,yellow;
}
我们使用反编译后代码内容如下
通过反编译后代码我们可以看到类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承,同时我们看到这个类中有几个static类型的属性和方法。 因为static类型的属性会在类被加载之后被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的。