二十九、优先考虑类型安全的异构容器:
泛型通常用于集合,如Set和Map等。这样的用法也就限制了每个容器只能有固定数目的类型参数,一般来说,这也确实是我们想要的。然而有的时候我们需要更多的灵活性,如数据库可以用任意多的Column,如果能以类型安全的方式访问所有Columns就好了,幸运的是有一种方法可以很容易的做到这一点,就是将key进行参数化,而不是将容器参数化,见以下代码:
public class Favorites {
public void putFavorite(Class type,T instance);
public T getFavorite(Class type);
}
下面是该类的使用示例:
publicstaticvoidmain(String[] args) {
Favorites f = newFavorites();
f.putFavorite(String.class,"Java");
f.putFavorite(Integer.class,0xcafebabe);
f.putFavorite(Class.class,Favorites.class);
String favoriteString = f.getFavorite(String.class);
intfavoriteInteger = f.getFavorite(Integer.class);
Class> favoriteClass = f.getFavorite(Class.class);
System.out.printf("%s %x %s\n",favoriteString
,favoriteInteger,favoriteClass.getName());
}
//Java cafebabe Favorites
这里Favorites实例是类型安全的:当你请求String的时候,它是不会给你Integer的。同时它也是异构的容器,不像普通的Map,他的所有键都是不同类型的。下面就是Favorites的具体实现:
public class Favorites {
private Map,Object> favorites =
new HashMap,Object>();
public void putFavorite(Class type,T instance) {
if (type == null)
throw new NullPointerException("Type is null");
favorites.put(type,type.cast(instance));
}
public T getFavorite(Class type) {
return type.cast(favorites.get(type));
}
}
可以看出每个Favorites实例都得到一个Map,Object>容器的支持。由于该容器的值类型为Object,为了进一步确实类型的安全性,我们在put的时候通过Class.cast()方法将Object参数尝试转换为Class所表示的类型,如果类型不匹配,将会抛出ClassCastException异常。以此同时,在从Map中取出值对象的时候,由于该对象当前的类型是Object,因此我们需要再次利用Class.cast()函数将其转换为我们的目标类型。
对于Favorites类的put/get方法,有一个非常明显的限制,即我们无法将“不可具体化”类型存入到该异构容器中,如List、List等泛型类型。这样的限制主要源于Java中泛型类型在运行时的类型擦出机制,即List.class和List.class是等同的对象,均为List.class。如果Java编译器通过了这样的调用代码,那么List.class和List.class将会返回相同的对象引用,从而破坏Favorites的内部结构。
三十、用enum代替int常量:
枚举类型是指由一组固定的常量组成合法值的类型,该特征是在Java 1.5中开始被支持的,之前的Java代码都是通过“公有静态常量域字段”的方法来简单模拟枚举的,如:
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
... ...
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
这样的写法是比较脆弱的。首先是没有提供相应的类型安全性,如两个逻辑上不相关的常量值之间可以进行比较或运算(APPLE_FUJI - ORANGE_TEMPLE),再有就是常量int是编译时常量,被直接编译到使用他们的客户端中。如果与该常量关联的int发生了变化,客户端就必须重新编译。如果没有重新编译,程序还是可以执行,但是他们的行为将不确定。
下面我们来看一下Java 1.5中提供的枚举的声明方式:
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
和“公有静态常量域字段”不同的是,如果函数的参数是枚举类型,如Apple,那么他的实际值只能来自于该枚举所声明的枚举值,即FUJI, PIPPIN, GRANNY_SMITH。如果试图将Apple和Orange中的枚举值进行比较,将会导致编译错误。和C/C++中提供的枚举不同的是,Java中允许在枚举中添加任意的方法和域,并实现任意的接口。下面先给出一个带有域方法和域字段的枚举声明:
public enum Planet {
MERCURY(3.302e+23,2.439e6),
VENUS(4.869e+24,6.052e6),
EARTH(5.975e+24,6.378e6),
MARS(6.419e+23,3.393e6),
JUPITER(1.899e+27,7.149e7),
SATURN(5.685e+26,6.027e7),
URANUS(8.683e+25,2.556e7),
NEPTUNE(1.024e+26,2.477e7);
private final double mass; //千克
private final double radius; //米
private final double surfaceGravity;
private static final double G = 6.67300E-11;
Planet(double mass,double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}
public double mass() {
return mass;
}
public double radius() {
return radius;
}
public double surfaceGravity() {
return surfaceGravity;
}
public double surfaceWeight(double mass) {
return mass * surfaceGravity;
}
}
在上面的枚举示例代码中,已经将数据和枚举常量关联起来了,因此需要声明实例域字段,同时编写一个带有数据并将数据保存在域中的构造器。枚举天生就是不可变的,因此所有的域字段都应该为final的。下面看一下该枚举的应用示例:
public class WeightTable {
public static void main(String[] args) {
double earthWeight = Double.parseDouble(args[0]);
double mass = earthWeight/Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.printf("Weight on %s is %f%n",p,p.surfaceWeight(mass));
}
}
// Weight on MERCURY is 66.133672
// Weight on VENUS is 158.383926
// Weight on EARTH is 175.000000
// Weight on MARS is 66.430699
// Weight on JUPITER is 442.693902
// Weight on SATURN is 186.464970
// Weight on URANUS is 158.349709
// Weight on NEPTUNE is 198.846116
枚举的静态方法values()将按照声明顺序返回他的值数组。枚举的toString方法返回每个枚举值的声明名称。在实际的编程中,我们常常需要针对不同的枚举常量提供不同的数据操作行为,见如下代码:
public enum Operation {
PLUS,MINUS,TIMES,DIVIDE;
double apply(double x,double y) {
switch (this) {
case PLUS: return x + y;
case MINUS: return x - y;
case TIMES: return x * y;
case DIVIDE: return x / y;
}
throw new AssertionError("Unknown op: " + this);
}
}
上面的代码已经表达出这种根据不同的枚举值,执行不同的操作。但是上面的代码在设计方面确实存在一定的缺陷,或者说漏洞,如果我们新增枚举值的时候,所有和apply类似的域函数,都需要进行相应的修改,如有遗漏将会导致异常的抛出。幸运的是,Java的枚举提供了一种更好的方法可以将不同的行为与每个枚举常量关联起来:在枚举类型中声明一个抽象的apply方法,并在特定于常量的类主体中,用具体的方法覆盖每个常量的抽象apply方法,如:
public enum Operation {
PLUS { double apply(double x,double y) { return x + y;} },
MINUS { double apply(double x,double y) { return x - y;} },
TIMES { double apply(double x,double y) { return x * y;} },
DIVIDE { double apply(double x,double y) { return x / y;} };
abstract double apply(double x, double y);
}
这样在添加新枚举常量时就不会轻易忘记提供相应的apply方法了。我们在进一步看一下如何将枚举常量和特定的数据进行关联,见如下代码:
public enum Operation {
PLUS("+") { double apply(double x,double y) { return x + y;} },
MINUS("-") { double apply(double x,double y) { return x - y;} },
TIMES("*") { double apply(double x,double y) { return x * y;} },
DIVIDE("/") { double apply(double x,double y) { return x / y;} };
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
abstract double apply(double x, double y);
}
下面给出以上代码的应用示例:
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n",x,op,y,op.apply(x,y));
}
}
// 2.000000 + 4.000000 = 6.000000
// 2.000000 - 4.000000 = -2.000000
// 2.000000 * 4.000000 = 8.000000
// 2.000000 / 4.000000 = 0.500000
没有类型有一个自动产生的valueOf(String)方法,他将常量的名字转变为枚举常量本身,如果在枚举中覆盖了toString方法(如上例),就需要考虑编写一个fromString方法,将定制的字符串表示法变回相应的枚举,见如下代码:
public enum Operation {
PLUS("+") { double apply(double x,double y) { return x + y;} },
MINUS("-") { double apply(double x,double y) { return x - y;} },
TIMES("*") { double apply(double x,double y) { return x * y;} },
DIVIDE("/") { double apply(double x,double y) { return x / y;} };
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
abstract double apply(double x, double y);
//新增代码
private static final Map stringToEnum = new HashMap();
static {
for (Operation op : values())
stringToEnum.put(op.toString(),op);
}
public static Operation fromString(String symbol) {
return stringToEnum.get(symbol);
}
}
需要注意的是,我们无法在枚举常量构造的时候将自身放入到Map中,这样会导致编译错误。与此同时,枚举构造器不可以访问枚举的静态域,除了编译时的常量域之外。
三十一、用实例域代替序数:
Java中的枚举提供了ordinal()方法,他返回每个枚举常量在类型中的数字位置,如:
public enum Color {
WHITE,RED,GREEN,BLUE,ORANGE,BLACK;
public int indexOfColor() {
return ordinal() + 1;
}
}
上面的枚举中提供了一个获取颜色索引的方法(indexOfColor),该方法将返回颜色值在枚举类型中的声明位置,如果我们的外部程序依赖了该顺序值,那么这将会是非常危险和脆弱的,因为一旦这些枚举值的位置出现变化,或者在已有枚举值的中间加入新的枚举值时,都将导致该索引值的变化。该条目推荐使用实例域的方式来代替枚举提供的序数值,见如下修改后的代码:
public enum Color {
WHITE(1),RED(2),GREEN(3),ORANGE(4),BLACK(5);
private final int indexOfColor;
Color(int index) {
this.indexOfColor = index;
}
public int indexOfColor() {
return indexOfColor;
}
}
Enum规范中谈到ordinal时这么写道:“大多数程序员都不需要这个方法。它是设计成用于像EnumSet和EnumMap这种基于枚举的通用数据结构的。”除非你在编写的是这种数据结构,否则最好避免使用ordinal()方法。
三十二、用EnumSet代替位域:
下面的代码给出了位域的实现方式:
publicclassText {
publicstaticfinalintSTYLE_BOLD =1<<0;
publicstaticfinalintSTYLE_ITALIC =1<<1;
publicstaticfinalintSTYLE_UNDERLINE =1<<2;
publicstaticfinalintSTYLE_STRIKETHROUGH =1<<3;
publicvoidapplyStyles(intstyles) { ... }
}
这种表示法让你用OR位运算将几个常量合并到一个集合中,使用方式如下:
text.applyStyles(Text.STYLE_BOLD | Text.STYLE_ITALIC);
Java中提供了EnumSet类,该类继承自Set接口,同时也提供了丰富的功能,类型安全性,以及可以从任何其他Set实现中得到的互用性。但是在内部具体实现上,没有EnumSet内容都表示为位矢量。如果底层的枚举类型有64个或者更少的元素,整个EnumSet就用单个long来表示,因此他的性能也是可以比肩位域的。与此同时,他提供了大量的操作方法,其实现也是基于位操作的,但是相比于手工位操作,由于EnumSet替我们承担了这部分的开发,从而也避免了一些容易出现的低级错误,代码的美观程度也会有所提升,见如下修改的代码:
publicclassText {
publicenumStyle { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
publicvoidapplyStyles(Set
}
新的使用方式如下:
text.applyStyles(EnumSet.of(Style.BOLD,Style.ITALIC));
需要说明的是,EnumSet提供了丰富的静态工厂来轻松创建集合。
三十三、用EnumMap代替序数索引:
前面的条目已经给出了尽量不要直接使用枚举的ordinal()方法的原因,这里就不在做过多的赘述了。在这个条目中,只是再一次给出了ordinal()的典型用法,与此同时也再一次提供了一个更为合理的解决方案用于替换ordinal()方法,从而进一步证明我们在编码过程中应该尽可能减少对枚举中ordinal()函数的依赖。见如下代码:
publicclassHerb {
publicenumType { ANNUAL, PERENNIAL, BIENNIAL }
privatefinalString name;
privatefinalType type;
Herb(String name, Type type) {
this.name = name;
this.type = type;
}
@OverridepublicString toString() {
returnname;
}
}
publicstaticvoidmain(String[] args) {
Herb[] garden = getAllHerbsFromGarden();
Set herbsByType = (Set[])newSet[Herb.Type.values().length];
for(inti =0; i < herbsByType.length; ++i) {
herbsByType[i] = newHashSet();
}
for(Herb h : garden) {
herbsByType[h.type.ordinal()].add(h);
}
for(inti =0; i < herbsByType.length; ++i) {
System.out.printf("%s: %s%n",Herb.Type.values()[i],herbByType[i]);
}
}
这里我需要简单描述一下上面代码的应用场景:在一个花园里面有很多的植物,它们被分成3类,分别是一年生(ANNUAL)、多年生(PERENNIAL)和两年生(BIENNIAL),正好对应着Herb.Type中的枚举值。现在我们需要做的是遍历花园中的每一个植物,并将这些植物分为3类,最后再将分类后的植物分类打印出来。下面将提供另外一种方法,即通过EnumMap来实现和上面代码相同的逻辑:
publicstaticvoidmain(String[] args) {
Herb[] garden = getAllHerbsFromGarden();
Map> herbsByType =
newEnumMap>(Herb.Type.class);
for(Herb.Type t : Herb.Type.values()) {
herbssByType.put(t,newHashSet());
}
for(Herb h : garden) {
herbsByType.get(h.type).add(h);
}
System.out.println(herbsByType);
}
和之前的代码相比,这段代码更加清晰,也更加安全,运行效率方面也是可以与使用ordinal()的方式想媲美的。