Java是一种面向对象的高级编程语言。它的出众之处就在于它的简洁。一个程序员所要做的就是创建类(Create Class)以及定义接口(Define Interface),如此而已。当然,这种简洁和优美是有代价的,比如失去了Enum这种广泛使用的数据类型就是一个不小的损失。在Java 1.5以前,程序员们不得不通过一些变通的方法来间接的解决这一问题。比如说,被普遍使用的整数枚举替代法和类型安全类替代法(Type safe Enum)。在正式讨论Java 1.5的枚举类型之前,让我们先简单回顾一下这两种枚举替代方法。
一.整数枚举替代法
比如说我们要定义一个春夏秋冬四季的枚举类型,如果使用整数来模拟,其样子大概为:
对于上面这段程序大家可能不会陌生,因为你可能在你的程序中已经多次使用了这样的整数类型枚举。尽管这是非常普遍的一种枚举替代品,但这并不说明它是一种好的替代品。这种简单的方法有很多严重的问题。
问题1:类型安全问题
首先,使用整数我们无法保证类型安全问题。比如我我们设计一个函数,我们的意图是让调用者传入春夏秋冬之中的某一个值,但是,使用 “整数枚举”我们无法保证用户不传入其它意想不到的值。如下所示:
程序seasonTest(Season.SUMMER)是我们期望的使用方式,而seasonTest(5)是一个明显的错误,但是在编译的时候,编译器会认为这是合法的函数调用而给于通过。这显然是不符合Java类型安全的宗旨的。
问题2:字符串的表达问题
使用枚举的大多数场合,我们需要很方便的得到枚举类型的字符表达形式,比如Spring, Summer,Fall,Winter,甚至是汉语的春,夏,秋,冬。但这种整数类型的枚举和字符没有任何联系,我们要使用一些其他辅助函数来达到这样的效果,显得不够方便,也就是外国人讲的不是一个“Generic solution”。
二.类型安全类替代法
比较好的Enum替代品是一种被叫做类型安全的枚举方法。虽然不同的人的具体实现可能会有些不同,但它们的核心思想是一致的。让我们先看一个简单的例子。
它的特点是:
1. 定义一个类,用这个类的实例来表达枚举值
2. 不提供公开构造函数以杜绝客户自己生成该类的实例
3. 所有的类的实例都是final的,不允许有任何改动
4. 所有的类的实例都是public static的,这样客户可以直接使用它
5. 所有的枚举值都是唯一的,所以程序中可以使用==运算符,而不必使用费时的equals()方法
以上这些特点保证了类型安全。如果有这样的调用程序
那么我们可以放心的是,myFunction方法传入的参数一定是Season类型,绝对不可能是其他类型。而具体的值只能是我们给出的春夏秋冬的某一个(唯一的例外就是传入一个null。那是一个其它性质的问题,是所有Java程序共有的,不是我们今天讨论的话题)。这不就是使用枚举的最根本初衷吗!
它的缺点是:
1. 不够直观,不够简洁
2. 有些情况下不如整数方便,比如不能使用switch语句
3. 内存开销比整数型的要大,虽然对于大部分Java程序这不是一个问题,但对于Java移动设备却可能会是一个潜在的问题
比较完整的实现
上面的源程序是一个最基本的框架。在现实的程序开发中,我们会给它增加一些东西,使它更完善,更便于使用。最常见的是增加一个整数变量,来表示枚举值的先后顺序或是大小级别,英文里叫做Ordinal。这样我们就可以在各个枚举值之间可以进行比较了。另外我们可能会需要得到这个枚举的所有值来进行遍历或是循环等操作。有时候我们可能还希望给出一个字符串(比如Summer)而得到相应的枚举类。如果将这些常见的要求加到我们的具体实现中,那么我们上面的那个程序将会扩展为:
上面给出的这个例子虽然比较好的解决了我们对枚举类型的基本要求,但它显然不够简洁。如果我们要写很多这样的枚举类,那将会是一个不小的任务。并且重复的写类似的程序是非常枯燥和容易犯错误的。为了将程序员从这些繁琐的工作中解放出来,人们开发了一些工具软件来完成这些重复的工作。比如比较流行的JEnum,请参看《再谈在Java中使用枚举》(http://tech.ccidnet.com/pub/article/c1078_a95621_p1.html )
这些工具软件其实是一些“程序生成器”。你按照它的语法规则定义你的枚举(语法相对简单直观),然后运行这些工具软件,它会将你的定义转化为一个完整的Java类,就像我们上面所写的那个程序一样。看到这里,读者也许会想:“为什么不能将这种工具软件的功能放到Java编译器中呢?那样我们不是就可以简单方便的定义枚举了吗?而具体的Java类程序由编译器来生成,我们不必再手工完成那么冗长的程序行,也不必使用什么第三方工具来生成这样的程序行了吗?”如果你有这样的想法,那么就要恭喜你了。因为你和Java的设计开发人员想到一块儿去了。好,现在就让我们来看看Java 1.5中新增的枚举的功能。
Java 1.5的枚举类型
在新的Java 1.5中,如果定义我们上面提到的春夏秋冬四季的枚举,那么语句非常简单,如下所示
怎么样,够简单了吧。在我们全面展开讨论之前,让我们先从这个小例子中我们看一下在Java 1.5中定义一个枚举类型的基本要求。
1. 使用关键字enum
2. 类型名称,比如这里的Season
3. 一串允许的值,比如上面定义的春夏秋冬四季
4. 枚举可以单独定义在一个文件中,也可以嵌在其它Java类中
除了这样的基本要求外,用户还有一些其他选择
1.枚举可以实现一个或多个接口(Interface)
2.可以定义新的变量
3.可以定义新的方法
4.可以定义根据具体枚举值而相异的类
这些选项的具体使用和特点我们会在后面的例子中逐步提到。那么这样的小程序和我们前面提到的“类型安全枚举替代”方案有什么内在联系呢?从表面上看,好像大相径庭,但如果我们深入一步就会发现这两者的本质几乎是一模一样的。怎么样,有些吃惊吗?把上面的枚举编译后,我们得到了Season.class。我们把这个Season.class反编译后就会发现Java 1.5枚举的真实面目,其反编译后的源程序为:
对比一下这个例子和我们前面给出的“类型安全枚举”,你会发现他们几乎是同出一辙。比较显著的一个区别是Java 1.5的所有枚举都是Enum类型的衍生子类。但如果你看看Enum类的源程序,你就会发现它只不过是提供了一些基本服务的基类,就本质而言,Java 1.5的枚举和我们所说的“类型安全枚举”是一致的。
带有参数的构造函数
现在让我们来看一个比较复杂一点的例子,来进一步阐述Java 1.5枚举的本质。 下面这段程序是Sun公司提供的枚举示范程序,是用太阳系中九大行星来说明枚举的一种能力-- 即我们可以创建新的构造函数,而不是仅仅局限于Enum的缺省的那一个。我们还可以自己定义新的方法,常数等等。
将这段程序编译后然后再反编译,我们得到了这样的程序,也就是Java虚拟机实际使用的源程序。
从反编译的程序来看,这个Java 1.5的枚举的其实没有任何神秘之处。它只不过是稍微改变了一下构造函数,增加了几个变量和函数。我们前面提到的“类型安全枚举”一样可以完成同样的工作。
依附于具体变量之上的方法
下面这段小程序也是一个比较有代表性的例子。即在列出的加,减,乘,除四个枚举值中,每一个值都有其相应的类定义段(就是所谓的Value-Specific Class Bodies)。这样,加减乘除四个枚举值就有了各自版本的eval函数实现方法。使用Java 1.5的Enum服务,其源程序为:
这段程序看起来比较复杂,如果我们反编译一下Java编译器生成的Operation.class文件,我们就发现其原理并不复杂。
那么在Java 1.5以前我们是怎样解决这样的问题的呢?如果使用面向对象的原则来解决这一问题,其源程序为:
这种方法的本质和Java 1.5中的Enum是一致的。就是定义一个抽象函数(abstract function),然后每一个Enum值提供一个具体的实现方法。在Java 1.5以前,有的人可能会用一种看起来似乎简单的方法来完成类似的任务。比如:
这种实现的方法和前面提到的方法的区别之处在于它消除了抽象类和抽象函数,使用了条件判断语句来给加减乘除四个枚举值以不同的方法。从效果上看是完全可以的,但这不是面向对象编程所提倡的。所以这种思想没有被引入到Java 1.5中来。
方便的Switch功能
在Java 1.5以前,Switch语句只能和int, short, char以及byte这些数据类型联合使用。现在,在Java 1.5的枚举中,你也可以方便的使用switch 语句了。比如:
(注:A, B, C, D, E, F, Incomplete是美国学校里普遍采用的学习成绩等级)
看了上面这个例子,大家肯定不禁要问:“是Java 1.5的Switch功能增强了吗?”其实事情并不是这样,我们看到的只是一个假象。在编译器编译完程序后,一切又回到从前了。Switch还是在整数上进行转跳。下面就是反编译后的程序片断:
Java 1.5中枚举的本质
从上面我们列举的例子一路看过来,到了这里,读者一定会问,为什么Java 1.5的枚举和Java 1.5以前的“类型安全枚举”是那么的相似呢(抛开Java 1.5中的Generics不说)?其实这非常好理解。大家也许注意到了这样一个细节,Java 1.5中Enum的源程序的第一作者是Josh Bloch。这位Java大师在2001年出版的那本Java经典编程手册《Effective Java Programming Language Guide》中的第五章里,就已经全面清晰地阐述了“类型安全枚举”的核心思想和实现方法。Java 1.5中的枚举不是一种崭新的思想,而是原有思想的一个实现和完善。其进步之处就在于将这些思想体现在了Java的编译器中,程序员看到的是一个简单,直观的枚举服务,将原先需要手工完成或是借助于第三方工具完成的任务直接的放在了编译器中。
当然,Java 1.5中的枚举实现也不是“完美”的。它不是在Java虚拟机层次实现的枚举,而是在编译器层次实现的,本质上是一种“Java程序自动生成器”。这样的实现的好处是不言而喻的。因为它对Java的虚拟机没有任何新的要求,这样极大的减轻了Java虚拟机的压力,Java 1.5的虚拟机不必要做大的改动,在以有的基础上改进和完善就可以了。同时这种做法还使得程序有比较好的向前兼容性。这一指导思想和Java Generics是完全一致的,在Java 编译器层增加功能,而不是大的更动以有的Java虚拟机。所以我们可以将Java 1.5中新增的枚举看作是一种的“语法糖衣(Syntax Sugar)” 当然,不在虚拟机层次实现枚举在有些情况下会暴露一些问题,有时候不免会有一些不伦不类的地方,并且我们多多少少还是牺牲了一些功能。情形和我以前提到的Java Generics一样 (请参看http://tech.ccidnet.com/pub/article/c1078_a170543_p1.html )。下面就让我们讨论一下几个比较显著的问题。
特殊的Enum类
从前面的例子中大家可以看出,所有的枚举类型都是隐式的衍生于基类java.lang.Enum。从表面上看,这个Enum类和其他的Java类没有什么区别。如果看一下我们前面给出的Enum源程序,那么这个问题是非常显而易见的。既然是一个普通的Java类,那么我们可以不可以像其它类那样使用呢?比若说,我们扩展一下这个Enum类,生成一个子类:
从语法上讲,这样的定义是完全合法的。但是如果你试图编译这段程序,Java的编译器就会给出你错误信息。也就是说Enum类可以内部隐式的被扩展,去不允许你直接显式的去扩展。所以说这个Enum类似比较“特殊”的一个类,编译器对它有“特殊政策”。从理论上讲,这是一种不值得推荐的做法。明明是一个非Final的类,你却不能去Extends它,这有悖于类最基本原则的。这是Java 1.5枚举实现中的一个瑕纰,不免叫人感到有些遗憾。
无法扩展的Enum类型
在很多的时候,我们希望我们定义的枚举有扩展能力,就像我们定义的其它类那样。不管从逻辑的角度,还是从面向对象的程序设计原则来看,这个要求都是非常合理的。比如我们前面定义的Operation枚举,在那里我们定义了加减乘除四个基本的运算。如果有一天我们想扩展一下这个枚举,加入取对数和乘方的能力,我们可能会很自然的想到这样的方法:
很遗憾,当你试图编译这样的程序的时候,编译器会给出错误信息,编译不会通过。也就是说,我们失去了扩展一个枚举类型的能力。而在Java 1.5以前,我们是可以手工来完成这样的工作的,比如说我们将Operation基类的构造函数定义为protected的,那么我们就可以方便的扩展Operation类,其源程序为:
从这个意义上来说,Java 1.5的枚举给了我们一些束缚,使我们不再像以前那样可以随意的操作我们自己定义的类,这可以算是倒退了一小步。不过从总体上来说,Java 1.5的枚举是能满足绝大部分程序员的要求的,它的简明,易用的特点是很突出的,牺牲了一些东西还是值得的。如果你对枚举有超出一般的特殊要求,那么你还是可以回到Java 1.5以前的老路上来,手工完成你的枚举类。同时你不用担心太多,因为无论是使用Java 1.5的枚举服务,还是手工完成,其本质都是一样的。为了进一步方便程序员对枚举的操作,Java 1.5中还提供了一些辅助类。比如大家今后可能会经常用到EnumMap和EnumSet类。这些类是对枚举功能的一个补充和完善。
一.整数枚举替代法
比如说我们要定义一个春夏秋冬四季的枚举类型,如果使用整数来模拟,其样子大概为:
public class Season
{
public static final int SPRING = 0;
public static final int SUMMER = 1;
public static final int FALL = 2;
public static final int WINTER = 3;
} |
对于上面这段程序大家可能不会陌生,因为你可能在你的程序中已经多次使用了这样的整数类型枚举。尽管这是非常普遍的一种枚举替代品,但这并不说明它是一种好的替代品。这种简单的方法有很多严重的问题。
问题1:类型安全问题
首先,使用整数我们无法保证类型安全问题。比如我我们设计一个函数,我们的意图是让调用者传入春夏秋冬之中的某一个值,但是,使用 “整数枚举”我们无法保证用户不传入其它意想不到的值。如下所示:
……
public void seasonTest(int season)
{
//season 应该是 Season.SPRING,Season.SUMMER, Season.FALL, Season.WINTER
//但是我们无法保证这一点
……
}
public void foo()
{
seasonTest(Season.SUMMER); //理想中的使用方法
seasonTest(5); //错误调用
}
…… |
程序seasonTest(Season.SUMMER)是我们期望的使用方式,而seasonTest(5)是一个明显的错误,但是在编译的时候,编译器会认为这是合法的函数调用而给于通过。这显然是不符合Java类型安全的宗旨的。
问题2:字符串的表达问题
使用枚举的大多数场合,我们需要很方便的得到枚举类型的字符表达形式,比如Spring, Summer,Fall,Winter,甚至是汉语的春,夏,秋,冬。但这种整数类型的枚举和字符没有任何联系,我们要使用一些其他辅助函数来达到这样的效果,显得不够方便,也就是外国人讲的不是一个“Generic solution”。
……
public String getSeasonString(int season)
{
If(season == Season.SPRING)
return “Spring;
else If(season == Season.SUMMRT)
return “Summer;
……
} |
二.类型安全类替代法
比较好的Enum替代品是一种被叫做类型安全的枚举方法。虽然不同的人的具体实现可能会有些不同,但它们的核心思想是一致的。让我们先看一个简单的例子。
public class Season
{
private Season(String s)
{
m_name = s;
}
public String toString()
{
return m_name;
}
private final String m_name;
public static final Season SPRING = new Season("Spring");
public static final Season SUMMER = new Season("Summer");
public static final Season FALL = new Season("Fall");
public static final Season WINTER = new Season("Winter");
} |
它的特点是:
1. 定义一个类,用这个类的实例来表达枚举值
2. 不提供公开构造函数以杜绝客户自己生成该类的实例
3. 所有的类的实例都是final的,不允许有任何改动
4. 所有的类的实例都是public static的,这样客户可以直接使用它
5. 所有的枚举值都是唯一的,所以程序中可以使用==运算符,而不必使用费时的equals()方法
以上这些特点保证了类型安全。如果有这样的调用程序
public class ClientProgam
{
….
public String myFunction(Season season)
{
……
}
} |
那么我们可以放心的是,myFunction方法传入的参数一定是Season类型,绝对不可能是其他类型。而具体的值只能是我们给出的春夏秋冬的某一个(唯一的例外就是传入一个null。那是一个其它性质的问题,是所有Java程序共有的,不是我们今天讨论的话题)。这不就是使用枚举的最根本初衷吗!
它的缺点是:
1. 不够直观,不够简洁
2. 有些情况下不如整数方便,比如不能使用switch语句
3. 内存开销比整数型的要大,虽然对于大部分Java程序这不是一个问题,但对于Java移动设备却可能会是一个潜在的问题
比较完整的实现
上面的源程序是一个最基本的框架。在现实的程序开发中,我们会给它增加一些东西,使它更完善,更便于使用。最常见的是增加一个整数变量,来表示枚举值的先后顺序或是大小级别,英文里叫做Ordinal。这样我们就可以在各个枚举值之间可以进行比较了。另外我们可能会需要得到这个枚举的所有值来进行遍历或是循环等操作。有时候我们可能还希望给出一个字符串(比如Summer)而得到相应的枚举类。如果将这些常见的要求加到我们的具体实现中,那么我们上面的那个程序将会扩展为:
public class Season implements Comparable
{
private Season(String s)
{
m_ordinal = m_nextOrdinal++;
m_name = s;
}
public String toString()
{
return m_name;
}
public String Name()
{
return m_name;
}
public int compareTo(Object obj)
{
return m_ordinal - ((Season)obj).m_ordinal;
}
public static final Season[] values()
{
return m_seasons;
}
public static Season valueOf(String s)
{
for(int i = 0; i < m_seasons.length; i++)
if(m_seasons[i].Name().equals(s))
return m_seasons[i];
throw new IllegalArgumentException(s);
}
private final String m_name;
private static int m_nextOrdinal = 0;
private final int m_ordinal;
public static final Season SPRING;
public static final Season SUMMER;
public static final Season FALL;
public static final Season WINTER;
private static final Season m_seasons[];
static
{
SPRING = new Season("Spring");
SUMMER = new Season("Summer");
FALL = new Season("Fall");
WINTER = new Season("Winter");
m_seasons = (new Season[] {
SPRING, SUMMER, FALL, WINTER
});
}
} |
上面给出的这个例子虽然比较好的解决了我们对枚举类型的基本要求,但它显然不够简洁。如果我们要写很多这样的枚举类,那将会是一个不小的任务。并且重复的写类似的程序是非常枯燥和容易犯错误的。为了将程序员从这些繁琐的工作中解放出来,人们开发了一些工具软件来完成这些重复的工作。比如比较流行的JEnum,请参看《再谈在Java中使用枚举》(http://tech.ccidnet.com/pub/article/c1078_a95621_p1.html )
这些工具软件其实是一些“程序生成器”。你按照它的语法规则定义你的枚举(语法相对简单直观),然后运行这些工具软件,它会将你的定义转化为一个完整的Java类,就像我们上面所写的那个程序一样。看到这里,读者也许会想:“为什么不能将这种工具软件的功能放到Java编译器中呢?那样我们不是就可以简单方便的定义枚举了吗?而具体的Java类程序由编译器来生成,我们不必再手工完成那么冗长的程序行,也不必使用什么第三方工具来生成这样的程序行了吗?”如果你有这样的想法,那么就要恭喜你了。因为你和Java的设计开发人员想到一块儿去了。好,现在就让我们来看看Java 1.5中新增的枚举的功能。
Java 1.5的枚举类型
在新的Java 1.5中,如果定义我们上面提到的春夏秋冬四季的枚举,那么语句非常简单,如下所示
public enum Season
{
SPRING, SUMMER, FALL, WINTER
} |
怎么样,够简单了吧。在我们全面展开讨论之前,让我们先从这个小例子中我们看一下在Java 1.5中定义一个枚举类型的基本要求。
1. 使用关键字enum
2. 类型名称,比如这里的Season
3. 一串允许的值,比如上面定义的春夏秋冬四季
4. 枚举可以单独定义在一个文件中,也可以嵌在其它Java类中
除了这样的基本要求外,用户还有一些其他选择
1.枚举可以实现一个或多个接口(Interface)
2.可以定义新的变量
3.可以定义新的方法
4.可以定义根据具体枚举值而相异的类
这些选项的具体使用和特点我们会在后面的例子中逐步提到。那么这样的小程序和我们前面提到的“类型安全枚举替代”方案有什么内在联系呢?从表面上看,好像大相径庭,但如果我们深入一步就会发现这两者的本质几乎是一模一样的。怎么样,有些吃惊吗?把上面的枚举编译后,我们得到了Season.class。我们把这个Season.class反编译后就会发现Java 1.5枚举的真实面目,其反编译后的源程序为:
public final class Season extends Enum
{
public static final Season[] values()
{
return (Season[])$VALUES.clone();
}
public static Season valueOf(String s)
{
Season aseason[] = $VALUES;
int i = aseason.length;
for(int j = 0; j < i; j++)
{
Season season = aseason[j];
if(season.name().equals(s))
return season;
}
throw new IllegalArgumentException(s);
}
private Season(String s, int i)
{
super(s, i);
}
public static final Season SPRING;
public static final Season SUMMER;
public static final Season FALL;
public static final Season WINTER;
private static final Season $VALUES[];
static
{
SPRING = new Season("SPRING", 0);
SUMMER = new Season("SUMMER", 1);
FALL = new Season("FALL", 2);
WINTER = new Season("WINTER", 3);
$VALUES = (new Season[] {
SPRING, SUMMER, FALL, WINTER
});
}
} |
对比一下这个例子和我们前面给出的“类型安全枚举”,你会发现他们几乎是同出一辙。比较显著的一个区别是Java 1.5的所有枚举都是Enum类型的衍生子类。但如果你看看Enum类的源程序,你就会发现它只不过是提供了一些基本服务的基类,就本质而言,Java 1.5的枚举和我们所说的“类型安全枚举”是一致的。
/*
* @(#)Enum.java 1.12 04/06/08
*
* Copyright 2004 Sun Microsystems, Inc. All rights reserved.
* SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package java.lang;
import java.io.Serializable;
/**
* This is the common base class of all Java language enumeration types.
*
* @author Josh Bloch
* @author Neal Gafter
* @version 1.12, 06/08/04
* @since 1.5
*/
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
private final String name;
public final String name() {
return name;
}
private final int ordinal;
public final int ordinal() {
return ordinal;
}
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
public String toString() {
return name;
}
public final boolean equals(Object other) {
return this==other;
}
public final int hashCode() {
return System.identityHashCode(this);
}
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
public final int compareTo(E o) {
Enum other = (Enum)o;
Enum self = this;
if (self.getClass() != other.getClass() && // optimization
self.getDeclaringClass() != other.getDeclaringClass())
throw new ClassCastException();
return self.ordinal - other.ordinal;
}
public final Class<E> getDeclaringClass() {
Class clazz = getClass();
Class zuper = clazz.getSuperclass();
return (zuper == Enum.class) ? clazz : zuper;
}
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 const " + enumType +"." + name);
}
} |
带有参数的构造函数
现在让我们来看一个比较复杂一点的例子,来进一步阐述Java 1.5枚举的本质。 下面这段程序是Sun公司提供的枚举示范程序,是用太阳系中九大行星来说明枚举的一种能力-- 即我们可以创建新的构造函数,而不是仅仅局限于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),
PLUTO (1.27e+22, 1.137e6);
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();
}
} |
将这段程序编译后然后再反编译,我们得到了这样的程序,也就是Java虚拟机实际使用的源程序。
public final class Planet extends Enum
{
public static final Planet[] values()
{
return (Planet[])$VALUES.clone();
}
public static Planet valueOf(String s)
{
Planet aplanet[] = $VALUES;
int i = aplanet.length;
for(int j = 0; j < i; j++)
{
Planet planet = aplanet[j];
if(planet.name().equals(s))
return planet;
}
throw new IllegalArgumentException(s);
}
private Planet(String s, int i, double d, double d1)
{
super(s, i);
mass = d;
radius = d1;
}
private double mass()
{
return mass;
}
private double radius()
{
return radius;
}
double surfaceGravity()
{
return (6.6729999999999999E-011D * mass) / (radius * radius);
}
double surfaceWeight(double d)
{
return d * surfaceGravity();
}
public static final Planet MERCURY;
public static final Planet VENUS;
public static final Planet EARTH;
public static final Planet MARS;
public static final Planet JUPITER;
public static final Planet SATURN;
public static final Planet URANUS;
public static final Planet NEPTUNE;
public static final Planet PLUTO;
private final double mass;
private final double radius;
public static final double G = 6.6729999999999999E-011D;
private static final Planet $VALUES[];
static
{
MERCURY = new Planet("MERCURY", 0, 3.3030000000000001E+023D, 2439700D);
VENUS = new Planet("VENUS", 1, 4.8690000000000001E+024D, 6051800D);
EARTH = new Planet("EARTH", 2, 5.9760000000000004E+024D, 6378140D);
MARS = new Planet("MARS", 3, 6.4209999999999999E+023D, 3397200D);
JUPITER = new Planet("JUPITER", 4, 1.9000000000000001E+027D, 71492000D);
SATURN = new Planet("SATURN", 5, 5.6879999999999998E+026D, 60268000D);
URANUS = new Planet("URANUS", 6, 8.686E+025D, 25559000D);
NEPTUNE = new Planet("NEPTUNE", 7, 1.0239999999999999E+026D, 24746000D);
PLUTO = new Planet("PLUTO", 8, 1.2700000000000001E+022D, 1137000D);
$VALUES = (new Planet[] {
MERCURY, VENUS, EARTH, MARS, JUPITER, SATURN, URANUS, NEPTUNE, PLUTO
});
}
} |
从反编译的程序来看,这个Java 1.5的枚举的其实没有任何神秘之处。它只不过是稍微改变了一下构造函数,增加了几个变量和函数。我们前面提到的“类型安全枚举”一样可以完成同样的工作。
依附于具体变量之上的方法
下面这段小程序也是一个比较有代表性的例子。即在列出的加,减,乘,除四个枚举值中,每一个值都有其相应的类定义段(就是所谓的Value-Specific Class Bodies)。这样,加减乘除四个枚举值就有了各自版本的eval函数实现方法。使用Java 1.5的Enum服务,其源程序为:
public enum Operation
{
PLUS { double eval(double x, double y) { return x + y; } },
MINUS { double eval(double x, double y) { return x - y; } },
TIMES { double eval(double x, double y) { return x * y; } },
DIVIDE { double eval(double x, double y) { return x / y; } };
abstract double eval(double x, double y);
} |
这段程序看起来比较复杂,如果我们反编译一下Java编译器生成的Operation.class文件,我们就发现其原理并不复杂。
public abstract class Operation extends Enum
{
public static final Operation[] values()
{
return (Operation[])$VALUES.clone();
}
public static Operation valueOf(String s)
{
Operation aoperation[] = $VALUES;
int i = aoperation.length;
for(int j = 0; j < i; j++)
{
Operation operation = aoperation[j];
if(operation.name().equals(s))
return operation;
}
throw new IllegalArgumentException(s);
}
private Operation(String s, int i)
{
super(s, i);
}
abstract double eval(double d, double d1);
public static final Operation PLUS;
public static final Operation MINUS;
public static final Operation TIMES;
public static final Operation DIVIDE;
private static final Operation $VALUES[];
static
{
PLUS = new Operation("PLUS", 0) {
double eval(double d, double d1)
{
return d + d1;
}
};
MINUS = new Operation("MINUS", 1) {
double eval(double d, double d1)
{
return d - d1;
}
};
TIMES = new Operation("TIMES", 2) {
double eval(double d, double d1)
{
return d * d1;
}
};
DIVIDE = new Operation("DIVIDE", 3) {
double eval(double d, double d1)
{
return d / d1;
}
};
$VALUES = (new Operation[] {
PLUS, MINUS, TIMES, DIVIDE
});
}
} |
那么在Java 1.5以前我们是怎样解决这样的问题的呢?如果使用面向对象的原则来解决这一问题,其源程序为:
public abstract class Operation
{
private final String m_name;
Operation(String name) {m_name = name;}
public static final Operation PLUS = new Operation("Plus"){
protected double eval(double x, double y){
return x + y;
}
}
public static final Operation MINUS = new Operation("Minus"){
protected double eval(double x, double y){
return x - y;
}
}
public static final Operation TIMES = new Operation("Times"){
protected double eval(double x, double y){
return x * y;
}
}
public static final Operation DEVIDE = new Operation("Devide"){
protected double eval(double x, double y){
return x / y;
}
}
abstract double eval (double x, double y);
public String toString() {return m_name; }
private static int m_nextOridnal = 0;
private final int m_ordinal = m_nextOridnal++;
private static final Operation[] VALUES = {
PLUS, MINUS, TIMES, DEVIDE
};
} |
这种方法的本质和Java 1.5中的Enum是一致的。就是定义一个抽象函数(abstract function),然后每一个Enum值提供一个具体的实现方法。在Java 1.5以前,有的人可能会用一种看起来似乎简单的方法来完成类似的任务。比如:
public class Operation
{
private final String m_name;
Operation(String name) {m_name = name;}
public static final Operation PLUS = new Operation("Plus");
public static final Operation MINUS = new Operation("Minus");
public static final Operation TIMES = new Operation("Times");
public static final Operation DEVIDE = new Operation("Devide");
public double eval (double x, double y){
if(this == Operation.PLUS){
return x + y;
}
else if(this == Operation.MINUS){
return x - y;
}
else if(this == Operation.TIMES){
return x * y;
}
else if(this == Operation.DEVIDE){
return x / y;
}
return -1;
}
public String toString() {return m_name; }
private static int m_nextOridnal = 0;
private final int m_ordinal = m_nextOridnal++;
private static final Operation[] VALUES = {
PLUS, MINUS, TIMES, DEVIDE
};
} |
这种实现的方法和前面提到的方法的区别之处在于它消除了抽象类和抽象函数,使用了条件判断语句来给加减乘除四个枚举值以不同的方法。从效果上看是完全可以的,但这不是面向对象编程所提倡的。所以这种思想没有被引入到Java 1.5中来。
方便的Switch功能
在Java 1.5以前,Switch语句只能和int, short, char以及byte这些数据类型联合使用。现在,在Java 1.5的枚举中,你也可以方便的使用switch 语句了。比如:
public class EnumTest {
public enum Grade {A,B,C,D,E,F,Incomplete }
private Grade m_grade;
public EnumTest(Grade grade) {
this.m_grade = grade;
testing();
}
private void testing(){
switch(this.m_grade){
case A:
System.out.println(Grade.A.toString());
break;
case B:
System.out.println(Grade.B.toString());
break;
case C:
System.out.println(Grade.C.toString());
break;
case D:
System.out.println(Grade.D.toString());
break;
case E:
System.out.println(Grade.E.toString());
break;
case F:
System.out.println(Grade.F.toString());
break;
case Incomplete:
System.out.println(Grade.Incomplete.toString());
break;
}
}
public static void main(String[] args){
new EnumTest(Grade.A);
}
} |
(注:A, B, C, D, E, F, Incomplete是美国学校里普遍采用的学习成绩等级)
看了上面这个例子,大家肯定不禁要问:“是Java 1.5的Switch功能增强了吗?”其实事情并不是这样,我们看到的只是一个假象。在编译器编译完程序后,一切又回到从前了。Switch还是在整数上进行转跳。下面就是反编译后的程序片断:
…… //从略
private void testing()
{
static class _cls1
{
static final int $SwitchMap$EnumTest1$Grade[];
static
{
$SwitchMap$EnumTest1$Grade = new int[Grade.values().length];
try
{
$SwitchMap$EnumTest1$Grade[Grade.A.ordinal()] = 1;
}
catch(NoSuchFieldError nosuchfielderror) { }
try
{
$SwitchMap$EnumTest1$Grade[Grade.B.ordinal()] = 2;
}
catch(NoSuchFieldError nosuchfielderror1) { }
try
{
$SwitchMap$EnumTest1$Grade[Grade.C.ordinal()] = 3;
}
catch(NoSuchFieldError nosuchfielderror2) { }
try
{
$SwitchMap$EnumTest1$Grade[Grade.D.ordinal()] = 4;
}
catch(NoSuchFieldError nosuchfielderror3) { }
try
{
$SwitchMap$EnumTest1$Grade[Grade.E.ordinal()] = 5;
}
catch(NoSuchFieldError nosuchfielderror4) { }
try
{
$SwitchMap$EnumTest1$Grade[Grade.F.ordinal()] = 6;
}
catch(NoSuchFieldError nosuchfielderror5) { }
try
{
$SwitchMap$EnumTest1$Grade[Grade.Incomplete.ordinal()] = 7;
}
catch(NoSuchFieldError nosuchfielderror6) { }
}
}
switch(_cls1..SwitchMap.EnumTest1.Grade[m_grade.ordinal()])
{
case 1: // '/001'
System.out.println(Grade.A.toString());
break;
case 2: // '/002'
System.out.println(Grade.B.toString());
break;
case 3: // '/003'
System.out.println(Grade.C.toString());
break;
case 4: // '/004'
System.out.println(Grade.D.toString());
break;
case 5: // '/005'
System.out.println(Grade.E.toString());
break;
case 6: // '/006'
System.out.println(Grade.F.toString());
break;
case 7: // '/007'
System.out.println(Grade.Incomplete.toString());
break;
}
} |
Java 1.5中枚举的本质
从上面我们列举的例子一路看过来,到了这里,读者一定会问,为什么Java 1.5的枚举和Java 1.5以前的“类型安全枚举”是那么的相似呢(抛开Java 1.5中的Generics不说)?其实这非常好理解。大家也许注意到了这样一个细节,Java 1.5中Enum的源程序的第一作者是Josh Bloch。这位Java大师在2001年出版的那本Java经典编程手册《Effective Java Programming Language Guide》中的第五章里,就已经全面清晰地阐述了“类型安全枚举”的核心思想和实现方法。Java 1.5中的枚举不是一种崭新的思想,而是原有思想的一个实现和完善。其进步之处就在于将这些思想体现在了Java的编译器中,程序员看到的是一个简单,直观的枚举服务,将原先需要手工完成或是借助于第三方工具完成的任务直接的放在了编译器中。
当然,Java 1.5中的枚举实现也不是“完美”的。它不是在Java虚拟机层次实现的枚举,而是在编译器层次实现的,本质上是一种“Java程序自动生成器”。这样的实现的好处是不言而喻的。因为它对Java的虚拟机没有任何新的要求,这样极大的减轻了Java虚拟机的压力,Java 1.5的虚拟机不必要做大的改动,在以有的基础上改进和完善就可以了。同时这种做法还使得程序有比较好的向前兼容性。这一指导思想和Java Generics是完全一致的,在Java 编译器层增加功能,而不是大的更动以有的Java虚拟机。所以我们可以将Java 1.5中新增的枚举看作是一种的“语法糖衣(Syntax Sugar)” 当然,不在虚拟机层次实现枚举在有些情况下会暴露一些问题,有时候不免会有一些不伦不类的地方,并且我们多多少少还是牺牲了一些功能。情形和我以前提到的Java Generics一样 (请参看http://tech.ccidnet.com/pub/article/c1078_a170543_p1.html )。下面就让我们讨论一下几个比较显著的问题。
特殊的Enum类
从前面的例子中大家可以看出,所有的枚举类型都是隐式的衍生于基类java.lang.Enum。从表面上看,这个Enum类和其他的Java类没有什么区别。如果看一下我们前面给出的Enum源程序,那么这个问题是非常显而易见的。既然是一个普通的Java类,那么我们可以不可以像其它类那样使用呢?比若说,我们扩展一下这个Enum类,生成一个子类:
public class MyEnum extends Enum
{
protected Enum(String name, int ordinal);
protected Object clone( );
} |
从语法上讲,这样的定义是完全合法的。但是如果你试图编译这段程序,Java的编译器就会给出你错误信息。也就是说Enum类可以内部隐式的被扩展,去不允许你直接显式的去扩展。所以说这个Enum类似比较“特殊”的一个类,编译器对它有“特殊政策”。从理论上讲,这是一种不值得推荐的做法。明明是一个非Final的类,你却不能去Extends它,这有悖于类最基本原则的。这是Java 1.5枚举实现中的一个瑕纰,不免叫人感到有些遗憾。
无法扩展的Enum类型
在很多的时候,我们希望我们定义的枚举有扩展能力,就像我们定义的其它类那样。不管从逻辑的角度,还是从面向对象的程序设计原则来看,这个要求都是非常合理的。比如我们前面定义的Operation枚举,在那里我们定义了加减乘除四个基本的运算。如果有一天我们想扩展一下这个枚举,加入取对数和乘方的能力,我们可能会很自然的想到这样的方法:
public enum OperationExt extends Operation
{
LOG { double eval(double x, double y) { return Math.log(y) / Math.log(x);} },
POWER { double eval(double x, double y) { return Math.power(x,y);} },
} |
很遗憾,当你试图编译这样的程序的时候,编译器会给出错误信息,编译不会通过。也就是说,我们失去了扩展一个枚举类型的能力。而在Java 1.5以前,我们是可以手工来完成这样的工作的,比如说我们将Operation基类的构造函数定义为protected的,那么我们就可以方便的扩展Operation类,其源程序为:
abstract class OperationExt extends Opetation
{
private final String m_name;
Operation(String name) {m_name = name;}
public static final LOG = new Operation("Log"){
protected double eval(double x, double y){
return Math.log(y) / Math.log(x);
}
}
public static final POWER = new Operation("Power"){
protected double eval(double x, double y){
return Math.log(x,y);
}
}
protected double eval (double x, double y);
public String toString() {return m_name; }
private static int m_nextOridnal = 0;
private final int m_ordinal = m_nextOridnal++;
private static final Operation[] VALUES = {
LOG, POWER;
}
} |
从这个意义上来说,Java 1.5的枚举给了我们一些束缚,使我们不再像以前那样可以随意的操作我们自己定义的类,这可以算是倒退了一小步。不过从总体上来说,Java 1.5的枚举是能满足绝大部分程序员的要求的,它的简明,易用的特点是很突出的,牺牲了一些东西还是值得的。如果你对枚举有超出一般的特殊要求,那么你还是可以回到Java 1.5以前的老路上来,手工完成你的枚举类。同时你不用担心太多,因为无论是使用Java 1.5的枚举服务,还是手工完成,其本质都是一样的。为了进一步方便程序员对枚举的操作,Java 1.5中还提供了一些辅助类。比如大家今后可能会经常用到EnumMap和EnumSet类。这些类是对枚举功能的一个补充和完善。