文章目录

  • 1. 为什么需要枚举
  • 2. 枚举类型的特性和使用
    • 2.1. 定义枚举
    • 2.2. 枚举特性–定义实例变量、实例代码块、实例方法和构造方法
    • 2.3. 枚举特性–定义静态变量、静态代码块以及静态方法
    • 2.4. 枚举特性–枚举不能继承其他类或枚举,也不可以被继承,但是可以实现接口
    • 2.5. 枚举特性–可以用作switch语句
    • 2.6. 枚举特性–enum不可以作为数据类型
    • 2.7. 枚举特性–enum可以声明抽象方法
    • 2.8. 枚举的常用方法
    • 2.9. 枚举的常用工具类库
  • 3. 枚举的实现原理
  • 4. 参考文章

1. 为什么需要枚举

在JDK1.5之前,我们要是想定义一些有关常量的内容,例如定义几个常量,表示从周一到周末,一般都是在一个类,或者一个接口中,写类似于如下代码:

public class WeekDayConstant { 
    public static final int MONDAY = 0;
    public static final int TUESDAY = 1;
    public static final int WEDNESDAY = 2;
    public static final int THURSDAY = 3;
    //...
}

这样做也可以实现功能,有几个缺点:

  • 各个常量的值可能会一样,出现混淆,例如不小心把TUESDAY 定义为0
  • 使用起来并不是很方便,例如想要获取某一种枚举的所有枚举值列表,根名称获取值等,还要去编码实现
  • 并不是很安全,例如反射修改常量的值,MONDAY 的值可能被修改为1
  • 方式并不是很优雅

为了不重复造轮子,Java组织在JDK1.5的时候,引入了枚举enum关键字(enum就是enumeration的缩写),我们可以定义枚举类型。

2. 枚举类型的特性和使用

2.1. 定义枚举

定义的格式是:

访问修饰符 enum 枚举类型名称{ 
一个或多个枚举值定义,一般采用大写加下划线的方式,用英文逗号分隔,例如,
A,B,C;
在最后一个枚举值后面建议加一个分号,对于与只有枚举值的枚举定义来说,可以没有分号
后面就是一些方法的定义
}

例如,周一到周末的枚举定义:

public enum WeekDay { 
    MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY;
}

可以单独定义一个文件,也可以定义在其他类的文件中。

2.2. 枚举特性–定义实例变量、实例代码块、实例方法和构造方法

在枚举中可以定义自己的构造方法,有参数无参数都可以,例如

//定义功课的枚举,每一门功课都有一个老师,通过构造方法传入
public enum Subject { 
    CHINESE("王老师"),
    MATH("张老师"),
    ENGLISH("Mr. Yang");

    private String teacherName;
    
 	//枚举的构造方法访问权限默认是私有的,因为枚举是常量,各个枚举常量,枚举值的个数在编译期间就定义好了的,在运行时不能被改变
    private Subject(String teacherName) { 
        if (teacherName != null && !teacherName.isEmpty())
            this.teacherName = teacherName;
    }
    
    public String getTeacherName() { 
        return teacherName;
    }

    public void setTeacherName(String teacherName) { 
        this.teacherName = teacherName;
    }
	//实例代码块
    { 
        teacherName = "未知";
    }
}

注意点:

  • 枚举的构造方法访问权限是私有的,否则不会通过编译
  • 如果构造方法有参数,每一个枚举值需要提供参数的值,具体为什么,后面会讲解

2.3. 枚举特性–定义静态变量、静态代码块以及静态方法

这个就不做举例了,大家可以自己验证。

2.4. 枚举特性–枚举不能继承其他类或枚举,也不可以被继承,但是可以实现接口

具体为什么不能被继承或继承其他类,后面解释

2.5. 枚举特性–可以用作switch语句

在JDK1.7以后,可以在switch中使用枚举,使用时,case语句中不能使用枚举类型名称.枚举值的方式,编译不会通过,例如:

直接写枚举常量的名称即可。

2.6. 枚举特性–enum不可以作为数据类型

enum实质上是java编译器的语法糖,他不是一种数据类型,因此定义变量,函数形参定义,不可以用它做数据类型表示;

其实所有的枚举都是继承一个java.lang.Enum抽象类的,所以枚举是类类型,不是一个新的类型,

2.7. 枚举特性–enum可以声明抽象方法

public enum Subject { 
    CHINESE("王老师"){ 
        @Override
        int score() { 
            return 80;
        }
    },
    MATH("张老师"){ 
        @Override
        int score() { 
            return 100;
        }
    },
    ENGLISH("Mr. Yang") { 
        @Override
        int score() { 
            return 50;
        }
    };

    private String teacherName;

    private Subject(String teacherName) { 
        if (teacherName != null && !teacherName.isEmpty())
            this.teacherName = teacherName;
    }

    { 
        teacherName = "未知";
    }

    abstract int score();
}

定义抽象方法时,每一个枚举常量实现该抽象方法

2.8. 枚举的常用方法

  • name(),是一个实例方法,该方法在java.lang.Enum中,返回枚举的名称,枚举的名称就是定义枚举常量时用的字符串,该方法被final修饰,因此不能被重写
  • values(),是一个静态方法,按照声明的顺序返回枚举类中定义的所有枚举常量组成的数组

    这个方法是一个隐含的方法,由编译器生成的。

  • valueOf(String),是一个静态方法,它根据一个名称返回一个枚举常量,

    如果名称所表示的枚举常量不存在,则抛出java.lang.IllegalArgumentException异常。这个方法是一个隐含的方法,由编译器生成的,对于一个具体的枚举类来说,这个方法是有的,但是java.lang.Enum中没有这个方法。

  • valueOf(Class,String),是一个静态的方法,存在于java.lang.Enum中,它的作用跟上一个方法类似,只不过第一个参数是Class类型的,需要指定获取那个类型的常量,第二个参数是常量的名称。
  • getDeclaringClass() ,是一个实例方法,存在于java.lang.Enum中,可以获取代表当前枚举类型的Class对象,被final关键字修饰,不能被重写
  • ordinal(),是一个实例方法,返回当前枚举常量的序号,序号是在枚举类中声明的顺序,从0开始,最大值是java.lang.Integer.MAX_VALUE,被final关键字修饰,不能被重写
  • toString(),是一个实例方法,来自于java.lang.Object,在java.lang.Enum的实现是直接返回了name属性,就是name()方法的返回值,这个方法可以被重写。
  • compareTo(E) 是一个实例方法,java.lang.Enum类实现了Comparable接口,用于比较当前枚举实例和指定的枚举实例,可以看一下它的实现

    这是在JDK1.8中的实现版本,如果两个枚举实例的类型都不一样,直接会怕抛出异常,否则比较的是他们的ordinal值,这个值是ordinal()方法的返回值,由于这个方法由final修饰,因此不能被重写。

  • Enum(String , int ),这是java.lang.Enum类的构造方法,在JDK1.8中,它的实现是这样的:

    这个方法程序员不能直接调用的,由编译器生成代码调用的,也就是说,编译器会为我们自定义的枚举类生成构造方法,在生成的构造方法中,调用java.lang.Enum类的构造方法,这个我们会在下面进行验证。

2.9. 枚举的常用工具类库

  • EnumSet
    这是个抽象类,是枚举专用的Set接口的抽象实现,通过查看其add方法发现,它不允许null值,否则会抛出空指针异常,获取它的对象一般有如下几种静态工厂方法:

    1. EnumSet noneOf(Class elementType) ,根据elementType类型创建一个空的Set集合,如果传的Class类型不是枚举,就会抛出类转换异常
    2. EnumSet allOf(Class elementType),根据elementType类型创建一个包含elementType所有枚举常量的Set集合,如果传的Class类型不是枚举,就会抛出类转换异常
    3. EnumSet range(E from, E to),返回一个包含ordinal值从from.ordinal()到to.ordinal()的所有枚举常量的Set集合(包括from和to),如果from.ordinal()大于to.ordinal(),则抛出IllegalArgumentException异常
    4. EnumSet complementOf(EnumSet s),返回的是所有E类型中不包含在s中的枚举常量的Set集合(补集),s不能为null
    5. EnumSet of(E e),返回一个Set集合包含e,当然这个e不能为null,否则抛出空指针异常,这个方法有5个重载的版本,分别可以传1-5个Enum参数
    6. EnumSet of(E first, E… rest) 这是上个方法的可变参数版本
    7. EnumSet copyOf(EnumSet s),创建一个包含s中所有元素的副本,调用clone方法实现的
    8. EnumSet copyOf(Collection c),创建一个包含c中所有枚举实例的Set,c不能为空集合,否则抛出IllegalArgumentException异常

    除了这些工厂方法之外,还有基本的add,addAll,remove,removeAll,contains,containsAll
    ,clear,equals等基本方法,就不一一介绍了,值得注意的是,EnumSet的实现类存储Enum对象的方式是采用一个long类型的整数存储的,使用位运算,基本操作在常数时间内完成,效率很高,有兴趣的同学可以去看一下EnumSet的源码。

  • EnumMap<K extends Enum, V>
    这是一个Key类型是枚举的特定Map接口实现,不允许null Key,但允许null值,线程不安全的。获取这个类的对象可以直接使用其构造方法:

    1. public EnumMap(Class keyType) keyType显然必须是枚举类类型,返回一个空的Map集合
    2. public EnumMap(EnumMap<K, ? extends V> m) 传入一个EnumMap实例,返回一个副本

其他增删改查的方法跟HashMap基本是一样的,EnumMap把Key和Value都使用数组的方式管理起来,根据Key的ordinal序号,作为Value数组的下表来访问Key对应的值,实现很简洁,效率很高。

3. 枚举的实现原理

先从简单的来,我现在把我们上面使用的Subject枚举类改成下面的样子:

public enum Subject { 
    CHINESE,
    MATH,
    ENGLISH;
}

切换到class所在的目录,然后:

javap -c Subject.class

javap是JDK自带的反汇编工具,得到下面的结果:

Compiled from "Subject.java"
public final class com.victory.test.object_size.Subject extends java.lang.Enum<com.victory.test.object_size.Subject> { 
  public static final com.victory.test.object_size.Subject CHINESE;

  public static final com.victory.test.object_size.Subject MATH;

  public static final com.victory.test.object_size.Subject ENGLISH;

  public static com.victory.test.object_size.Subject[] values();
    Code:
       0: getstatic     #1 // Field $VALUES:[Lcom/victory/test/object_size/Subject;
       3: invokevirtual #2 // Method "[Lcom/victory/test/object_size/Subject;".clone:()Ljava/lang/Object;
       6: checkcast     #3 // class "[Lcom/victory/test/object_size/Subject;"
       9: areturn

  public static com.victory.test.object_size.Subject valueOf(java.lang.String);
    Code:
       0: ldc           #4 // class com/victory/test/object_size/Subject
       2: aload_0
       3: invokestatic  #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
       6: checkcast     #4 // class com/victory/test/object_size/Subject
       9: areturn

  static { };
    Code:
       0: new           #4 // class com/victory/test/object_size/Subject
       3: dup
       4: ldc           #7 // String CHINESE
       6: iconst_0
       7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
      10: putstatic     #9 // Field CHINESE:Lcom/victory/test/object_size/Subject;
      13: new           #4 // class com/victory/test/object_size/Subject
      16: dup
      17: ldc           #10 // String MATH
      19: iconst_1
      20: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
      23: putstatic     #11 // Field MATH:Lcom/victory/test/object_size/Subject;
      26: new           #4 // class com/victory/test/object_size/Subject
      29: dup
      30: ldc           #12 // String ENGLISH
      32: iconst_2
      33: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
      36: putstatic     #13 // Field ENGLISH:Lcom/victory/test/object_size/Subject;
      39: iconst_3
      40: anewarray     #4 // class com/victory/test/object_size/Subject
      43: dup
      44: iconst_0
      45: getstatic     #9 // Field CHINESE:Lcom/victory/test/object_size/Subject;
      48: aastore
      49: dup
      50: iconst_1
      51: getstatic     #11 // Field MATH:Lcom/victory/test/object_size/Subject;
      54: aastore
      55: dup
      56: iconst_2
      57: getstatic     #13 // Field ENGLISH:Lcom/victory/test/object_size/Subject;
      60: aastore
      61: putstatic     #1 // Field $VALUES:[Lcom/victory/test/object_size/Subject;
      64: return
}

解释:

  • 当我们使用enum关键字定义枚举类时,java编译器会自动帮我们生成一个类,继承自java.lang.Enum,(见第2行)并且生成的这个类是用final修饰的,因此枚举不能被继承,也不能继承其他类了,但是可以实现接口
  • 我们声明的每一个枚举常量,都是Subject类的一个实例(见第3,5,7行)
  • 第9行和第16行是编译器生成的values(),valueOf(String)静态方法
  • 第24行是编译器插入的静态代码块,后面是静态代码块的JVM指令,举例分析一下
    • 第26行是使用new指令创建了一个Subject类的一个对象,
    • 28行 ldc #7是把常量池中7号位置的常量压入操作数栈,即把CHINESE这个字符串的引用压入操作数栈
    • 第29行,iconst_0指令是把数字0压入操作数栈
    • 第30行invokespecial 是方法调用指令,可以看出,调用的是Subject类的<init>方法,<init>方法也就是构造方法,参数是(Ljava/lang/String;I)V,即一个String,一个整数,返回void。这个方法是编译器生成的,invokespecial 指令会把操作数栈的栈顶三个操作数依次弹出,然后执行init方法,通过分析我们知道依次弹出的是0,CHINESE,new出来的Subject类的对象的引用,其实这个0就是序号,即上面所说的ordinal()方法的返回值。
    • 对于Subject类的构造方法,通过jclasslib等字节码分析工具,可以看到其方法签名如下:

      其字节码指令如下:
      java的每一个函数都有一个局部变量表,对于实例方法,局部变量表中第一个槽位放置的是当前对象的引用this,后面放置的依次是各个形参的引用,当调用方法的时候,形参引用的值被替换成实际的引用;
      aload_0是把局部变量表的第一个槽位的值放到操作数栈,就是this,
      aload_1是把局部变量表的第二个槽位的值放到操作数栈,就是String参数的引用,这个引用就是前面我们看到的枚举常量的名称,
      iload_2把局部变量表的第三个槽位的整数值放到操作数栈,就是序号,ordinal,
      invokespecial就是调用java.lang.Enum的init方法,即父类的构造方法,在这里我们验证了前面我们所说的编译器生成自定义枚举类的构造方法,并调用了java.lang.Enum类的构造方法。对于JVM指令不太了解的同学可以移步查看Oracle官方文档:JVM指令集参考

  • 从第28行到第30行,是创建一个对象的过程,可以看出,在静态代码块中这样的过程执行了3次,也就是说,有几个枚举常量,就会调用几次枚举类的构造方法
  • 如果自己定义了构造方法,例如在Subject类中做如下改动,
public enum Subject { 
    CHINESE("王老师"),
    MATH("张老师"),
    ENGLISH("Mr. Yang");

    private String teacherName;
    Subject(String teacherName){ 
        this.teacherName=teacherName;
    }
}

再次编译,反汇编,看到的构造函数如下:

可以看到,编译器会把自己定义的参数,加在原来的构造方法后面。

  • 对于含有抽象方法的情况,例如改成这样:
public enum Subject { 
    CHINESE("王老师"){ 
        @Override
        int score() { 
            return 1;
        }
    },
    MATH("张老师"){ 
        @Override
        int score() { 
            return 2;
        }
    },
    ENGLISH("Mr. Yang"){ 
        @Override
        int score() { 
            return 3;
        }
    };

    private String teacherName;
    Subject(String teacherName){ 
        this.teacherName=teacherName;
    }

    abstract int score();
}

有几个枚举常量,会生成几个Subject的内部类,

而Subject是一个继承了java.lang.Enum的抽象类,每一个内部类都继承自Subject,有兴趣的读者可以自己去验证。
最后值得一提的是枚举有很好的安全性,由于枚举在编译期间就确定了枚举常量的个数,因此JDK保证在运行时不能创建枚举对象,即使是使用反射,序列化和反序列化等手段也不行,因此枚举可以在某些要求安全性高的地方用作单例模式的实现方案。

到这里有关Java枚举的内容就总结完成了,你学会了吗?如果有什么遗漏或错误,欢迎各位大神不吝赐教。

4. 参考文章

深入理解Java枚举类型(enum)

本文地址:https://blog.csdn.net/qq_37684467/article/details/114225123