本文分类:
Java

Java 中 Enum 类使用了自限定泛型,本文通过翻译和解读文章Groking Enum (aka Enum>)来介绍自限定泛型。

查看 JDK 中 Enum 类的定义:

package java.lang;

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
    ......
}

形如 Enum<E extends Enum<E>> 的泛型定义称为自限定的泛型。那么自限定类型有什么用呢?Groking Enum (aka Enum>)这篇文章循序渐进地解释了这个语法现象,下面简单翻译一下这篇文章,Grok的意思是凭直觉理解某事,翻译为意会。

Groking Enum

老虎的 beta 版本已经发布,并且实现了JSR 201。JSR 201引入了新的枚举特性,所以我想玩一玩这个特性,并把结果献给那些敢于直面老虎的人。

注:老虎指的是2014年发布的JDK 1.5.0,该版本代号为Tiger,即老虎,这是Java语言发展时上一个很重要的里程碑版本,为体现这个版本的重要性,原来的J2SE 1.5改名为J2SE 5.0。该版本实现了JSR 201规范,引入了若干新的Java特性,包括枚举、自动装箱、增强格式的循环和静态导入等。可以看出,原作者非常期待这个版本,而且是个幽默感十足的人。

开始

下面是一个如何定义和使用枚举的示例,改编自JSR 201。

public class Card {
    public enum Suit {
        HEART, DIAMOND, CLUB, SPADE
    };

    public enum Rank {
        ACE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING
    };

    private final Suit suit;
    private final Rank rank;

    public Card(Suit suit, Rank rank) {
        this.suit = suit;
        this.rank = rank;
    }

    public Suit getSuit() {
        return suit;
    }

    public Rank getRank() {
        return rank;
    }

    public String toString() {
        return rank + " of " + suit;
    }

    public static void main(String... args) {
        Card upMySleeve = new Card(Suit.HEART, Rank.ACE);
        System.out.println("what is up my sleeve? " + upMySleeve);
        // outputs: what is up my sleeve? ACE of HEART
    }
}

有些语言的枚举仅仅是对整数的封装,但 Java 不一样,上面的代码中,Suit 是一个确确实实的类,事实上,它是 java.lang.Enum 的子类。编译器把 Suit 枚举翻译成有点类似于如下代码,就是在 Suit 中的每个枚举类型常量,都是一个 Suit 的实例,并且在 Suit 中声明为 public static final。

// This is sort of what happens.
public class Suit extends Enum {
    public static final Suit HEART = new Suit("HEART", 1);
    public static final Suit DIAMOND = new Suit("DIAMOND", 2);
    public static final Suit CLUB = new Suit("CLUB", 3);
    public static final Suit SPADE = new Suit("SPADE", 4);
    public Suit(String name, int ordinal) {
        super(name, ordinal);
    }
    // some more stuff here
}

注:下面用实际代码查看反编译后的枚举。以下是枚举类Suit:

public enum Suit {
    HEART, DIAMOND, CLUB, SPADE
};

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name:   Suit.java

public final class Suit extends Enum
{

    private Suit(String s, int i)
    {
        super(s, i);
    }

    public static Suit[] values()
    {
        Suit asuit[];
        int i;
        Suit asuit1[];
        System.arraycopy(asuit = ENUM$VALUES, 0, asuit1 = new Suit[i = asuit.length], 0, i);
        return asuit1;
    }

    public static Suit valueOf(String s)
    {
        return (Suit)Enum.valueOf(com/hiwzc/java/Suit, s);
    }

    public static final Suit HEART;
    public static final Suit DIAMOND;
    public static final Suit CLUB;
    public static final Suit SPADE;
    private static final Suit ENUM$VALUES[];

    static
    {
        HEART = new Suit("HEART", 0);
        DIAMOND = new Suit("DIAMOND", 1);
        CLUB = new Suit("CLUB", 2);
        SPADE = new Suit("SPADE", 3);
        ENUM$VALUES = (new Suit[] {
            HEART, DIAMOND, CLUB, SPADE
        });
    }
}

但是这并不是枚举类的全部,因为当你再去看一下 java.lang.Enum,你会发现 Enum 实际定义成一个泛型类:Enum<E extends Enum<E>>。我的第一反映是:“这他妈的什么意思?”答案且听我慢慢道来,不过在此之前有一点点前奏。

Foo<T> 意味着什么

你的大脑神经元很快会在 List<String> 和“一个存放 String 的列表”之间形成一种关联,但是当你看到 Foo<String>,那是什么意思呢?考虑如下的代码:

public class Foo<T> {         // (A)
    //...
}
Foo<String> f1 = ...;         // (B)
Foo<Integer> f2 = ...;        // (C)

这里主要想说明的是:类型参数使泛型类的使用者在调用泛型方法时不用类型转换。

java.lang.Class<T> 是什么意思

如果你看过 JDK 1.5 的 javadoc 文档,你会发现 java.lang.Class 现在定义成带有类型参数:java.lang.Class<T>。好吧,Class不是集合API的一部分,那我们这里应该建立什么样的神经元关联呢?

这个技巧是用来说明 .class 静态属性上所做的改变:表达式 Foo.class 返回 Class<Foo>,而不像之前仅仅是一个 Class。类型参数实际上告诉用户,这个 class 实际是什么类型,并在使用 Class 的某些方法时避免类型转换。用newInstance()和cast()举个示例,这两个方法都返回了类型T的对象。

Class<Date> c1 = Date.class;
Class c2 = Date.class;
Date d1 = c1.newInstance(); // 新风格,不会出现潜在的ClassCastException
Date d2 = (Date) c2.newInstance(); // 旧风格

Object o = d1;

// 无需手工强制类型转换,cast()方法会在必要的时候抛出ClassCastException
Date d3 = c1.cast(o);

// 旧风格,需要手工强制类型转换
Date d4 = (Date) o;

所以,你的神经元准备好了吗?Class<T> 意思就是“类型 T 的 Class 实例”。

更多类型参数的技巧

请看下面的代码:

abstract class Foo<SubClassOfFoo extends Foo<SubClassOfFoo>> {
    /** 这个方法强制子类返回自身 */
    public abstract SubClassOfFoo subclassAwareDeepCopy();
}

class Bar extends Foo<Bar> {
    public Bar subclassAwareDeepCopy() {
        Bar b = new Bar();
        // ...
        return b;
    }
}

Bar b = new Bar();
Foo<Bar> f = b;
Bar b2 = b.subclassAwareDeepCopy();
Bar b3 = f.subclassAwareDeepCopy(); // no need to cast, return type is Bar

Foo<SubClassOfFoo extends Foo<SubClassOfFoo>>这个语法结构的含义是:

综合上面的分析,Foo类形成了这样一个约定:任何它的子类必需实现subclassAwareDeepCopy方法,并且必须把该方法的返回值声明为子类实际的类型。换句话说:这个用法允许父类(比如抽象工厂)定义参数和返回值都是子类类型的方法。

回到Enum<E extends Enum<E>></h3>

如果你理解了上面的Foo及其子类这样的用法,你就可以理解为什么Enum要定义成Enum<E extends Enum<E>>。因为E用作了 getDeclaringClass() 的返回值和 compareTo() 的参数。这意味着,你能够像下面这样编写代码:a)无需类型转换;b)可以以具体的枚举子类的形式调用Enum类里的方法:

Rank r = Rank.ACE;
Suit s = Suit.HEART;
r.compareTo(s); // syntax error, argument must be of type Rank
Rank z = Enum.valueOf(Rank.class, "TWO");

关于enum更多有意思的内容

方法,字段和构造器

不像仅仅是封装的整数,你可以像类一样给枚举添加字段、方法和构造器,比如

public enum TrafficLightState {
    RED(30), // constructor call
    AMBER(10),
    GREEN(40); // note the semi-colon
    // field
    private final int duration;
    // constructor
    public TrafficLightState(int duration) {
        this.duration = duration;
    }
    // method
    public int getDuration() {
        return duration;
    }
}

常量可以有类体

枚举中的每个常量都可以有自己的类体(就像匿名内部类),该类体创建了这个枚举类型的子类。

public abstract enum TrafficLightState // note the abstract
{
    RED(30) {
        public TrafficLightState next() {
            return GREEN;
        }
    },
    AMBER(10) {
        public TrafficLightState next() {
            return RED;
        }
    },
    GREEN(40) {
        public TrafficLightState next() {
            return AMBER;
        }
    };
    private final int duration;
    public TrafficLightState(int duration) {
        this.duration = duration;
    }
    public int getDuration() {
        return duration;
    }
    public abstract TrafficLightState next();
}

静态导入和枚举常量

只要你愿意,你可以静态导入枚举常量:

import static foo.TrafficLightState.*;
RED.toString();
TrafficLightState state = RED;

编译器如何工作

是这样的,当编译器看到第一个TrafficLightState,编译器将把它转换成类似下面这样的Java类,注意,编译器会:

public class TrafficLightState extends Enum<TrafficLightState> {
    public static final TrafficLightState RED = new TrafficLightState("RED", 1, 30);
    public static final TrafficLightState AMBER = new TrafficLightState("AMBER", 2, 10);
    public static final TrafficLightState GREEN = new TrafficLightState("GREEN", 3, 40);
    private final int duration;
    public TrafficLightState(String name, int ordinal, int duration) {
        super(name, ordinal);
        this.duration = duration;
    }
    public int getDuration() {
        return duration;
    }
    public static TrafficLightState[] values() {...};
    public static TrafficLightState valueOf(String name) {...};
}

所以,除了增加了一些额外的JDK类,JVM没有为支持枚举做任何修改,枚举只不过是个语法糖,不是吗?

Switch语句支持

如果使用Enum作为switch的参数,你无需给每个枚举常量添加枚举类型作为前缀:

TrafficLightState s = ...;
switch (s) {
case RED:
    System.out.println("break");
    break;
case GREEN:
    System.out.println("go");
    break;
case AMBER:
    System.out.println("go faster");
    break;
}

唯一一个我希望有但是没有找到的事情是“未覆盖所有分支”编译警告。比如,如果你从上面的switch语句中删除RED分支,我希望编译器能发出“使用TrafficLightState的switch语句中没有RED分支”。GCC用-Wall选项编译C++时就会有这样的告警。

序列化和复制

枚举已经实现了必要的机制,保证在JVM中的每个枚举常量只实例化一次,所以,你总是可以安全地用“==”比较两个枚举常量。java.lang.Enum覆盖了clone方法,并声明为final(子类不能再覆盖它),其实现是抛出CloneNotSupportedException异常。因为枚举常量是单例的,不能复制,是这样的吗?

EnumMap 和 EnumSet

新增了两个集合类:EnumSet 和 EnumMap。EnumSet是特定枚举类型的枚举常量的集合,因为枚举都只有固定数量的常量(比如3),所以特定枚举类的EnumSet的元素数量最多也只有那么多个元素(比如3个)。基于枚举中的ordinal值是一个递增值的事实,EnumSet被高效地实现为位向量(注:如果枚举的常量少于64个,则用一个long变量的64个位表示每个枚举常量,如果超过64个,则用一个long数组的每一个位表示)。类似地,EnumMap把枚举常量映射到对象,并且高效地使用数组进行实现。

最后

enum是不是很有趣呢?

本文来自 [时光记 - 王智超的个人空间](www.hiwzc.com),转载请注明出处。