阅读 79

枚举 学习笔记

枚举类型是 JDK 5 之后引进的一种非常重要的引用类型,可以用来定义一系列枚举常量。

在没有引入 enum 关键字之前,要表示可枚举的变量,只能使用 public static final 的方式。

public staic final int SPRING = 1;
public staic final int SUMMER = 2;
public staic final int AUTUMN = 3;
public staic final int WINTER = 4;
复制代码

这种实现方式有几个弊端。首先,类型不安全。试想一下,有一个方法期待接受一个季节作为参数,那么只能将参数类型声明为 int,但是传入的值可能是 99。显然只能在运行时进行参数合理性的判断,无法在编译期间完成检查。其次,指意性不强,含义不明确。我们使用枚举,很多场合会用到该枚举的字串符表达,而上述的实现中只能得到一个数字,不能直观地表达该枚举常量的含义。当然也可用 String 常量,但是又会带来性能问题,因为比较要依赖字符串的比较操作。

使用 enum 来表示枚举可以更好地保证程序的类型安全和可读性。

enum 是类型安全的。除了预先定义的枚举常量,不能将其它的值赋给枚举变量。这和用 int 或 String 实现的枚举很不一样。

enum 有自己的名称空间,且可读性强。在创建 enum 时,编译器会自动添加一些有用的特性。每个 enum 实例都有一个名字 (name) 和一个序号 (ordinal),可以通过 toString() 方法获取 enum 实例的字符串表示。还以通过 values() 方法获得一个由 enum 常量按顺序构成的数组。

enum 还有一个特别实用的特性,可以在 switch 语句中使用,这也是 enum 最常用的使用方式了。

反编译枚举类型源码

public enum Season {
  SPRING, SUMMER, AUTUMN, WINTER;
}
复制代码

用 javap 反编译一下生成的 class 文件:

public final class Season extends java.lang.Enum<Season> {
  public static final Season SPRING;
  public static final Season SUMMER;
  public static final Season AUTUMN;
  public static final Season WINTER;
  public static Season[] values();
  public static Season valueOf(java.lang.String);
  static {};
}
复制代码

可以看到,实际上在经过编译器编译后生成了一个 Season 类,该类继承自 Enum 类,且是 final 的。从这一点来看,Java 中的枚举类型似乎就是一个语法糖。

每一个枚举常量都对应类中的一个 public static final 的实例,这些实例的初始化应该是在 static {} 语句块中进行的。因为枚举常量都是 final 的,因而一旦创建之后就不能进行更改了。 此外,Season 类还实现了 values() 和 valueOf() 这两个静态方法。

再用 jad 进行反编译,我们可以大致看到 Season 类内部的实现细节:

public final class Season extends Enum
{

    public static Season[] values()
    {
        return (Season[])$VALUES.clone();
    }

    public static Season valueOf(String s)
    {
        return (Season)Enum.valueOf(Season, s);
    }

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

    public static final Season SPRING;
    public static final Season SUMMER;
    public static final Season AUTUMN;
    public static final Season WINTER;
    private static final Season $VALUES[];

    static 
    {
        SPRING = new Season("SPRING", 0);
        SUMMER = new Season("SUMMER", 1);
        AUTUMN = new Season("AUTUMN", 2);
        WINTER = new Season("WINTER", 3);
        $VALUES = (new Season[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}
复制代码

除了对应的四个枚举常量外,还有一个私有的数组,数组中的元素就是枚举常量。编译器自动生成了一个 private 的构造方法,这个构造方法中直接调用父类的构造方法,传入了一个字符串和一个整型变量。从初始化语句中可以看到,字符串的值就是声明枚举常量时使用的名称,而整型变量分别是它们的顺序(从0开始)。枚举类的实现使用了一种多例模式,只有有限的对象可以创建,无法显示调用构造方法创建对象。

values() 方法返回枚举常量数组的一个浅拷贝,可以通过这个数组访问所有的枚举常量;而 valueOf() 则直接调用父类的静态方法 Enum.valueOf(),根据传入的名称字符串获得对应的枚举对象。

Enum 类是不能被继承的,如果我们按照上面反编译的结果自己写一个这样的实现,是不能编译成功的。Java 编译器限制了我们显式的继承 java.Lang.Enum 类, 报错 The type may not subclass Enum explicitly。

Enum 类源码

上面反编译出的 Season 类继承自 Enum 类,且调用了父类的构造函数和静态方法 valueOf,我们来通过 Enum 源码看一下其内部的实现。

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
}
复制代码

从类的声明来看,Enum 是个抽象类,且用到了泛型,类型参数的值必须要是 Enum 的子类。Enum 类还实现了 Comparable 和 Serializable 接口。

Enum 类有两个私有的成员,name 和 ordinal,在 protected 的构造方法中初始化,分别是表示枚举常量名称的字符串、枚举常量在枚举定义中序号的整型变量。这两个常量当然也会被其子类继承,前面看到 Season 类的构造方法中就是直接调用父类的构造方法设置这两个成员的。name 和 ordinal 都是 final 修饰的,一旦初始化后就不能进行修改了。

private final String name;

public final String name() {
    return name;
}

private final int ordinal; //从0开始

public final int ordinal() {
    return ordinal;
}

protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}
复制代码

toString() 方法默认返回枚举常量的名称,该方法在子类中可以进行重写。

public String toString() {
    return name;
}
复制代码

前面我们看到,编译后生成的枚举类中使用了多例模式,其对象只有有限个,且一旦创建后就不能修改。因为每一个枚举常量总是单例的,因而可以使用 == 直接进行比较。equals() 方法就是直接使用 == 比较两个枚举类型的变量的。

//直接使用 `==` 比较,不可在子类重写
public final boolean equals(Object other) {
    return this==other;
}

// 返回该枚举常量的哈希码。和equals一致,该方法不可以被重写。  
public final int hashCode() {
    return super.hashCode();
}  

//类型要相同,根据它们在枚举声明中的先后顺序来返回大小(前面的小,后面的大)。
//子类不可以重写该方法 
public final int compareTo(E o) {
    Enum<?> other = (Enum<?>)o;
    Enum<E> self = this;
    if (self.getClass() != other.getClass() && // optimization
        self.getDeclaringClass() != other.getDeclaringClass())
        throw new ClassCastException();
    return self.ordinal - other.ordinal;
}
复制代码

valueOf() 方法根据传入的字符串返回对应名称的枚举常量。调用 Class 对象的 enumConstantDirectory() (package-private)方法会创建一个名称和枚举常量的 Map,然后以名称为键进行查找。这里名称必须和枚举声明时的名称完全一致。

//返回带指定名称的指定枚举类型的枚举常量。名称必须与在此类型中声明枚举常量所用的标识符完全匹配。
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                            String name) {
    T result = enumType.enumConstantDirectory().get(name);
    if (result != null)
        return result;
    if (name == null)
        throw new NullPointerException("Name is null");
    throw new IllegalArgumentException(
        "No enum constant " + enumType.getCanonicalName() + "." + name);
}

// 得到枚举常量所属枚举类型的Class对象  
public final Class<E> getDeclaringClass() {  
    Class clazz = getClass();  
    Class zuper = clazz.getSuperclass();  
    return (zuper == Enum.class) ? clazz : zuper;  
}
复制代码

枚举对象不能序列化和反序列化,也不允许克隆:

protected final Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
}

/**
 * enum classes cannot have finalize methods.
 */
protected final void finalize() { }

/**
 * 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");
}
复制代码

使用枚举

Java 中的枚举相较于其它编程语言要强大许多,这里给出几个枚举的简单用法。

作为常量

enum Color {
  RED, GREEN, BLUE
}

class Test {
  public void main(String[] args) {
    Color color = Color.RED;
    switch(color) {
      case RED:
        System.out.println("red");
        break;
      case GREEN:
        System.out.println("green");
        break;
      case GREEN:
        System.out.println("blue");
        break;
      default:
        System.out.println("unknow");
    }
  }
}
复制代码

向枚举中添加成员和方法

除了不能继承自 enum 外,我们基本可以将 enum 看作一个普通的类。可以向 enum 中添加成员和方法。添加成员则必须要有对应的构造函数,成员和构造函数都隐式强制为私有的,不可以从外部调用。方法则和普通类中的方法一致。

public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7);
    
    private final double mass;   // in kilograms
    private final double radius; // in meters

    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }

    private double mass() { return mass; }
    private double radius() { return radius; }

    // universal gravitational constant  (m3 kg-1 s-2)
    public static final double G = 6.67300E-11;

    double surfaceGravity() {
        return G * mass / (radius * radius);
    }
    double surfaceWeight(double otherMass) {
        return otherMass * surfaceGravity();
    }

    public static void main(String[] args) {
        if (args.length != 1) {
            System.err.println("Usage: java Planet <earth_weight>");
            System.exit(-1);
        }
        double earthWeight = Double.parseDouble(args[0]);
        double mass = earthWeight/EARTH.surfaceGravity();
        for (Planet p : Planet.values())
           System.out.printf("Your weight on %s is %f%n",
                             p, p.surfaceWeight(mass));
    }
}
复制代码

反编译后发现构造方法如下:

private Planet(String s, int i, double d, double d1)
{
    super(s, i);
    mass = d;
    radius = d1;
}
复制代码

为个别枚举常量重写方法

上一个例子中的为枚举新添加的方法是所有枚举实例共有的,实际上还可以为个别枚举实例单独重写方法:

public enum Gender {
  Male {
    @Override
    public void behave() {
      System.out.println("Male special behavior");
    }
  },
  Female;
  
  @Override
  public void behave() {
    System.out.println("Common behavior");
  }
  
  public static void main(String[] args) {
    Gender.Male.behave(); //Male special behavior
    Gender.Female.behave();  //Common behavior
  }
}
复制代码

这里实际上产生了一个 Gender 的匿名子类 Gender$1,子类重写了父类 Gender 的 behave() 方法。在 Gender 的初始化语句中,生成枚举实例时:

public static final Male = new Gender$1("Male", 0);
public static final Female = new Gender("Female", 1);
复制代码

实现接口

枚举类默认继承自 Enum,由于不支持多继承,无法再继承其它类,但还可以实现接口。

interface Behavior {
  void behave();
}

public enum Gender implements Behavior {
  Male {
    @Override
    public void behave() {
      System.out.println("Male special behavior");
    }
  },
  Female;
  
  @Override
  public void behave() {
    System.out.println("Common behavior");
  }
  
  public static void main(String[] args) {
    Gender.Male.behave(); //Male special behavior
    Gender.Female.behave();  //Common behavior

    Behavior man = Gender.Male;
    man.behave();
  }
}
复制代码

用枚举实现单例

写一个线程安全的单例模式并不是一个简单的事情,但是借助枚举我们可以轻易地做到这一点。

每一个枚举常量在枚举类中的定义都是 public static final。static 类型保证了该实例会在类加载时被初始化(Java 的类加载机制,类第一次被使用时加载静态资源),且类加载和初始化是线程安全的。 final 则可以保证一旦完成初始化,该常量将不能被更改。 此外,由于父类 Enum 中明确了枚举对象不能被序列化和反序列化。 枚举完美地解决了单例模式中的序列化和反序列化问题,使用起来也极其简单:

public enum Singleton {
  INSTANCE;
  public void doSomething() {
    //todo
  }
}
复制代码

使用接口组织枚举

public interface Food {
  enum Coffee implements Food{
    BLACK_COFFEE,DECAF_COFFEE,LATTE,CAPPUCCINO
  }
  enum Dessert implements Food{
    FRUIT, CAKE, GELATO
  }
}
复制代码

EnumSet 和 EnumMap

EnumSet 是一个特殊的 Set,其内部的元素必须是来自同一个 enum。EnumSet 内部使用 bit 向量实现,这种实现方式更紧凑高效,类似于传统基于 int 的位标志。相比于位标志,EnumSet 的可读性更强,且性能上也相差不大。

EnumMap 是一种特殊的 Map,要求其中的键 (key) 必须来自于同一个 enum。由于 enum 自身的实例数量是有限的,EnumMap 在内部可由数组实现,因此速度很快。除了只能使用 enum 作为键以外,其它的操作和一般的 Map 没有太大区别。

文章分类
Android