二十二、优先考虑静态成员类:
在Java中嵌套类主要分为四种类型,下面给出这四种类型的应用场景。
1. 静态成员类:
静态成员类可以看做外部类的公有辅助类,仅当与它的外部类一起使用时才有意义。例如,考虑一个枚举,它描述了计算器支持的各种操作。Operation枚举应该是Calculator类的公有静态成员类,然后,Calculator类的客户端就可以用诸如Calculator.Operation.PLUS和Calculator.Operation.MINUS这样的名称来引用这些操作。
2. 非静态成员类:
一种常见的用法是定义一个Adapter,它允许外部类的实例被看做是另一个不相关的类的实例。如Map接口的实现往往使用非静态成员类来实现它们的集合视图,这些集合视图是由Map的keySet、entrySet和Values方法返回的。
从语法上讲,静态成员类和非静态成员类之间唯一的区别是,静态成员类的声明中包含了static修饰符,尽管语法相似,但实际应用却是大相径庭。每个非静态成员类的实例中都隐含一个外部类的对象实例,在非静态成员类的实例方法内部,可以调用外围实例的方法。如果嵌套类的实例可以在它的外围类的实例之外独立存在,这个嵌套类就必须是静态成员类。由于静态成员类中并不包含外部类实例的对象引用,因此在创建时减少了内存开销。
3. 匿名类:
匿名类没有自己的类名称,也不是外围类的一个成员。匿名类可以出现在代码中任何允许存在表达式的地方。然而匿名类的适用性受到诸多限制,如不能执行instanceof测试,或者任何需要类名称的其他事情。我们也无法让匿名类实现多个接口,当然也不能直接访问其任何成员。最后需要说的是,建议匿名类的代码尽量短小,否则会影响程序的可读性。
匿名类在很多时候可以用作函数对象。
4. 局部类:
是四种嵌套类中最少使用的类,在任何"可以声明局部变量"的地方,都可以声明局部类,并且局部类也遵守同样的作用域规则。
二十三、请不要在新代码中使用原生态类型:
先简单介绍一下泛型的概念和声明形式。声明中具有一个或者多个类型参数的类或者接口,就是泛型类或接口,如List<E>,这其中E表示List集合中元素的类型。在Java中,相对于每个泛型类都有一个原生类与之对应,即不带任何实际类型参数的泛型名称,如List<E>的原生类型List。他们之间最为明显的区别在于List<E>包含的元素必须是E(泛型)类型,如List<String>,那么他的元素一定是String,否则将产生编译错误。和泛型不同的是,原生类型List可以包含任何类型的元素,因此在向集合插入元素时,即使插入了不同类型的元素也不会引起编译期错误。那么在运行,当List的使用从List中取出元素时,将不得不针对类型作出判断,以保证在进行元素类型转换时不会抛出ClassCastException异常。由此可以看出,泛型集合List<E>不仅可以在编译期发现该类错误,而且在取出元素时不需要再进行类型判断,从而提高了程序的运行时效率。
//原生类型的使用方式
class TestRawType {
private final List stamps = new List();
public static void main(String[] args) {
stamps.add(new Coin(...));
}
}
class MyRunnable implements Runnable {
@Override
void run() {
for (Iterator i = stamps.iterator(); i.hasNext(); ) {
Stamp s = (Stamp)i.next(); //这里将抛出类型转换异常
//TODO: do something.
}
}
}
以上仅为简化后的示例代码,当run()方法中抛出异常时,可以很快发现是在main()中添加了非Stamp类型的元素。如果给stamps对象添加元素的操作是在多个函数或线程中完成的,那么迅速定位到底是哪个或哪几个函数添加了非Stamp类型的元素,将会需要更多的时间去调试。
//泛型类型的使用方式
class TestGenericType {
private final List<Stamp> stamps = new List<Stamp>();
public static void main(String[] args) {
stamps.add(new Coin(...)); //该行将直接导致编译错误。
}
}
class MyRunnable implements Runnable {
@Override
void run() {
for (Stamp s : stamps) { //这里不再需要类型转换了。
//TODO: do something
}
}
}
通过以上两个例子可以看出泛型类型相对于原生类型还是有着非常明显的优势的。一般而言,原生类型的使用都是为了保持一定的兼容性,毕竟泛型是在Java 1.5中才推出的。如原有的代码中(Java 1.5之前)包含一个函数,其参数为原生类型,如void func(List l); 在之后的升级代码中,如果给该函数传入泛型类型的List<E>对象将是合法的,不会产生编译错误。同时Java的泛型对象在运行时也会被擦除类型,即List<E>擦除类型后将会变成List,Java之所以这样实现也就是为了保持向后的兼容性。
现在我们比较一下List和List<Object>这两个类型之间的主要区别,尽管这两个集合可以包含任何类型的对象元素,但是前者是类型不安全的,而后者则明确告诉使用者可以存放任意类型的对象元素。另一个区别是,如果void func(List l)改为void func(List<Object> l),List<String>类型的对象将不能传递给func函数,因为Java将这两个泛型类型视为完全不同的两个类型。
在新代码中不要使用原生类型,这条规则有两个例外,两者都源于“泛型信息可以在运行时被擦除”这一事实。在Class对象中必须要使用原生类型。JLS不允许使用Class的参数化类型。换句话说,List.class, String[].class和int.class都是合法的,但是List<String>.class和List<?>.class则是不合法。这条规则的第二个例外与instanceof操作符相关。由于泛型信息可以在运行时被擦除,因此在泛型类型上使用instanceof操作符是非法的。如:
private void test(Set o) {
if (o instanceof Set) {
Set<?> m = (Set<?>)o;
}
}
二十四、消除非受检警告:
在进行泛型编程时,经常会遇到编译器报出的非受检警告(unchecked cast warnings),如:Set<Lark> exaltation = new HashSet(); 对于这样的警告要尽可能在编译期予以消除。对于一些比较难以消除的非受检警告,可以通过@SuppressWarnings("unchecked")注解来禁止该警告,前提是你已经对该条语句进行了认真地分析,确认运行期的类型转换不会抛出ClassCastException异常。同时要在尽可能小的范围了应用该注解(SuppressWarnings),如果可以应用于变量,就不要应用于函数。尽可能不要将该注解应用于Class,这样极其容易掩盖一些可能引发异常的转换。见如下代码:
public <T> T[] toArray(T[] a) {
if (a.length < size)
return (T[])Arrays.copyOf(elements,size,a.getClass());
System.arraycopy(elements,0,a,0,size);
if (a.length > size)
a[size] = null;
return a;
}
编译该代码片段时,编译器会针对(T[])Arrays.copyOf(elements,size,a.getClass())语句产生一条非受检警告,现在我们需要做的就是添加一个新的变量,并在定义该变量时加入@SuppressWarnings注解,见如下修订代码:
public <T> T[] toArray(T[] a) {
if (a.length < size) {
//TODO: 加入更多的注释,以便后面的维护者可以非常清楚该转换是安全的。
@SuppressWarnings("unchecked") T[] result =
(T[])Arrays.copyOf(elements,size,a.getClass());
return result;
}
System.arraycopy(elements,0,a,0,size);
if (a.length > size)
a[size] = null;
return a;
}
这个方法可以正确的编译,禁止非受检警告的范围也减少到了最小。
为什么要消除非受检警告,还有一个比较重要的原因。在开始的时候,如果工程中存在大量的未消除非受检警告,开发者认真分析了每一处警告并确认不会产生任何运行时错误,然而所差的是在分析之后没有消除这些警告。那么在之后的开发中,一旦有新的警告发生,极有可能淹没在原有的警告中,而没有被开发者及时发现,最终成为问题的隐患。如果恰恰相反,在分析之后消除了所有的警告,那么当有新警告出现时将会立即引起开发者的注意。
二十五、列表优先于数组:
数组和泛型相比,有两个重要的不同点。首先就是数组是协变的,如:Object[] objArray = new Long[10]是合法的,因为Long是Object的子类,与之相反,泛型是不可协变的,如List<Object> objList = new List<Long>()是非法的,将无法通过编译。因此泛型可以保证更为严格的类型安全性,一旦出现插入元素和容器声明时不匹配的现象是,将会在编译期报错。二者的另一个区别是数组是具体化的,因此数组会在运行时才知道并检查它们的元素类型约束。如将一个String对象存储在Long的数组中时,就会得到一个ArrayStoreException异常。相比之下,泛型则是通过擦除来实现的。因此泛型只是在编译时强化类型信息,并在运行时丢弃它们的元素类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行交互。由此可以得出混合使用泛型和数组是比较危险的,因为Java的编译器禁止了这样的使用方法,一旦使用,将会报编译错误。见如下用例:
public void test() {
//这里我们先假设该语句可以通过编译
List<String>[] stringLists = new List<String>[1];
//该语句是正常的,intList中将仅包含值为42的一个整型元素
List<Integer> intList = Arrays.asList(42);
//该语句也是合法的,因为数组支持协变
Object[] objects = stringLists;
//由于泛型对象在运行时是擦除对象类型信息的,擦除后intList将变为List类型
//而objects是Object类型的数组,List本身也是Object的子类,因此下面的语句合法。
objects[0] = intList;
//下面的语句将会抛出ClassCastException异常。很显然stringLists[0]是List<Integer>对象。
String s = stringLists[0].get(0);
}
从以上示例得出,当你得到泛型数组创建错误时,最好的解决办法通常是优先使用集合类型List<E>,而不是数组类型E[]。这样可能会损失一些性能或简洁性,但是换回的却是更高的类型安全性和互用性。见如下示例代码:
static Object reduce(List l, Function f, Object initVal) {
Object[] snapshot = l.toArray();
Object result = initVal;
for (Object o : snapshot) {
return = f.apply(result,o);
}
return result;
}
interface Function {
Object apply(Object arg1,Object arg2);
}
事实上,从以上函数和接口的定义可以看出,如果他们被定义成泛型函数和泛型接口,将会得到更好的类型安全,同时也没有对他们的功能造成任何影响,见如下修改为泛型的示例代码:
static <E> E reduce(List<E> l,Function<E> f,E initVal) {
E[] snapshot = l.toArray();
E result = initVal;
for (E e : snapshot) {
result = f.apply(result,e);
}
return result;
}
interface Function<E> {
E apply(E arg1,E arg2);
}
这样的写法回提示一个编译错误,即E[] snapshot = l.toArray();是无法直接转换并赋值的。修改方式也很简单,直接强转就可以了,如E[] snapshot = (E[])l.toArray();在强转之后,仍然会收到编译器给出的一条警告信息,即无法在运行时检查转换的安全性。尽管结果证明这样的修改之后是可以正常运行的,但是这样的写法确实也是不安全的,更好的办法是通过List<E>替换E[],见如下修改后的代码:
static <E> E reduce(List<E> l,Function<E> f,E initVal) {
E[] snapshot = new ArrayList<E>(l);
E result = initVal;
for (E e : snapshot) {
result = f.apply(result,e);
}
return result;
}
二十六、优先考虑泛型:
如下代码定义了一个非泛型集合类:
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements,2 * size + 1);
}
}
在看与之相对于的泛型集合实现方式:
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new E[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null;
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements,2 * size + 1);
}
}
上面的泛型集合类Stack<E>在编译时会引发一个编译错误,即elements = new E[DEFAULT_INITIAL_CAPACITY]语句不能直接实例化泛型该类型的对象。修改方式如下:elements = (E[])new Object[DEFAULT_INITIAL_CAPACITY],只要我们保证所有push到该数组中的对象均为该类型的对象即可,剩下需要做的就是添加注解以消除该警告:
@SuppressWarning("unchecked")
public Stack() {
elements = (E[])new Object[DEFAULT_INITIAL_CAPACITY];
}
总而言之,使用泛型比使用需要在客户端代码中进行转换的类型来的更加安全,也更加容易。在设计新类型的时候,要确保它们不需要这种转换就可以使用。这通常意味着要把类做成是泛型的。
二十七、优先考虑泛型方法:
和优先选用泛型类一样,我们也应该优先选用泛型方法。特别是静态工具方法尤其适合于范兴华。如Collections.sort()和Collections.binarySearch()等静态方法。见如下非泛型方法:
public static Set union(Set s1, Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
这个方法在编译时会有警告报出。为了修正这些警告,最好的方法就是使该方法变为类型安全的,要将方法声明修改为声明一个类型参数,表示这三个集合的元素类型,并在方法中使用类型参数,见如下修改后的泛型方法代码:
public static <E> Set<E> union(Set<E> s1,Set<E> s2) {
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
和调用泛型对象构造函数来创建泛型对象不同的是,在调用泛型函数时无须指定函数的参数类型,而是通过Java编译器的类型推演来填充该类型信息,见如下泛型对象的构造:
Map<String,List<String>> anagrams = new HashMap<String,List<String>>();
很明显,以上代码在等号的两边都显示的给出了类型参数,并且必须是一致的。为了消除这种重复,可以编写一个泛型静态工厂方法,与想要使用的每个构造器相对应,如:
public static <K,V> HashMap<K,V> newHashMap() {
return new HashMap<K,V>();
}
我们的调用方式也可以改为:Map<String,List<String>> anagrams = newHashMap();
除了在以上的情形下使用泛型函数之外,我们还可以在泛型单例工厂的模式中应用泛型函数,这些函数通常为无状态的,且不直接操作泛型对象的方法,见如下示例:
public interface UnaryFunction<T> {
T apply(T arg);
}
private static UnaryFunction<Object> IDENTITY_FUNCTION
= new UnaryFunction<Object>() {
public Object apply(Object arg) {
return arg;
}
};
@SuppressWarning("unchecked")
public static <T> UnaryFunction<T> identityFunction() {
return (UnaryFunction<T>)IDENTITY_FUNCTION;
}
调用方式如下:
public static void main(String[] args) {
String[] strings = {"jute","hemp","nylon"};
UnaryFunction<String> sameString = identityFunction();
for (String s : strings)
System.out.println(sameString.apply(s));
Number[] numbers = {1,2.0,3L};
UnaryFunction<Number> sameNumber = identityFunction();
for (Number n : numbers)
System.out.println(sameNumber.apply(n));
}
对于该静态函数,如果我们为类型参数添加更多的限制条件,如参数类型必须是Comparable<T>的实现类,这样我们的函数对象便可以基于该接口做更多的操作,而不仅仅是像上例中只是简单的返回参数对象,见如下代码:
public static <T extends Comparable<T>> T max(List<T> l) {
Iterator<T> i = l.iterator();
T result = i.next();
while (i.hasNext()) {
T t = i.next();
if (t.compareTo(result) > 0)
result = T;
}
return result;
}
总而言之,泛型方法就想泛型对象一样,提供了更为安全的使用方式。
二十八、利用有限制通配符来提升API的灵活性:
前面的条目已经解释为什么泛型不支持协变,而在我们的实际应用中可能确实需要一种针对类型参数的特化,幸运的是,Java提供了一种特殊的参数化类型,称为有限制的通配符类型(bounded wildcard type),来处理类似的情况。见如下代码:
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
现在我们需要增加一个方法:
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
如果我们的E类型为Number,而我们却喜欢将Integer对象也插入到该容器中,现在的写法将会导致编译错误,因为即使Integer是Number的子类,由于类型参数是不可变的,因此这样的写法也是错误的。需要进行如下的修改:
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
修改之后该方法便可以顺利通过编译了。因为参数中Iterable的类型参数被限制为E(Number)的子类型即可。
既然有了pushAll方法,我们可能也需要新增一个popAll的方法与之对应,见如下代码:
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
popAll方法将当前容器中的元素全部弹出,并以此添加到参数集合中。如果Collections中的类型参数和Stack完全一致,这样的写法不会有任何问题,然而在实际的应用中,我们通常会将Collection中的元素视为更通用的对象类型,如Object,见如下应用代码:
Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objs = createNewObjectCollection();
numberStack.popAll(objs);
这样的应用方法将会导致编译错误,因为Object和Stack中Number参数类型是不匹配的,而我们对目标容器中对象是否为Number并不关心,Object就已经满足我们的需求了。为了到达这种更高的抽象,我们需要对popAll做如下的修改:
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
修改之后,之前的使用方式就可以顺利通过编译了。因为参数集合的类型参数已经被修改为E(Number)的超类即可。
这里给出了一个助记方式,便于我们记住需要使用哪种通配符类型:
PECS(producer-extends, consumer-super)
解释一下,如果参数化类型表示一个T生产者,就使用<? extends T>,如果它表示一个T消费者,就使用<? super T>。在我们上面的例子中,pushAll的src参数产生E实例供Stack使用,因此src相应的类型为Iterable<? extends E>;popAll的dst参数通过Stack消费E实例,因此dst相应的类型为Collection<? super E>。PECS这个助记符突出了使用通配符类型的基本原则。
在上一个条目中给出了下面的泛型示例函数:
public static <E> Set<E> union(Set<E> s1, Set<E> s2);
这里的s1和s2都是生产者,根据PECS原则,它们的声明可以改为:
public static <E> Set<E> union(Set<? extends E> s1,Set<? extends E> s2);
由于泛型函数在调用时,其参数类型是可以通过函数参数的类型推演出来的,如果上面的函数被如下方式调用时,将会导致Java的编译器无法推演出泛型参数的实际类型,因此引发了编译错误。
Set<Integer> integers = new Set<Integer>();
Set<Double> doubles = new Set<Double>();
Set<Number> numbers = union(integers,doubles);
如果想顺利通过编译并得到正确的执行结果,我们只能通过显示的方式指定该函数类型参数的实际类型,从而避免了编译器的类型参数自动推演,见修改后的代码:
Set<Number> numbers = Union.<Number>union(integers,doubles);
现在我们再来看一下前面也给出过的max方法,其初始声明为:
public static <T extends Comparable<T>> T max<List<T> srcList);
下面是修改过的使用通配符类的声明:
public static <T extends Comparable<? super T>> T max(List<? extends T> srcList);
下面将逐一给出新声明的解释:
1. 函数参数srcList产生了T实例,因此将类型从List<T>改为List<? extends T>;
2. 最初T被指定为扩展Comparable<T>,然而Comparable又是T的消费者,用于比较两个T之间的顺序关系。因此参数化类型Comparable<T>被替换为Comparable<? super T>。
注:Comparator和Comparable一样,他们始终都是消费者,因此Comparable<? super T>优先于Comparable<T>。
二十九、优先考虑类型安全的异构容器:
泛型通常用于集合,如Set和Map等。这样的用法也就限制了每个容器只能有固定数目的类型参数,一般来说,这也确实是我们想要的。然而有的时候我们需要更多的灵活性,如数据库可以用任意多的Column,如果能以类型安全的方式访问所有Columns就好了,幸运的是有一种方法可以很容易的做到这一点,就是将key进行参数化,而不是将容器参数化,见以下代码:
public class Favorites {
public <T> void putFavorite(Class<T> type,T instance);
public <T> T getFavorite(Class<T> type);
}
下面是该类的使用示例:
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class,"Java");
f.putFavorite(Integer.class,0xcafebabe);
f.putFavorite(Class.class,Favorites.class);
String favoriteString = f.getFavorite(String.class);
int favoriteInteger = 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<Class<?>,Object> favorites =
new HashMap<Class<?>,Object>();
public <T> void putFavorite(Class<T> type,T instance) {
if (type == null)
throw new NullPointerException("Type is null");
favorites.put(type,type.cast(instance));
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
可以看出每个Favorites实例都得到一个Map<Class<?>,Object>容器的支持。由于该容器的值类型为Object,为了进一步确实类型的安全性,我们在put的时候通过Class.cast()方法将Object参数尝试转换为Class所表示的类型,如果类型不匹配,将会抛出ClassCastException异常。以此同时,在从Map中取出值对象的时候,由于该对象当前的类型是Object,因此我们需要再次利用Class.cast()函数将其转换为我们的目标类型。
对于Favorites类的put/get方法,有一个非常明显的限制,即我们无法将“不可具体化”类型存入到该异构容器中,如List<String>、List<Integer>等泛型类型。这样的限制主要源于Java中泛型类型在运行时的类型擦出机制,即List<String>.class和List<Integer>.class是等同的对象,均为List.class。如果Java编译器通过了这样的调用代码,那么List<String>.class和List<Integer>.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<String,Operation> stringToEnum = new HashMap<String,Operation>();
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()方法。