java编程思想之枚举类型详解(初步)

2017/12/20 javaThinking

关键字 enum 可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用。这是一种非常有用的功能。

在之前的学习中我们已经简单了解到枚举的概念。现在我们可以更深入的学习 Java SE5 中的枚举了。我们将在本章节中看到,使用 enum 可以做很多有趣的事情,同时我们也会深入的了解其他的 Java 特性,例如泛型和反射。还会学习一些设计模式的内容。

基本 enum 特性

调用 enum 的 values() 方法,可以遍历 enum 的实例。values() 方法返回 enum 实例的数组,而且改数组中的元素将严格保持其在 enum 中声明时的顺序,因此你可以在循环中使用 valus() 返回的数组。

下面的例子演示了 Enum 提供的一些功能:

enum Shrubery {
	GROUND,CRAWLING,HANGING
};

public class EnumClass {

	public static void main(String[] args) {
		for (Shrubery string : Shrubery.values()) {
			//string.ordinal()返回每个实例声明的顺序,从 0 开始
			System.out.println(string + "  ordinal:"+string.ordinal());
			//排序的方法比较
			System.out.println(string.compareTo(Shrubery.CRAWLING));
			System.out.println(string.equals(Shrubery.CRAWLING));
			//比较实例是否相等,编译器会自动提供 equals 和 hasCode
			System.out.println(string == Shrubery.CRAWLING);
			//返回这个枚举实例所属的类
			System.out.println(string.getDeclaringClass());
			//返回实例声明时的名字
			System.out.println(string.name());

			System.out.println("---------------------");
		}

		for (String string : "GROUND CRAWLING HANGING".split(" ")) {
			//valueof() 是 Enum 的静态方法。根据给定的名字返回相应的 Enum 实例。如果不存在给定的名字就抛出异常
			Shrubery shrubery = Enum.valueOf(Shrubery.class,string);
			System.out.println(shrubery);
		}

	}

}

测试结果:

GROUND  ordinal:0
-1
false
false
class enumpackage.Shrubery
GROUND
---------------------
CRAWLING  ordinal:1
0
true
true
class enumpackage.Shrubery
CRAWLING
---------------------
HANGING  ordinal:2
1
false
false
class enumpackage.Shrubery
HANGING
---------------------
GROUND
CRAWLING
HANGING

将静态导入用于 enum

看下面这个示例:

enum Spiciness{
	NOT,MILD,MEDIUM,HOT,FLAMING
}

静态导入枚举示例:

public class Burrito {
	Spiciness degree;

	protected Burrito(Spiciness degree) {
		super();
		this.degree = degree;
	}

	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return "Buffrito is "+ degree;
	}

	public static void main(String[] args) {
		System.out.println(new Burrito(NOT));
		System.out.println(new Burrito(MEDIUM));
		System.out.println(new Burrito(HOT));
	}

}

测试结果:

Buffrito is NOT
Buffrito is MEDIUM
Buffrito is HOT

我们看到使用 static import 能够将 enum 的实例的标识符带入当前的命名空间,所以无需使用 enum 类型来修饰实例。那么是显示的修饰好呢?还是静态导入好呢?这要看代码的复杂程度了,编译器可以保证我们使用的是正确的类型,所以唯一担心的是使用静态导入会不会导致你的代码难以理解。

向 enum 中添加新方法

除了不能继承 enum 之外,我们基本上可以将 enum 看做是一个常规的类。我们可以向 enum 中添加方法。enum 甚至也可以有 main() 方法。

我们定义一个方法返回枚举实例的描述:

public enum OzWitch {
	//如果要打算定义自己的方法必须在实例的最后添加分号隔开。
	WEST("AAAAAAAAA"),NORTH("SSSSS"),EAST("DDDDDDD");
	private String description;

	private OzWitch(String description) {
		this.description = description;
	}

	public String getDescription() {
		return description;
	}

	public static void main(String[] args) {
		for (OzWitch string : OzWitch.values()) {
			System.out.println(string+": "+string.getDescription());
		}
	}

}

测试结果:

WEST: AAAAAAAAA
NORTH: SSSSS
EAST: DDDDDDD

注意:你打算自定义你的方法,那么必须在 enum 实例序列的最后添加一个分号。同时,Java 要求你必须先定义 enum 实例。如果在定义 enum 实例之前定义了任何方法和属性,那么编译器会得到错误的信息。

覆盖 enum 的方法

覆盖 toString() 方法,给我们提供了另外一种方式来为枚举实例生成不同的字符串描述信息。在下面的实例中,我们希望改变一下格式:

public enum SpaceShip {
	SCOUT,CARGO,TRANSPORT,CRUISER,BATTLESHIP,MOTHERSHIP;
	@Override
	public String toString() {
		String id = name();
		String lower = id.substring(1).toLowerCase();
		return id.charAt(0) + lower;
	}

	public static void main(String[] args) {
		for (SpaceShip string : values()) {
			System.out.println(string);
		}
	}
}

测试结果:

Scout
Cargo
Transport
Cruiser
Battleship
Mothership

通过调用 name() 方法取得 SpaceShip 的名字,然后将其修改为只有首字母大写的格式。

switch 语句中的 enum

在 switch 中使用 enum,是 enum 提供的一项非常便利的功能。一般来说在 switch 中只能使用整数型的值,而枚举实例天生具有整数值的次序,并且可以通过 ordinal() 方法取得次序,因此我们可以在 switch 中使用 enum。

下面的示例演示使用 enum 构造一个小型状态机:

enum Signal{
	GREEN,YELLOW,RED
}

public class Trafficlight {

	Signal color = Signal.RED;
	public void change() {
		switch (color) {
		case GREEN:
			color = Signal.RED;
			break;

        case YELLOW:
        	color = Signal.YELLOW;
			break;
        case RED:
        	color = Signal.GREEN;
	        break;

		default:
			break;
		}

	}

	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return "颜色是:"+color;
	}

	public static void main(String[] args) {

		Trafficlight trafficlight = new Trafficlight();
		for (int i = 0; i < 7; i++) {
			System.out.println(trafficlight);
			trafficlight.change();
		}

	}

}

测试结果:

颜色是:RED
颜色是:GREEN
颜色是:RED
颜色是:GREEN
颜色是:RED
颜色是:GREEN
颜色是:RED

注意:case 语句的多少编译器并不会管。意味着我们必须确保自己覆盖了所有的分支。但是,如果在 case 语句中调用了 return,那么编译器就会抱怨缺少 defult 语句。

values() 的神秘之处

编译器会为你创建的 enum 类都继承自 Enum 类。然而 Enum 类却并没有 values() 方法。难道存在某种隐藏的方法?我们使用反射机制编写一个简单的程序来验证:

enum Exploredemo{
	HERR,THERE
}
public class Reflection {
	public static Set<String> analyze(Class<?> enumClass) {
		System.out.println("输入的类型:"+enumClass);
		for (Type type: enumClass.getGenericInterfaces()) {
			System.out.println("接口类型:"+type);
		}

		System.out.println(enumClass.getSuperclass());
		System.out.println("------------------");
		Set<String> methods = new TreeSet<String>();
		for (Method string : enumClass.getMethods()) {
			methods.add(string.getName());
		}
		System.out.println(methods);
		return methods;
	}

	public static void main(String[] args) {
		Set<String> esploreMethods = analyze(Exploredemo.class);
		Set<String> enuMethods = analyze(Enum.class);

	}

}

测试结果:

输入的类型:class enumpackage.Exploredemo
class java.lang.Enum
------------------
[compareTo, equals, getClass, getDeclaringClass, hashCode, name, notify, notifyAll, ordinal, toString, valueOf, values, wait]
输入的类型:class java.lang.Enum
接口类型:java.lang.Comparable<E>
接口类型:interface java.io.Serializable
class java.lang.Object
------------------
[compareTo, equals, getClass, getDeclaringClass, hashCode, name, notify, notifyAll, ordinal, toString, valueOf, wait]

我们使用 Javap -c 命令反编译一个 enum 文件:

我们看到,values() 是由编译器添加的 static 方法。

在看反编译之后的类:

最后输出的结果来看,编译器将枚举类标记为 final 类,所以无法继承自 enum。由于 values() 方法是由编译器插入到 enum 定义中的静态方法。所以如果将 enum 实例向上转型为 Enum,那么 valuses() 方法将无法访问。不过,class 中有一个 getEnumConstants() 方法,所以即便 Enum 接口中没有 valsue() 方法,我们仍然可以通过 Class 对象获取 enum 的实例。

enum Search{
	HITHER,YON
}
public class UpcastEnum {

	public static void main(String[] args) {
		Search[] vals = Search.values();
		Enum enum1 = Search.HITHER;
		for (Enum search : enum1.getClass().getEnumConstants()) {
			System.out.println(search);
		}

	}

}

测试结果:

HITHER
YON

实现而非继承

我们知道所有的 enum 都继承自 java.lang.Enum 类。由于 Java 不支持多重继承,所以你的 enum 不能在继承其他的类:

然而我们可以同时实现一个或者多个接口:

enum CartoonCharacter implements Generator<CartoonCharacter> {
  SLAPPY, SPANKY, PUNCHY, SILLY, BOUNCY, NUTTY, BOB;
  private Random rand = new Random(47);
  public CartoonCharacter next() {
    return values()[rand.nextInt(values().length)];
  }
}
public class EnumImplementation {

	public static <T> void printNext(Generator<T> rg) {
	    System.out.print(rg.next() + ", ");
	}

	public static void main(String[] args) {
	    // Choose any instance:
	    CartoonCharacter cc = CartoonCharacter.BOB;
	    for(int i = 0; i < 10; i++)
	      printNext(cc);
	}

}

测试结果:

BOB, PUNCHY, BOB, SPANKY, NUTTY, PUNCHY, SLAPPY, NUTTY, NUTTY, SLAPPY,

随机选取

很多示例都需要从 enum 实例中进行随机选择。我们可以利用泛型,从而使得这个工作更一般化。

public class Enums {
	private static Random random = new Random(47);

	public static <T extends Enum<T>> T random(Class<T> ec) {
		return random(ec.getEnumConstants());
	}

	private static <T> T random(T[] enumConstants) {
		// TODO Auto-generated method stub
		return enumConstants[random.nextInt(enumConstants.length)];
	}
}

下面是一个简单的测试示例:

enum Activity { SITTING, LYING, STANDING, HOPPING,
	  RUNNING, DODGING, JUMPING, FALLING, FLYING
}

public class RandomTest {

	public static void main(String[] args) {
	 for(int i = 0; i < 5; i++)
		System.out.print(Enums.random(Activity.class) + " ");
	}

}

测试结果:

STANDING FLYING RUNNING STANDING RUNNING

使用接口组织枚举

无法从 enum 继承子类。但是我们有时希望扩展 enum 的元素或者对 enum 的元素进行分组。在一个接口内部,创建实现该接口的枚举,以此将元素进行分组,可以达到将枚举元素分类组织的目的。

假设我们用 enum 表示不同类别的食物,同时还希望每个 enum 元素仍然保持 food 类型:

public interface Food {
	enum Appetizer implements Food {
	    SALAD, SOUP, SPRING_ROLLS;
	}
    enum MainCourse implements Food {
	    LASAGNE, BURRITO, PAD_THAI,
	    LENTILS, HUMMOUS, VINDALOO;
    }
	enum Dessert implements Food {
	    TIRAMISU, GELATO, BLACK_FOREST_CAKE,
	    FRUIT, CREME_CARAMEL;
	}
	enum Coffee implements Food {
	    BLACK_COFFEE, DECAF_COFFEE, ESPRESSO,
	    LATTE, CAPPUCCINO, TEA, HERB_TEA;
	}
}

对于 enum 而言,实现接口是使其子类化的唯一办法,所以嵌入在 Food 中的每个 enum 都实现了 Food 接口。继续看下面的实例:所有的东西都是某种类型的 Food

public class TypeOfFood {

	public static void main(String[] args) {
		Food food = Appetizer.SALAD;
		food = MainCourse.LASAGNE;
		food = Dessert.GELATO;
		food = Coffee.CAPPUCCINO;

	}

}

如果 enum 实现了 Food 接口,那么我们就可以将其向上转型为 Food,所以上例中的所有东西都是 Food。

如果我们要创建一个枚举的枚举,那么可以创建一个新的 enum,然后用其实例包装 Food 中的每一个 enum 类:

public enum Course {
	APPET(Food.Appetizer.class),MAIN(Food.MainCourse.class),DESS(Food.Dessert.class),COFF(Food.Coffee.class);

	private Food[] values;

	private Course(Class<? extends Food> kind) {
		values = kind.getEnumConstants();
	}

	public Food randomSelection() {
		return Enums.random(values);
	}
}

上面的程序中,每一个 Course 的实例都将对应的 Class 对象作为构造器的参数。通过 getEnumConstants() 方法,可以从 Class 对象中取得某个 Food 子类的所有 enum 实例。通过从每一个 Course 实例中随机选择一个 Food,我们就能生成一份菜单:

public class Meal {

	public static void main(String[] args) {
		for (int i = 0; i < 3; i++) {
			for (Course course : Course.values()) {
				Food food = course.randomSelection();
				System.out.println(food);
			}
			System.out.println("--------------");
		}

	}

}

测试结果:

SPRING_ROLLS
VINDALOO
FRUIT
DECAF_COFFEE
--------------
SOUP
VINDALOO
FRUIT
TEA
--------------
SALAD
BURRITO
FRUIT
TEA
--------------

此外,还有一种更简洁的关联枚举的办法,就是将一个 enum 嵌套在另一个 enum 内。示例:

public enum SecurityCategory {
	  STOCK(Security.Stock.class), BOND(Security.Bond.class);
	  Security[] values;
	  SecurityCategory(Class<? extends Security> kind) {
	    values = kind.getEnumConstants();
	  }
	  interface Security {
	    enum Stock implements Security { SHORT, LONG, MARGIN }
	    enum Bond implements Security { MUNICIPAL, JUNK }
	  }
	  public Security randomSelection() {
	    return Enums.random(values);
	  }
	  public static void main(String[] args) {
	    for(int i = 0; i < 10; i++) {
	      SecurityCategory category = Enums.random(SecurityCategory.class);
	      System.out.println(category + ": " +category.randomSelection());
	    }
	  }
}

测试结果:

BOND: MUNICIPAL
BOND: MUNICIPAL
STOCK: MARGIN
STOCK: MARGIN
BOND: JUNK
STOCK: SHORT
STOCK: LONG
STOCK: LONG
BOND: MUNICIPAL
BOND: JUNK

我们看到上面的例子中是有一个接口的,接口的作用是将枚举类型包装为一个公共的类型,这一点是必要的。然后才能将 Security 中的 enum 作为构造参数使用。这种方式会使得我们的代码更具有清晰的结构。

使用 EnumSet 替代标志

Set 是一种集合,只能向其中添加不重复的对象。当然,enum 也要起成员不能重复。Java SE5 中引入 EnumSet,是为了通过 enum 创建一种替代品,以替代传统的基于 int 的 “位标志”。

EnumSet 充分考虑到了速度的因素,他是非常高效的。使用 EnumSet 的优点是,它在说明一个二进制位是否存在时,具有更好的表达能力,并且无需担心性能。

EnumSet 的元素必须来自一个 enum。示例代码:

enum AlarmPoints {
	STAIR1, STAIR2, LOBBY, OFFICE1, OFFICE2, OFFICE3,
	  OFFICE4, BATHROOM, UTILITY, KITCHEN
}

public class EnumSets {

	public static void main(String[] args) {
		 EnumSet<AlarmPoints> points = EnumSet.noneOf(AlarmPoints.class); // Empty set
		 points.add(AlarmPoints.BATHROOM);
		 Print.print(points);
		 points.addAll(EnumSet.of(AlarmPoints.STAIR1, AlarmPoints.STAIR2, AlarmPoints.KITCHEN));
		 Print.print(points);
		 points = EnumSet.allOf(AlarmPoints.class);
		 points.removeAll(EnumSet.of(AlarmPoints.STAIR1, AlarmPoints.STAIR2, AlarmPoints.KITCHEN));
		 Print.print(points);
		 points.removeAll(EnumSet.range(AlarmPoints.OFFICE1, AlarmPoints.OFFICE4));
		 Print.print(points);
		 points = EnumSet.complementOf(points);
		 Print.print(points);
	}

}

测试结果:

[BATHROOM]
[STAIR1, STAIR2, BATHROOM, KITCHEN]
[LOBBY, OFFICE1, OFFICE2, OFFICE3, OFFICE4, BATHROOM, UTILITY]
[LOBBY, BATHROOM, UTILITY]
[STAIR1, STAIR2, OFFICE1, OFFICE2, OFFICE3, OFFICE4, KITCHEN]

EnumSet 的基础是 long,一个 long 值有 64 位,而一个 enum 实例只需一位 bit 表示其是否存在。也就是说,在不超过一个 long 的表达能力情况下,你的 EnumSet 可以最多应用不超过 64 个 enum。

public class BigEnumSet {
   enum Big { A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10,
		    A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21,
		    A22, A23, A24, A25, A26, A27, A28, A29, A30, A31, A32,
		    A33, A34, A35, A36, A37, A38, A39, A40, A41, A42, A43,
		    A44, A45, A46, A47, A48, A49, A50, A51, A52, A53, A54,
		    A55, A56, A57, A58, A59, A60, A61, A62, A63, A64, A65,
		    A66, A67, A68, A69, A70, A71, A72, A73, A74, A75
	}
	public static void main(String[] args) {
        EnumSet<Big> bigEnumSet = EnumSet.allOf(Big.class);
		System.out.println(bigEnumSet);
    }
}

哈哈,其实并没有什么异常。显然,Enumset 可以应用于多与 64 个元素。EnumSet 会在必要的时候增加一个 long。

使用 EnumMap

EnumMap 是一种特殊的 Map,它要求其中的键必须来自一个 enum。EnumMap 内部可由数组实现。因此 EnumMap 的速度很快,我们可以放心的使用 enum 实例在 EnumMap 中进行查找操作。不过,我们只能将 enum 的实例作为键来调用 put() 方法,其他操作与一般的 map 差不多。

下面的例子演示命令设计模式的用法:

interface Command {
	void action();
}
public class EnumMaps {
	public static void main(String[] args) {
	    EnumMap<AlarmPoints,Command> em =new EnumMap<AlarmPoints,Command>(AlarmPoints.class);
	    em.put(AlarmPoints.KITCHEN, new Command() {
	      public void action() {
	    	  Print.print("Kitchen fire!");
	      }
	    });

	    em.put(AlarmPoints.BATHROOM, new Command() {
	      public void action() {
	    	  Print.print("Bathroom alert!");
	      }
	    });

	    for(Map.Entry<AlarmPoints,Command> e : em.entrySet()) {
	    	Print.printnb(e.getKey() + ": ");
	        e.getValue().action();
	    }
	    try { // If there's no value for a particular key:
	      em.get(AlarmPoints.UTILITY).action();
	    } catch(Exception e) {
	    	Print.print(e);
	    }
	  }
}

测试结果:

BATHROOM: Bathroom alert!
KITCHEN: Kitchen fire!
java.lang.NullPointerException

与 EnumSet 一样,enum 实例定义时的次序决定了其在 EnumMap 中的顺序。enum 的每个实例作为一个键,总是存在的。但是,如果你没有为这个键调用 put() 方法来存入相应的值,其对应的值是 null。

Show Disqus Comments

Search

    欢迎关注我的微信公众号

    Android开发吹牛皮

    Table of Contents