从C++转到Java上的程序员一开始总是对Java有不少抱怨,其中没有枚举就是一个比较突出的问题。那么为什么Java不支持枚举呢?从程序语言的角度讲,支持枚举意味着什么呢?我们能不能找到一种方法满足C++程序员对枚举的要求呢?那么现在就让我们一起来探讨一下这个问题。
枚举类型(Enumerated Types)
让我们先看下面这一段小程序:
enum Day {SUNDAY, MONDAY, TUESDAY,
WEDNESDAY, THURSDAY, FRIDAY, SATURDAY}; |
这种申明提供了一种用户友好的变量定义的方法,它枚举了这种数据类型所有可能的值,即星期一到星期天。抛开具体编程语言来看,枚举所具有的核心功能应该是:
类型安全(Type Safety)
紧凑有效的枚举数值定义 (Compact, Efficient Declaration of Enumerated Values)
无缝的和程序其它部分的交互操作(Seamless integration with other language features)
运行的高效率(Runtime efficiency)
现在我们就这几个特点逐一讨论一下。
1. 类型安全
枚举的申明创建了一个新的类型。它不同于其他的已有类型,包括原始类型(整数,浮点数等等)和当前作用域(Scope)内的其它的枚举类型。当你对函数的参数进行赋值操作的时候,整数类型和枚举类型是不能互换的(除非是你进行显式的类型转换),编译器将强制这一点。比如说,用上面申明的枚举定义这样一个函数:
如果你用整数来调用这个函数,编译器会给出错误的。
foo(4); // compilation error |
如果按照这个标准,那么Pascal, Ada, 和C++是严格意义上的支持枚举,而C语言都不是。
2. 紧凑有效的枚举数值定义
定义枚巨的程序应该很简单。比如说,在Java中我们有这样一种"准枚举"的定义方法:
public static final int SUNDAY = 0;
public static final int MONDAY = 1;
public static final int TUESDAY = 2;
public static final int WEDNESDAY = 3;
public static final int THURSDAY = 4;
public static final int FRIDAY = 5;
public static final int SATURDAY = 6; |
这种定义就似乎不够简洁。如果有大量的数据要定义,这一点就尤为重要,你也就会感受更深。虽然这一点不如其他另外3点重要,但我们总是希望申明能尽可能的简洁。
3. 无缝的和程序其它部分的交互操作
语言的运算符,如赋值,相等/大于/小于判断都应该支持枚举。枚举还应该支持数组下标以及switch/case语句中用来控制流程的操作。比如:
for (Day d = SUNDAY; d <= SATURDAY; ++d) {
switch(d) {
case MONDAY: ...;
break;
case TUESDAY: ...;
break;
case WEDNESDAY: ...;
break;
case THURSDAY: ...;
break;
case FRIDAY: ...;
break;
case SATURDAY:
case SUNDAY: ...;
}
} |
要想让这段程序工作,那么枚举必须是整数常数,而不能是对象(objects)。Java中你可以用equals() 或是 compareTo() 函数来进行对象的比较操作,但是它们都不支持数组下标和switch语句。
4. 运行的高效率
枚举的运行效率应该和原始类型的整数一样高。在运行时不应该由于使用了枚举而导致性能比使用整数有下降。
如果一种语言满足这四点要求,那么我们可以说这种语言是真正的支持枚举。比如前面所说的Pascal, Ada, 和C++。很明显,Java不是。
Java的创始人James Gosling是个资深的C++程序员,他很清楚什么是枚举。但似乎他有意的删除了Java的枚举能力。其原因我们不得而知。可能是他想强调和鼓励使用多态性(polymorphism),不鼓励使用多重分支。而多重分支往往是和枚举联合使用的。不管他的初衷如何,我们在Java中仍然需要枚举。
Java中的几种"准枚举"类型
虽然Java 不直接支持用户定义的枚举。但是在实践中人们还是总结出一些枚举的替代品。
第一种替代品可以解释为"整数常数枚举"。如下所示:
public static final int SUNDAY = 0;
public static final int MONDAY = 1;
public static final int TUESDAY = 2;
public static final int WEDNESDAY = 3;
public static final int THURSDAY = 4;
public static final int FRIDAY = 5;
public static final int SATURDAY = 6; |
这种方法可以让我们使用更有意义的变量名而不是直接赤裸裸的整数值。这样使得源程序的可读性和可维护性更好一些。这些定义可以放在任何类中。可以和其它的变量和方法混在一起。也可以单独放在一个类中。如果你选择将其单独放在一个类中,那么引用的时候要注意语法。比如"Day.MONDAY."。如果你想在引用的时候省一点事,那么你可以将其放在一个接口中(interface),其它类只要申明实现(implement)它就可以比较方便的引用。比如直接使用MONDAY。就Java接口的使用目的而言,这种用法有些偏,不用也罢!
这种方法显然满足了条件3和4,即语言的集成和执行效率(枚举就是整数,没有效率损失)。但是他却不能满足条件1和2。它的定义有些啰嗦,更重要的是它不是类型安全的。这种方法虽然普遍被Java程序员采用,但它不是一种枚举的良好替代品。
第二种方法是被一些有名的专家经常提及的。我们可以称它为"对象枚举"。即为枚举创建一个类,然后用公用的该类的对象来表达每一个枚举的值。如下所示:
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.io.Serializable;
import java.io.InvalidObjectException;
public final class Day implements Comparable, Serializable {
private static int size = 0;
private static int nextOrd = 0;
private static Map nameMap = new HashMap(10);
private static Day first = null;
private static Day last = null;
private final int ord;
private final String label;
private Day prev;
private Day next;
public static final Day SUNDAY = new Day("SUNDAY");
public static final Day MONDAY = new Day("MONDAY");
public static final Day TUESDAY = new Day("TUESDAY");
public static final Day WEDNESDAY = new Day("WEDNESDAY");
public static final Day THURSDAY = new Day("THURSDAY");
public static final Day FRIDAY = new Day("FRIDAY");
public static final Day SATURDAY = new Day("SATURDAY");
/**
* 用所给的标签创建一个新的day.
* (Uses default value for ord.)
*/
private Day(String label) {
this(label, nextOrd);
}
/**
* Constructs a new Day with its label and ord value.
*/
private Day(String label, int ord) {
this.label = label;
this.ord = ord;
++size;
nextOrd = ord + 1;
nameMap.put(label, this);
if (first == null)
first = this;
if (last != null) {
this.prev = last;
last.next = this;
}
last = this;
}
/**
* Compares two Day objects based on their ordinal values.
* Satisfies requirements of interface java.lang.Comparable.
*/
public int compareTo(Object obj) {
return ord - ((Day)obj).ord;
}
/**
* Compares two Day objects for equality. Returns true
* only if the specified Day is equal to this one.
*/
public boolean equals(Object obj) {
return super.equals(obj);
}
/**
* Returns a hash code value for this Day.
*/
public int hashCode() {
return super.hashCode();
}
/**
* Resolves deserialized Day objects.
* @throws InvalidObjectException if deserialization fails.
*/
private Object readResolve() throws InvalidObjectException {
Day d = get(label);
if (d != null)
return d;
else {
String msg = "invalid deserialized object: label = ";
throw new InvalidObjectException(msg + label);
}
}
/**
* Returns Day with the specified label.
* Returns null if not found.
*/
public static Day get(String label) {
return (Day) nameMap.get(label);
}
/**
* Returns the label for this Day.
*/
public String toString() {
return label;
}
/**
* Always throws CloneNotSupportedException; guarantees that
* Day objects are never cloned.
*
* @return (never returns)
*/
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
/**
* Returns an iterator over all Day objects in declared order.
*/
public static Iterator iterator() {
// anonymous inner class
return new Iterator()
{
private Day current = first;
public boolean hasNext() {
return current != null;
}
public Object next() {
Day d = current;
current = current.next();
return d;
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
/**
* Returns the ordinal value of this Day.
*/
public int ord() {
return this.ord;
}
/**
* Returns the number of declared Day objects.
*/
public static int size() {
return size;
}
/**
* Returns the first declared Day.
*/
public static Day first() {
return first;
}
/**
* Returns the last declared Day.
*/
public static Day last() {
return last;
}
/**
* Returns the previous Day before this one in declared order.
* Returns null for the first declared Day.
*/
public Day prev() {
return this.prev;
}
/**
* Returns the next Day after this one in declared order.
* Returns null for the last declared Day.
*/
public Day next() {
return this.next;
}
} |
枚举值被定义为公用静态对象(public static object)。此外该类含有私有构造函数;一个循环器(Iterator)用以遍历所有的值;一些Java中常用的函数,如toString(),equals()和compareTo(),以及一些方便客户程序调用的函数,如ord(),prev(),next(),first()和 last()。
这种实现方法有很好的类型安全和运行效率(条件1和4)。但是去不满足条件2和3。首先它的定义比较繁琐,大多数程序员也许因为这个而不去使用它;同时他还不可以被用作数组下标或是用在switch/case语句。这在一定程度上降低了他的使用的广泛性。
看起来,没有一种替代品是理想的。我们虽然没有权利修改Java语言,但是我们也许可以想一些办法来克服"对象枚举"的缺点,使它成为合格的枚举替代品。 |