谈Java语言规范之枚举类型


这不是一顿快餐,希望你沉淀下来,细细品尝


写在前面

枚举类型可以考虑用来替换接口中的常量声明。并且 《Effective Java》建议不要用“常量接口模式”,而是在类中声定义常量。可实际上在接口中定义常量往往更加简洁。


枚举类默认继承了 java.lang.Enum 类。


枚举类型

枚举声明指定一个新的枚举类型, 一种特殊的类的类型。

EnumDeclaration:
{ClassModifier} enum Identifier [Superinterfaces] EnumBody

如果枚举声明的类修饰符有`abstract` 或者 `final` 将产生编译时错误。

灵魂拷问:为什么不能显式声明为abstract 或者 final 的?参考推荐博文中的第一篇,思考其所说的反编译结果


枚举声明是隐式final,除非它包含至少一个具有类主体的枚举常量。

public enum Student {

 GOOD(){
     @Override
     public void action() {

     }
 };

 public abstract void action();
}

// 反编译如下 (javap -p命令)
Compiled from "Student.java"
public abstract class com.gysoft.utils.Student extends java.lang.Enum<com.gysoft.utils.Student> {
public static final com.gysoft.utils.Student GOOD;
private static final com.gysoft.utils.Student[] $VALUES;
public static com.gysoft.utils.Student[] values();
public static com.gysoft.utils.Student valueOf(java.lang.String);
private com.gysoft.utils.Student();
public abstract void action();
com.gysoft.utils.Student(java.lang.String, int, com.gysoft.utils.Student$1);
static {};
}

哪怕反编译出来的类是抽象的,我们依然不能显示的将其声明为abstract的,当然,此时它肯定不是final的了。


嵌套枚举类型是隐式静态的。嵌套枚举类型的声明允许冗余地指定静态修饰符。

灵魂拷问:为什么是静态的?因为非静态的内部类对象需要依赖外部类对象而存在,通过反编译的结果知道,编译器创建了Student 对象,如果是非静态内部类,它无法限制外部类的创建条件,最终也就无法构造 Student 对象。其实从存在的意义上来看,那并不是枚举类该有的


这意味着不可能在内部类(这里的内部类指的是我们常说的非静态的内部类)的主体中声明枚举类型,因为内部类除了常量变量外不能有静态成员。




枚举类型除了由其枚举常量定义的实例外,没有其他实例。试图显式实例化枚举类型(§15.9.1)是编译时错误。


枚举类型的构造方法只能是私有的


除了编译时错误之外,还有三种机制确保枚举类型的实例不存在于枚举常量定义的实例之外:

  1. Enum中的最后一个克隆方法确保永远不能克隆Enum常量。
protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

  1. 禁止枚举类型的反射实例化。
public enum  Student {

    GOOD;

    public static void main(String[] args) {

        try {
            Student.GOOD.getDeclaringClass().newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

//异常最终会从 Class 类的 如下方法抛出
/*
** parameterTypes 是一个{},但constructor(private 
** com.gysoft.utils.Student(java.lang.String,int))能够获取到两个参数类型,无法与传入** 的parameterTypes 匹配上。导致无法返
** 回,最终抛出异常。
*/
private Constructor<T> getConstructor0(Class<?>[] parameterTypes,
                                        int which) throws NoSuchMethodException
    {
        Constructor<T>[] constructors = privateGetDeclaredConstructors((which == Member.PUBLIC));
        for (Constructor<T> constructor : constructors) {
            if (arrayContentsEq(parameterTypes,
                                constructor.getParameterTypes())) {
                return getReflectionFactory().copyConstructor(constructor);
            }
        }
        throw new NoSuchMethodException(getName() + ".<init>" + argumentTypesToString(parameterTypes));
    }
 

  1. 序列化机制的特殊处理确保不会因为反序列化而创建重复的实例
	/**
        * prevent default deserialization
     */
    private void readObject(ObjectInputStream in) throws IOException,
        ClassNotFoundException {
        throw new InvalidObjectException("can't deserialize enum");
    }

    private void readObjectNoData() throws ObjectStreamException {
        throw new InvalidObjectException("can't deserialize enum");
    }


一. 枚举常量

枚举声明的主体能够包含枚举常量,一个枚举常量定义为一个枚举类型的实例。(参考上文反编译后的结果)

EnumBody:
{ [EnumConstantList] [,] [EnumBodyDeclarations] }

EnumConstantList:
EnumConstant {, EnumConstant}

EnumConstant:
{EnumConstantModifier} Identifier [( [ArgumentList] )] [ClassBody]

EnumConstantModifier:
Annotation

需要注意的是枚举常量的修饰符只能是注解类型,以下是一个稍微复杂的枚举类:

public enum Student {
	
    @Annotation GOOD("zhangsan",180){
        @Override
        void say() {
            System.out.println("我是"+name+",身高"+height);
        }
    };

     String name;
     int height;

    Student(String name,int height){
        this.name = name;
        this.height = height;
    }

    abstract void say();
}


// 下面这种写法也是合法的,但是你无法调用 say 方法
public enum Student {

    @Annotation GOOD("zhangsan",180){
        void say() {
            System.out.println("我是"+name+",身高"+height);
        }
    };

     String name;
     int height;

    Student(String name,int height){
        this.name = name;
        this.height = height;
    }
    
}

在枚举常量的类主体隐式定义了一个匿名类声明,它拓展了立即封闭的枚举类型。类体受匿名类的一般规则控制;特别是它不能包含任何构造函数。想要调用类主体中声明的实例方法,前提是该方法是覆盖的枚举类型中的可访问方法。


枚举常量的类主体声明一个abstract 方法将产生编译时错误。


这是因为每个枚举常量仅仅只有一个实例,所以当比较两个对象引用时,如果知道其中至少有一个是引用的枚举常量,则允许使用 == 操作符代替equals方法。

Enum中的equals方法是一个final方法:

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

二.枚举主体声明

除了枚举常量,枚举声明的主体能够包含构造器和成员声明 以及实例和静态初始化器。

EnumBodyDeclarations:
; {ClassBodyDeclaration}

ClassBodyDeclaration:
	ClassMemberDeclaration 
	InstanceInitializer 
	StaticInitializer 
	ConstructorDeclaration
	
ClassMemberDeclaration:
	FieldDeclaration 
	MethodDeclaration 
	ClassDeclaration 
	InterfaceDeclaration 
	;

一个符合以上规格的类如下:

public enum Student {

   /**
    *
    */
   @Annotation GOOD("zhangsan",180){

       void say() {
           System.out.println("我是"+name+",身高"+height);
       }
   },
   BAD("lisi",178)
   ;


    String name;
    int height;

   {
       System.out.println("实例初始化器执行!");
   }

   static{
       System.out.println("静态初始化器执行!");
   }

   Student(String name,int height){
       this.name = name;
       this.height = height;
       System.out.println("构造函数执行!");
   }

   public static void main(String[] args) {
       System.out.println("main 方法开始");
   }

}

// 执行 main 方法 输出如下:
实例初始化器执行!
构造函数执行!
实例初始化器执行!
构造函数执行!
静态初始化器执行!
main 方法开始

枚举声明的主体中的任何构造函数或成员声明都适用于枚举类型,就像它们曾经出现在普通类声明主体中一样,除非另有明确说明。


  • 在枚举声明里的构造函数声明为 public 或者 protected 会产生编译时错误。

  • 在枚举声明里的构造函数声明包含了父类构造函数调用 会产生编译时错误。

// 尽管Enum 中的构造函数访问控制修饰符为 protected
protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
}
// 只是不能调用 super(), 像super.hashcode() 等方法可以正常调用
  • 从枚举类型的构造函数、实例初始化器或实例变量初始化器表达式引用枚举类型的静态字段是编译时错误,除非该字段是常量变量。结合前文运行结果,以及后面的对枚举常数自我引用的限制可以理解。
public static final String ss = new String("ss"); // 编译时错误
// public static final String ss = "ss" 编译正常
String name;
int height;

{
       System.out.println("实例初始化器执行!"+ss);
 }
  • 如果枚举声明有一个抽象方法作为成员,除非该枚举类至少有一个枚举常量,并且该枚举类的所有枚举常量都有提供抽象方法的具体实现的类体。
  • 枚举声明如果 声明finalizer是编译时的错误(§12.6)。枚举类型的实例可能永远不会被终结。



在枚举声明中,没有访问修饰符的构造函数声明是私有的。

在没有构造函数声明的枚举声明中,隐式声明默认构造函数。默认构造函数是私有的,没有正式的参数,也没有throws子句。


对枚举常数自我引用的限制:

如果没有静态字段访问规则,显然合理的代码在运行时才会失败,这是由于枚举类型固有的初始化循环性造成的。(循环性存在于任何具有“自类型”静态字段的类中。)下面是一个会失败的代码的例子:

import java.util.Map;
import java.util.HashMap;

enum Color {
    RED, GREEN, BLUE;
    Color() { colorMap.put(toString(), this); } // 实例初始化器 编译无法通过

    static final Map<String,Color> colorMap =
        new HashMap<String,Color>();
}

此枚举的静态初始化将引发NullPointerException,因为在枚举常量的构造函数运行时,静态变量colorMap未初始化。上面的限制确保不能编译这样的代码。然而,代码可以很容易地重构,以正常工作:

import java.util.Map;
import java.util.HashMap;

enum Color {
    RED, GREEN, BLUE;

    static final Map<String,Color> colorMap =
        new HashMap<String,Color>();
    static {
        for (Color c : Color.values())
            colorMap.put(c.toString(), c);
    }
}

重构版本显然是正确的,因为静态初始化是从上到下进行的。


三.枚举成员

枚举类型 E 的成员都是以下类型:

  • 在 E 的声明主体中声明的成员;

  • Enum<E>继承的成员;

  • E 中声明的每个枚举常量 c , E 都具有类型E 隐式声明的公共静态final字段,该字段具有与 c 相同的名称(参考前文反编译后的结果进行理解)。字段有一个由 c 组成的变量初始化器,并被与 c 相同的注解所注解。


在声明E的主体中显式声明任何静态字段之前,这些字段以与相应枚举常量相同的顺序隐式声明。


枚举常量是在初始化相应的隐式声明字段时创建的。


官网最后列出了几个创建枚举类的例子,可以在最后的参考博文中找到官网地址


推荐博文


java基础加强–实现带有抽象方法的枚举


参考博文


Java8语言规范-枚举

如果你觉得我的文章对你有所帮助的话,欢迎关注我的公众号。赞!我与风来
认认真真学习,做思想的产出者,而不是文字的搬运工。错误之处,还望指出!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值