Java 枚举(Enum)学习笔记(枚举与反射关系)

一、什么是枚举?

枚举(Enumeration)是 Java 5 引入的一种特殊数据类型,本质是单例的集合,用于定义一组固定的、有限的常量值。

  • 核心特点:枚举中的每个值都是枚举类的一个实例,且是唯一的(单例)。
  • 适用场景:表示固定范围的取值,比如星期(周一到周日)、季节(春夏秋冬)、支付方式(微信 / 支付宝 / 银行卡)等。

1.1 基础定义(最简单的枚举)

// 定义一个表示季节的枚举
public enum Season {
    // 枚举常量(每个常量都是 Season 的实例,逗号分隔,分号结尾)
    SPRING, SUMMER, AUTUMN, WINTER
}

二、枚举的核心用法

2.1 枚举的基本使用

public class EnumBasicDemo {
    public static void main(String[] args) {
        // 1. 直接使用枚举常量
        Season currentSeason = Season.SPRING;
        
        // 2. 输出枚举值(默认打印常量名)
        System.out.println(currentSeason); // 输出:SPRING
        
        // 3. 枚举的常用方法
        // 3.1 name():返回枚举常量的名称(字符串)
        System.out.println(currentSeason.name()); // 输出:SPRING
        
        // 3.2 ordinal():返回枚举常量的序号(从 0 开始)
        System.out.println(currentSeason.ordinal()); // 输出:0
        
        // 3.3 values():返回所有枚举常量的数组(遍历枚举常用)
        Season[] allSeasons = Season.values();
        for (Season s : allSeasons) {
            System.out.println(s + " - 序号:" + s.ordinal());
        }
        
        // 3.4 valueOf():根据名称获取枚举常量(大小写敏感,不存在则抛 IllegalArgumentException)
        Season summer = Season.valueOf("SUMMER");
        System.out.println(summer); // 输出:SUMMER
    }
}

2.2 带属性和方法的枚举(进阶)

枚举本质是,因此可以拥有属性、构造方法、普通方法和抽象方法。

示例:带描述的季节枚举
public enum Season {
    // 枚举常量(调用构造方法初始化属性)
    SPRING("春天", "春暖花开"),
    SUMMER("夏天", "烈日炎炎"),
    AUTUMN("秋天", "秋高气爽"),
    WINTER("冬天", "寒风刺骨");

    // 枚举的属性
    private final String chineseName; // 中文名称
    private final String desc; // 描述

    // 枚举的构造方法(必须是 private,默认也是 private,不能省略)
    private Season(String chineseName, String desc) {
        this.chineseName = chineseName;
        this.desc = desc;
    }

    // 普通方法:获取属性
    public String getChineseName() {
        return chineseName;
    }

    public String getDesc() {
        return desc;
    }

    // 自定义方法:根据中文名称查找枚举
    public static Season getByChineseName(String chineseName) {
        for (Season s : Season.values()) {
            if (s.chineseName.equals(chineseName)) {
                return s;
            }
        }
        return null; // 未找到返回 null
    }

    // 重写 toString 方法(可选,默认返回常量名)
    @Override
    public String toString() {
        return "[" + name() + "]" + chineseName + ":" + desc;
    }
}

// 测试类
public class EnumAdvanceDemo {
    public static void main(String[] args) {
        Season spring = Season.SPRING;
        System.out.println(spring.getChineseName()); // 输出:春天
        System.out.println(spring.getDesc()); // 输出:春暖花开
        System.out.println(spring); // 输出:[SPRING]春天:春暖花开

        Season winter = Season.getByChineseName("冬天");
        System.out.println(winter); // 输出:[WINTER]冬天:寒风刺骨
    }
}

2.3 枚举的 switch 用法(推荐)

枚举非常适合用在 switch 语句中,代码更清晰、不易出错(避免魔法值)

public class EnumSwitchDemo {
    public static void main(String[] args) {
        Season season = Season.SUMMER;
        
        switch (season) {
            case SPRING:
                System.out.println("适合春游");
                break;
            case SUMMER:
                System.out.println("适合游泳");
                break;
            case AUTUMN:
                System.out.println("适合秋游");
                break;
            case WINTER:
                System.out.println("适合滑雪");
                break;
            // 无需 default(枚举值固定,编译器会检查)
        }
    }
}

2.4 枚举实现接口

枚举可以实现接口(但不能继承类,因为枚举默认继承 java.lang.Enum,Java 单继承)

// 定义接口
interface Describable {
    String getDescription();
}

// 枚举实现接口
public enum Season implements Describable {
    SPRING("春天", "春暖花开"),
    SUMMER("夏天", "烈日炎炎"),
    AUTUMN("秋天", "秋高气爽"),
    WINTER("冬天", "寒风刺骨");

    private final String chineseName;
    private final String desc;

    private Season(String chineseName, String desc) {
        this.chineseName = chineseName;
        this.desc = desc;
    }

    // 实现接口方法
    @Override
    public String getDescription() {
        return chineseName + "的特点是:" + desc;
    }
}

// 测试
public class EnumInterfaceDemo {
    public static void main(String[] args) {
        for (Season s : Season.values()) {
            System.out.println(s.getDescription());
        }
    }
}

三、枚举的核心特性

  1. 不可继承:枚举默认继承 java.lang.Enum,而 Java 是单继承,因此枚举不能继承其他类。
  2. 构造方法私有:枚举的构造方法只能是 private(显式 / 隐式),无法通过 new 创建实例,保证枚举常量的唯一性。
  3. 线程安全:枚举的每个常量在类加载时初始化,JVM 保证类加载是线程安全的,因此枚举天然线程安全。
  4. 可序列化:枚举实现了 Serializable 接口,序列化时不会创建新实例,保证单例特性。
  5. 可用于集合:Java 提供了 EnumSet(高效存储枚举的集合)和 EnumMap(键为枚举的 Map),性能优于普通集合。

四、枚举 vs 普通常量(为什么用枚举?)

对比项普通常量(public static final)枚举
类型安全无(可能传入非法值)有(只能使用枚举定义的常量)
可读性差(需要记住常量名)好(语义清晰)
扩展能力差(新增常量需修改多处)好(可添加属性 / 方法)
switch 支持支持但易出错支持且编译器检查
// 普通常量(存在风险:可以传入任意 int 值,比如 5,编译器不报错)
public class SeasonConstant {
    public static final int SPRING = 0;
    public static final int SUMMER = 1;
    public static final int AUTUMN = 2;
    public static final int WINTER = 3;

    public static void printSeason(int season) {
        switch (season) {
            case SPRING:
                System.out.println("春天");
                break;
            // 省略其他 case
            default:
                System.out.println("未知季节");
        }
    }
}

// 调用时传入非法值,编译器不报错
SeasonConstant.printSeason(5); // 输出:未知季节

总结

  1. 核心本质:Java 枚举是特殊的类,每个枚举常量都是该类的唯一实例,用于定义固定范围的常量。
  2. 核心优势:相比普通常量,枚举提供类型安全、更好的可读性和扩展能力,是定义常量的首选方式。
  3. 关键用法:基础定义、带属性 / 方法的枚举、switch 中使用、实现接口,掌握这些即可覆盖大部分开发场景。
    1. 五、枚举与反射的特别注意事项

      反射是 Java 中动态操作类 / 对象的机制,但枚举的设计天然限制了反射的某些操作,核心原因是:JVM 为了保证枚举常量的唯一性,对反射创建枚举实例做了严格禁止

      5.1 反射无法创建枚举实例(核心限制)

      1. 现象:调用 Constructor.newInstance() 创建枚举实例会抛异常

      枚举的构造方法虽然是 private,但即使通过反射获取构造方法并设置为可访问,也无法创建新实例,JVM 会直接抛出 IllegalArgumentException

      2. 代码示例(验证反射创建枚举实例失败)
  4. import java.lang.reflect.Constructor;
    
    public class EnumReflectDemo {
        public static void main(String[] args) {
            try {
                // 1. 获取 Season 枚举的 Class 对象
                Class<Season> seasonClass = Season.class;
                
                // 2. 获取枚举的构造方法(枚举的构造方法参数:String, int, 自定义属性...)
                // 注意:枚举默认的构造方法除了自定义参数,还隐含 String(name)和 int(ordinal)两个参数
                Constructor<Season> constructor = seasonClass.getDeclaredConstructor(
                    String.class, int.class, String.class, String.class
                );
                
                // 3. 设置构造方法可访问(突破 private 限制)
                constructor.setAccessible(true);
                
                // 4. 尝试创建枚举实例(此处会抛出异常)
                Season fakeSeason = constructor.newInstance(
                    "FAKE", 4, "假季节", "不存在的季节"
                );
                
            } catch (Exception e) {
                // 异常类型:IllegalArgumentException,提示「Cannot reflectively create enum objects」
                e.printStackTrace();
            }
        }
    }
    3. 异常原因

    查看 Constructor.newInstance() 的源码可知,JVM 会先判断当前类是否是枚举类型:

    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");

    这是 Java 层面的强制限制,目的是保证枚举常量的唯一性(枚举的实例只能是定义时的那些常量,不能动态新增)。

    5.2 反射可以操作枚举的其他内容(合法场景)

  5. 虽然不能创建枚举实例,但反射可以:

  6. 获取枚举的所有常量(等价于 values() 方法);
  7. 获取 / 调用枚举的属性、普通方法;
  8. 获取枚举的构造方法(但无法调用 newInstance())。
  9. import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    
    public class EnumReflectLegalDemo {
        public static void main(String[] args) throws Exception {
            Class<Season> seasonClass = Season.class;
            
            // 1. 反射获取枚举的所有常量(等价于 Season.values())
            Field[] fields = seasonClass.getDeclaredFields();
            for (Field field : fields) {
                if (field.isEnumConstant()) { // 判断是否是枚举常量
                    System.out.println("枚举常量:" + field.getName());
                }
            }
            
            // 2. 反射调用枚举的普通方法
            Season spring = Season.SPRING;
            Method getDescMethod = seasonClass.getDeclaredMethod("getDesc");
            String desc = (String) getDescMethod.invoke(spring);
            System.out.println("Spring 的描述:" + desc); // 输出:春暖花开
            
            // 3. 反射获取枚举的属性(即使是 private)
            Field chineseNameField = seasonClass.getDeclaredField("chineseName");
            chineseNameField.setAccessible(true);
            String chineseName = (String) chineseNameField.get(spring);
            System.out.println("Spring 的中文名称:" + chineseName); // 输出:春天
        }
    }

    5.3 枚举反射的实战注意点

  10. 不要试图绕过枚举的反射限制:JVM 的限制是无法通过常规手段突破的,强行尝试(如修改字节码)会破坏枚举的设计初衷,导致程序不稳定;
  11. 枚举的单例安全性:正因为反射无法创建枚举实例,枚举是实现单例模式的最佳方式(比懒汉式 / 饿汉式更安全,避免反射攻击);
  12. 序列化 + 反射的双重保障:枚举序列化时,不会序列化实例本身,只会序列化常量名;反序列化时,直接从 JVM 中获取已有的枚举实例,而非创建新实例,结合反射限制,彻底保证单例。
  13. // 枚举单例(天然线程安全、防反射、防序列化)
    public enum Singleton {
        INSTANCE; // 唯一实例
        
        // 单例的业务方法
        public void doSomething() {
            System.out.println("枚举单例执行方法");
        }
    }
    
    // 使用
    public class SingletonDemo {
        public static void main(String[] args) {
            Singleton instance1 = Singleton.INSTANCE;
            Singleton instance2 = Singleton.INSTANCE;
            System.out.println(instance1 == instance2); // 输出:true(单例)
        }
    }

    总结

  14. 核心本质:Java 枚举是特殊的类,每个枚举常量都是该类的唯一实例,用于定义固定范围的常量;相比普通常量,枚举提供类型安全、更好的可读性和扩展能力。
  15. 反射限制:JVM 禁止通过反射创建枚举实例(抛 IllegalArgumentException),但可反射获取 / 调用枚举的属性、方法。
  16. 实战价值:枚举的反射限制使其成为实现单例模式的最佳方式,天然防反射攻击、线程安全且支持序
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值