深入理解 Java 枚举 Enum 类型用法

简介: 深入理解 Java 枚举 Enum 类型用法

枚举(enum),是指一个经过排序的、被打包成一个单一实体的项列表

一个枚举的实例可以使用枚举项列表中任意单一项的值。枚举在各个语言当中都有着广泛的应用,通常用来表示诸如颜色、方式、类别、状态等等数目有限、形式离散、表达又极为明确的量。Java从JDK5开始,引入了对枚举的支持

在枚举出现之前,如果想要表示一组特定的离散值,往往使用一些常量。例如:

package com.fhp.enumexample;
 
public class Entity {
    
    // 视频
    public static final int VIDEO = 1;
    // 音频
    public static final int AUDIO = 2;
    // 文字
    public static final int TEXT = 3;
    // 图片
    public static final int IMAGE = 4;
    
    private int id;
    private int type;
    
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public int getType() {
        return type;
    }
    public void setType(int type) {
        this.type = type;
    }    
}

当然,常量也不仅仅局限于int型,诸如char和String等也是不在少数。然而,无论使用什么样的类型,这样做都有很多的坏处。这些常量通常都是连续、有无穷多个值的量,而类似这种表示类别的量则是离散的,并且通常情况下只有有限个值。用连续的量去表示离散量,会产生很多问题。例如,针对上述的Entity类,如果要对Entity对象的type属性进行赋值,一般会采用如下方法:

Entity e = new Entity();
e.setId(10);
e.setType(2);

这样做的缺点有:(1)代码可读性差、易用性低。由于setType()方法的参数是int型的,在阅读代码的时候往往会让读者感到一头雾水,根本不明白这个2到底是什么意思,代表的是什么类型。当然,要保证可读性,还有这样一个办法:

e.setType(Entity.AUDIO);

而这样的话,问题又来了。这样做,客户端必须对这些常量去建立理解,才能了解如何去使用这个东西。说白了,在调用的时候,如果用户不到Entity类中去看看,还真不知道这个参数应该怎么传、怎么调。像是setType(2)这种用法也是在所难免,因为它完全合法,不是每个人都能够建立起用常量名代替数值,从而增加程序可读性、降低耦合性的意识

(2)类型不安全。在用户去调用的时候,必须保证类型完全一致,同时取值范围也要正确。像是setType(-1)这样的调用是合法的,但它并不合理,今后会为程序带来种种问题。也许你会说,加一个有效性验证嘛,但是,这样做的话,又会引出下面的第(3)个问题

(3)耦合性高,扩展性差。假如,因为某些原因,需要修改Entity类中常量的值,那么,所有用到这些常量的代码也就都需要修改——当然,要仔细地修改,万一漏了一个,那可不是开玩笑的。同时,这样做也不利于扩展。例如,假如针对类别做了一个有效性验证,如果类别增加了或者有所变动,则有效性验证也需要做对应的修改,不利于后期维护

(4)常量作为参数时,是String,int等弱类型,开发人员可以传入没有在常量接口里定义的值,这个问题无法通过编译器发现

(5)编译时,是直接把常量的值编译到类的二进制代码里,常量的值在升级中变化后,需要重新编译引用常量的类,因为里面存的是旧值

(6)如果常量类的构造器不私有,无法限制开发员继承/实现接口,开发员能够在子接口里继续添加常量.而这些常量可能得不到祖先层的支持

枚举就是为了这样的问题而诞生的。它们给出了将一个任意项同另一个项相比较的能力,并且可以在一个已定义项列表中进行迭代。枚举(在Jave中简称为enum)是一个特定类型的类。所有枚举都是Java中的新类java.lang.Enum的隐式子类。此类不能手工进行子类定义。一个简单的枚举可以是这样:

package com.fhp.enumexample;
 
public enum TypeEnum {
    VIDEO, AUDIO, TEXT, IMAGE
}

上面的Entity类就可以改成这样:

package com.fhp.enumexample;
 
public class Entity {
    
    private int id;
    private TypeEnum type;
    
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
        
    public TypeEnum getType() {
        return type;
    }
    public void setType(TypeEnum type) {
        this.type = type;
    }
}

在为Entity对象赋值的时候,就可以这样:

Entity e = new Entity();
e.setId(10);
e.setType(TypeEnum.AUDIO);

怎么看,都是好了很多。在调用setType()时,可选值只有四个,否则会出现编译错误,因此可以看出,枚举是类型安全的,不会出现取值范围错误的问题。同时,客户端不需要建立对枚举中常量值的了解,使用起来很方便,并且可以容易地对枚举进行修改,而无需修改客户端。如果常量从枚举中被删除了,那么客户端将会失败并且将会收到一个错误消息。枚举中的常量名称可以被打印,因此除了仅仅得到列表中项的序号外还可以获取更多信息。这也意味着常量可用作集合的名称,例如HashMap

1 基本enum特征

所有创建的枚举类都继承自抽象类 java.lang.Enum;

一个枚举类,所有实例都要在第一句写出以 ,隔开。 如果只有实例最后可以不加 ; 枚举类因为继承了Enum,所以再不能继承别的类,任何类也不能继承枚举类(构造器默认为private)

public enum Color {
    RED,
    BLUE,
    YELLOW,
    PURPLE
}

注意 :RED,BLUE 这些是由 enum Color类调用默认private构造器创建的实例对象,和普通class相比只不过enum的实例对象只能在内部创建。时刻记着他们是一个实例对象

枚举类实例不能在外部 new 出来 ,因为枚举类构造器为private 一般也不用声明private,默认就是private,因为enum实例只能在编译期间enum类内部被创建,但可以外部得到一个实例的引用

Color red  = Color.RED;
Color blue = Color.BLUE;

枚举类的一些方法

  1. 使用枚举元素 Color.RED 或者 red
  2. Color.values( ) 返回一个枚举类数组,数组元素就是枚举的实例。
  3. red.ordinal() 方法返回该实例声明的次序从0开始。
  4. 2个实例可用 == 比较
  5. Enum 实现了 Comparable和 Serializable 可以使用comparableTo( )方法,可以序列化
  6. a. getDeclaringClass() 返回 Color.class 对象
  7. a.name和a.toString( )方法一样。
  8. ​ valuesOf(string ) 返回一个实例对象 Color b = Color.valueOf("BLUE");
  9. 根据class对象返回实例 Color b = Color.valueOf( Color.class, "BLUE" )

通过带参构造器为枚举实例添加描述信息。调用 getDes()就可以的到当前对象的描述信息

调用 getDes( )方法就可以的到当前对象的描述信息

public enum Color {
    RED("这是红色"),
    BLUE("这是蓝色"),
    YELLOW("这是黄色"),
    PURPLE("这是紫色");
    String des;
    Color( String s) {
        this.des = s;
    }
    public String getDes(){
        return des;
    }
}

重写toString( )方法来为enum实例添加描述信息, 通过name() 拿到当前对象名字

public enum Color {
    RED,
    BLUE,
    YELLOW,
    PURPLE;

    @Override
    public String toString() {
        String id = name();
        return id + " is " + id.toLowerCase();
    }
}

2 enum的特殊方法

除了不能继承enum类外,enum和其普通类没区别,可以添加字段,方法,甚至是main 方法

  1. enum 实例也可以用在 switch语句中
  2. values()方法不在Enum中 它是由编译器添加的static方法
  3. 编译器还添加了一个valuesOf(String s)方法,这个只需要一个参数就可以的得到实例,而Enum的需要2个
  4. 如果将enum向上转型为Enum那么values 和 valuesOf 无法再使用
  5. 与values 方法有相同作用的就是Class对象的getEnumConstants(),如果class是枚举类那么返回元素数组,不是枚举类返回null

3 使用接口组织枚举

为了实现扩展enum 或者将enum分类,因为无法继承所以靠扩展子类无法实现,可以利用接口来达到这些功能

public interface Food {
    enum Fruit implements Food{
        APPLE, BANANA, ORANGE;
    }
    enum Vegetables implements Food{
        TOMATO, POTATO, BEANS;
    }
    enum Drink implements Food{
        COFFEE, COCA, REDBULL;
    }
}


   public static void main(String[] args) {
        Food food = Food.Fruit.APPLE;
        food = Food.Drink.REDBULL;
        food = Food.Vegetables.BEANS;
    }

接口基础上创建一个枚举的枚举,通过该enum控制其他enum,而不是不同的类型分别都要向上转型为Food ,类多时分别向上转型不如每个用一个enum控制方便

通过实例调用getValues方法就可以的到该实例的所有元素

public enum Course {
    FRUIT(Food.Fruit.class),
    DRINK(Food.Drink.class),
    VEGETABLES(Food.Vegetables.class);
    private Food[] values;
    Course(Class<? extends Food> kind) {
        this.values = kind.getEnumConstants();
    }
    public Food[] getValues() {
        return values;
    }
}

enum嵌套在另一个enum 重新组织1 2 代码合二为一

public enum Course {
    FRUIT(Food.Fruit.class),
    DRINK(Food.Drink.class),
    VEGETABLES(Food.Vegetables.class);
    private Food[] values;

    Course(Class<? extends Food > kind) {
        this.values = kind.getEnumConstants();
    }

    interface Food {
        enum Fruit implements Food {
            APPLE, BANANA, ORANGE;
        }

        enum Vegetables implements Food {
            TOMATO, POTATO, BEANS;
        }

        enum Drink implements Food {
            COFFEE, COCA, REDBULL;
        }
    }

    public Food[] getValues() {
        return values;
    }
   
}

4 EnumSet

EnumSet (抽象类)一个用来存放enum 元素的Set,存取enum速度非常快,性能非常高

  1. EnumSet 只能存放enum元素,不能插入空元素
  2. 放入的元素位置和enum中保持一样,它处于排序状态,是一个有序Set
  3. Enum 是个抽象类且方法除了colon( )克隆一个EnumSet外都是静态方法返回值都是EnumSet
EnumSet<Color> enumSet = EnumSet.noneOf(Color.class); //创建一个空集
EnumSet<Color> enumSet2 = EnumSet.allOf(Color.class); //把集合中所有元素添加进去
EnumSet<Color> enumSet3 = EnumSet.of(RED);//添加一个元素

EnumSet不止这几个方法,对于of() 方法重载了6次,当传入2-5个参数调用相应方法,传入1个或者5个以上调用可变参数

5 EnumMap

特殊Map(类),key必须是enum, 由于enum元素有限所以内部只是由数组实现

  1. 这是一个有序map,保持enum的元素顺序
  2. EnumMap<Color,Object> map = new EnumMap<Color, Object>(Color.class)
  3. 方法和其他Map一样

6 实例对象添加方法

为每一个enum实例(相当于常量)添加一个方法,让他们有不同的行为

为每一个实例添加不同行为的方法

  1. 在enum中创建一个或者多个abstract 方法,因为是enum的实例所以就得实现这些方法
RED{
        @Override
        String getInfo() {
            return null;
        }

        @Override
        String getTime() {
            return null;
        }
    };
    abstract String getInfo();
    abstract String getTime();
  1. enum也可以有main方法作为enum执行入口
  2. 常量添加方法后和有了类的行为,貌似和内部类一样,但他们有着不同行为,enum的常量不能作为方法参数类型,因为他们不是类,只是enum类型的static final 实例
  3. 由于enum的常量是 static final 的所以常量的方法不能访问外部类的非静态方法

覆盖常量相关的方法

  1. enum中所有非抽象方法每个实例都可以调
  2. 如果不需要每个实例都实现抽象类,那么就可以不用定义抽象类,每个实例各自实现方法,实现的方法可以覆盖enum中的方法

使用enum职责链

  • 多种不同的方式解决问题,然后把它们连接在一起,但一个请求到达时遍历整个链,直到解决问题
  • enum非常适合作为解决某一个问题的职责链,请求到达时遍历整个enum,直到解决问题
import java.util.EnumSet;
import java.util.Random;

public enum COR {
    SOLUTION_ONE{
        @Override
        boolean Solve(int i) {
            if (i == 1){
                System.out.println(name()+" 解决问题 " +i);
                return true;
            } return false;
        }
    },
    SOLUTION_TWO{
        @Override
        boolean Solve(int i) {
            if (i == 2){
                System.out.println(name()+" 解决问题 " +i);
                return true;
            }return false;
        }
    },
    SOLUTION_THREE{
        @Override
        boolean Solve(int i) {
            if (i == 3){
                System.out.println(name()+" 解决问题 " +i);
                return true;
            }return false;
        }
    },
    SOLUTION_FOUR{
        @Override
        boolean Solve(int i) {
            if (i == 4){
                System.out.println(name()+" 可以解决问题 " +i);
                return true;
            }return false;
        }
    };

    abstract boolean Solve(int i);

    public static void main(String[] args) {
        Random random = new Random();
        EnumSet<COR> cors = EnumSet.allOf(COR.class);
        for (int i = 0; i < 6; i++) {
            int id = random.nextInt(4)+1;
            for (COR cor :cors) {
                if (cor.Solve(id)){
                    System.out.println(" 解决问题 " +id);
                    break;
                }
            }
        }
    }

}

enum状态机

  • 状态机可以具有 有限个 状态,通常根据输入,从一个状态转移到下一个状态,也可以有瞬时状态,一但任务结束就立刻离开瞬时状态

7 多路分发

  • 多种类型交互时有时并不能确定所有类型,如: NUM.complete(NUM) , NUM 是所有数字类型的超类,a.complete(b) ,a b可能是同种类型也可能不是同一种类型
  • Java 动态绑定只能处理一种类型,属于单路分发(分派),动态绑定能将complete绑定到分路a。只有方法调用才会执行动态绑定

可以为每一个分发实现自己的动态绑定

public enum Outcome { WIN, LOSE, DRAW } ///:~  
  
  
interface Item {  
    Outcome compete(Item it);  
  
    Outcome eval(Paper p);  
  
    Outcome eval(Scissors s);  
  
    Outcome eval(Rock r);  
}  
  
class Paper implements Item {  
    public Outcome compete(Item it) {  
        return it.eval(this);  
    }  
  
    public Outcome eval(Paper p) {  
        return DRAW;  
    }  
  
    public Outcome eval(Scissors s) {  
        return WIN;  
    }  
  
    public Outcome eval(Rock r) {  
        return LOSE;  
    }  
  
    public String toString() {  
        return "Paper";  
    }  
}  
  
class Scissors implements Item {  
    public Outcome compete(Item it) {  
        return it.eval(this);  
    }  
  
    public Outcome eval(Paper p) {  
        return LOSE;  
    }  
  
    public Outcome eval(Scissors s) {  
        return DRAW;  
    }  
  
    public Outcome eval(Rock r) {  
        return WIN;  
    }  
  
    public String toString() {  
        return "Scissors";  
    }  
}  
  
class Rock implements Item {  
    public Outcome compete(Item it) {  
        return it.eval(this);  
    }  
      
    public Outcome eval(Paper p) {  
        return WIN;  
    }  
  
    public Outcome eval(Scissors s) {  
        return LOSE;  
    }  
  
    public Outcome eval(Rock r) {  
        return DRAW;  
    }  
  
    public String toString() {  
        return "Rock";  
    }  
}  
  
public class RoShamBo1 {  
    static final int SIZE = 20;  
    private static Random rand = new Random(47);  
  
    public static Item newItem() {  
        switch (rand.nextInt(3)) {  
        default:  
        case 0:  
            return new Scissors();  
        case 1:  
            return new Paper();  
        case 2:  
            return new Rock();  
        }  
    }  
  
    public static void match(Item a, Item b) {  
        System.out.println(a + " vs. " + b + ": " + a.compete(b));  
    }  
  
    public static void main(String[] args) {  
        for (int i = 0; i < SIZE; i++)  
            match(newItem(), newItem());  
    }  
}

使用enum实现多路分发

  1. enum的实例不能作为类型参数,不可以重载方法
  2. 可以使用enum构造器初始化每个enum实例,并以一组结果作为参数如 ENUM_A( vsA_DRAW, vsB_LOSE, vsC_WIN ) 在比较方法中使用switch 判断 返回 结果
package enums;

import static enums.OutCome.*;

public enum RoSham {
    PAPER(DRAW, LOSE, WIN),
    SCISSORS(WIN, DRAW, LOSE),
    ROCK(LOSE, WIN, DRAW);

    private OutCome vPAPER, vSCISSORS, vROCK;

    RoSham(OutCome paper, OutCome scissors, OutCome rock) {
        this.vPAPER = paper;
        this.vSCISSORS = scissors;
        this.vROCK = rock;
    }

    public OutCome complete(RoSham it) {
        switch (it) {
            default:
            case PAPER:
                return vPAPER;
            case SCISSORS:
                return vSCISSORS;
            case ROCK:
                return vROCK;
        }
    }

    public static void main(String[] args) {
        System.out.println(PAPER.complete(ROCK));
    }
}

PAPER.complete()时把PAPER构造器中的结果与 OutCome 变量绑定,根据对比的参数返回对比结果,因此实例构造器中的参数位置非常重要

EnumMap实现真正的多路分发

package enums;

import java.util.EnumMap;

import static enums.OutCome.*;

public enum RoShamBo {
    PAPER, SCISSORS, ROCK;
    static EnumMap<RoShamBo, EnumMap<RoShamBo, OutCome>>
            table = new EnumMap<RoShamBo, EnumMap<RoShamBo, OutCome>>(RoShamBo.class);

    static {
        for (RoShamBo it : RoShamBo.values()) {

            table.put(it, new EnumMap<RoShamBo, OutCome>(RoShamBo.class));
        }

        initRow(PAPER, DRAW, LOSE, WIN);
        initRow(SCISSORS, WIN, DRAW, LOSE);
        initRow(ROCK, LOSE, WIN, DRAW);

    }

    static void initRow(RoShamBo it, OutCome vPAPER, OutCome vSCISSORS, OutCome vROCK) {
        EnumMap<RoShamBo, OutCome> row = RoShamBo.table.get(it);
        row.put(RoShamBo.PAPER, vPAPER);
        row.put(RoShamBo.SCISSORS, vSCISSORS);
        row.put(RoShamBo.ROCK, vROCK);
    }

    public OutCome complete(RoShamBo it) {
        return table.get(this).get(it);
    }

    public static void main(String[] args) {
        System.out.println(ROCK.complete(SCISSORS));
    }

}

complete方法实现了2次分发

使用二维数组

  • 简单,速度快,代码易懂,但是组数比较大时尺寸容易错
private static OutCome[][] tables = {
        {DRAW, LOSE, WIN},
        {WIN, DRAW, LOSE},
        {LOSE, WIN, DRAW},
    };

    public OutCome completes (RoShamBo other) {
        return tables[this.ordinal()][other.ordinal()];
    }

8 知识点

可以静态导入枚举类 直接使用枚举实例 import static ......Color.* 最好使用静态导入省去写enum类

9 参考文章

《Java编程思想》 -- 笔记

《Java编程思想》笔记 第十九章 枚举类型

浅谈在Java开发中的枚举的作用和用法

相关文章
|
22天前
|
安全 Java 测试技术
🎉Java零基础:全面解析枚举的强大功能
【10月更文挑战第19天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
103 60
|
15天前
|
存储 Java 开发者
Java 中 Set 类型的使用方法
【10月更文挑战第30天】Java中的`Set`类型提供了丰富的操作方法来处理不重复的元素集合,开发者可以根据具体的需求选择合适的`Set`实现类,并灵活运用各种方法来实现对集合的操作和处理。
|
15天前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
33 2
|
26天前
|
存储 安全 Java
深入理解Java中的FutureTask:用法和原理
【10月更文挑战第28天】`FutureTask` 是 Java 中 `java.util.concurrent` 包下的一个类,实现了 `RunnableFuture` 接口,支持异步计算和结果获取。它可以作为 `Runnable` 被线程执行,同时通过 `Future` 接口获取计算结果。`FutureTask` 可以基于 `Callable` 或 `Runnable` 创建,常用于多线程环境中执行耗时任务,避免阻塞主线程。任务结果可通过 `get` 方法获取,支持阻塞和非阻塞方式。内部使用 AQS 实现同步机制,确保线程安全。
|
1月前
|
Java 编译器
Java“返回类型为 void 的方法不能返回一个值”解决
在 Java 中,如果一个方法的返回类型被声明为 void,那么该方法不应该包含返回值的语句。如果尝试从这样的方法中返回一个值,编译器将报错。解决办法是移除返回值语句或更改方法的返回类型。
|
1月前
|
设计模式 Java
Java“不能转换的类型”解决
在Java编程中,“不能转换的类型”错误通常出现在尝试将一个对象强制转换为不兼容的类型时。解决此问题的方法包括确保类型间存在继承关系、使用泛型或适当的设计模式来避免不安全的类型转换。
|
1月前
|
Java
Java 中锁的主要类型
【10月更文挑战第10天】
|
1月前
|
Java 程序员 编译器
Java中的异常类型
Java中的异常类型
23 3
|
1月前
|
Java 开发者
Java“类 Y 中的方法 X 不能应用于给定类型”解决
在Java中遇到“类Y中的方法X无法应用于给定类型”的错误时,通常是因为方法调用时的参数类型与定义不符。解决此问题需检查方法签名,确保传递的参数类型正确无误,或使用显式类型转换以匹配方法所需的参数类型。这种错误提示帮助开发者及时修正类型不匹配的问题。
|
1月前
|
Java 编译器
Java“无效的方法声明;需求返回类型”怎解决
要解决Java中的“无效的方法声明;需要返回类型”错误,需为方法指定正确的返回类型。检查方法签名,添加如`void`、`int`、`String`等类型,并确保方法体内正确使用`return`语句。这能帮助Java编译器理解和验证方法的行为。遵守这些规则,可以避免语法错误并确保程序正常运行。