Java 5.0发布了,许多人都将开始使用这个JDK版本的一些新增特性。从增强的for循环到诸如泛型(generic)之类更复杂的特性,都将很快出现在您所编写的代码中。我们刚刚完成了一个基于Java 5.0的大型任务,而本文就是要介绍我们使用这些新特性的体验。本文不是一篇入门性的文章,而是对这些特性以及它们所产生的影响的深入介绍,同时还给出了一些在项目中更有效地使用这些特性的技巧。 简介 增强的for循环 在循环中,初始化表达式只计算一次。这意味着您通常可以移除一个变量声明。在这个例子中,我们必须创建一个整型数组来保存computeNumbers()的结果,以防止每一次循环都重新计算该方法。您可以看到,下面的代码要比上面的代码整洁一些,并且没有泄露变量numbers: 未增强的For: int sum = 0; Integer[] numbers = computeNumbers(); for (int i=0; i < numbers.length ; i++) sum += numbers[i]; 增强后的For: int sum = 0; for ( int number: computeNumbers() ) sum += number;局限性 有时需要在迭代期间访问迭代器或下标,看起来增强的for循环应该允许该操作,但事实上不是这样,请看下面的例子: for (int i=0; i < numbers.length ; i++) { if (i != 0) System.out.print(","); System.out.print(numbers[i]); } 我们希望将数组中的值打印为一个用逗号分隔的清单。我们需要知道目前是否是第一项,以便确定是否应该打印逗号。使用增强的for循环是无法获知这种信息的。我们需要自己保留一个下标或一个布尔值来指示是否经过了第一项。 这是另一个例子: for (Iterator<integer> it = n.iterator() ; it.hasNext() ; ) if (it.next() < 0) it.remove(); 在此例中,我们想从整数集合中删除负数项。为此,需要对迭代器调用一个方法,但是当使用增强的for 循环时,迭代器对我们来说是看不到的。因此,我们只能使用Java 5之前版本的迭代方法。 顺便说一下,这里需要注意的是,由于Iterator是泛型,所以其声明是Iterator<Integer>。许多人都忘记了这一点而使用了Iterator的原始格式。 注释注释处理是一个很大的话题。因为本文只关注核心的语言特性,所以我们不打算涵盖它所有的可能形式和陷阱。 我们将讨论内置的注释(SuppressWarnings,Deprecated和Override)以及一般注释处理的局限性。 Suppress Warnings @SuppressWarnings("deprecation") public static void selfDestruct() { Thread.currentThread().stop(); } 这可能是内置注释最有用的地方。遗憾的是,1.5.0_04的javac不支持它。但是1.6支持它,并且Sun正在努力将其向后移植到1.5中。 Deprecated Override @Override public int hashCode() { ... } 看上面的例子,如果没有在hashCode中将“C”大写,在编译时不会出现错误,但是在运行时将无法像期望的那样调用该方法。通过添加Override标签,编译器会提示它是否真正地执行了重写。 其它注释 public class Foo { @Property private int bar; } 其思想是为私有字段bar自动创建getter和setter方法。遗憾的是,这个想法有两个失败之处:1)它不能运行,2)它使代码难以阅读和处理。 它是无法实现的,因为前面已经提到了,Sun特别阻止了对出现注释的类进行修改。 枚举 public enum DatabaseType { ORACLE { public String getJdbcUrl() {...} }, MYSQL { public String getJdbcUrl() {...} }; public abstract String getJdbcUrl(); }现在枚举类型可以直接提供它的实用方法。例如: DatabaseType dbType = ...; String jdbcURL = dbType.getJdbcUrl(); 要获取URL,必须预先知道该实用方法在哪里。
Log.log(String code) Log.log(String code, String arg) Log.log(String code, String arg1, String arg2) Log.log(String code, String[] args)当讨论可变参数时,比较有趣的是,如果用新的可变参数替换前四个例子,将是兼容的: Log.log(String code, String... args) 所有的可变参数都是源兼容的——那就是说,如果重新编译log()方法的所有调用程序,可以直接替换全部的四个方法。然而,如果需要向后的二进制兼容性,那么就需要舍去前三个方法。只有最后那个带一个字符串数组参数的方法等效于可变参数版本,因此可以被可变参数版本替换。 类型强制转换 如果希望调用程序了解应该使用哪种类型的参数,那么应该避免用可变参数进行类型强制转换。看下面这个例子,第一项希望是String,第二项希望是Exception: Log.log(Object... objects) { String message = (String)objects[0]; if (objects.length > 1) { Exception e = (Exception)objects[1]; // Do something with the exception } }方法签名应该如下所示,相应的可变参数分别使用String和Exception声明: Log.log(String message, Exception e, Object... objects) {...} 不要使用可变参数破坏类型系统。需要强类型化时才可以使用它。对于这个规则,PrintStream.printf()是一个有趣的例外:它提供类型信息作为自己的第一个参数,以便稍后可以接受那些类型。 协变返回 协变返回的基本用法是用于在已知一个实现的返回类型比API更具体的时候避免进行类型强制转换。在下面这个例子中,有一个返回Animal对象的Zoo接口。我们的实现返回一个AnimalImpl对象,但是在JDK 1.5之前,要返回一个Animal对象就必须声明。: public interface Zoo { public Animal getAnimal(); } public class ZooImpl implements Zoo { public Animal getAnimal(){ return new AnimalImpl(); } }协变返回的使用替换了三个反模式:
ZooImpl._animal
((AnimalImpl)ZooImpl.getAnimal()).implMethod();
ZooImpl._getAnimal(); 除了泛型类型,Java 5还引入了泛型方法。在这个来自java.util.Collections的例子中,构造了一个单元素列表。新的List的元素类型是根据传入方法的对象的类型来推断的: static <T> List<T> Collections.singletonList(T o) 示例用法: public List<Integer> getListOfOne() { return Collections.singletonList(1); } 在示例用法中,我们传入了一个int。所以方法的返回类型就是List<Integer>。编译器把T推断为Integer。这和泛型类型是不同的,因为您通常不需要显式地指定类型参数。 emptyList()方法与泛型一起引入,作为java.util.Collections中EMPTY_LIST字段的类型安全置换: static <T> List<T> Collections.emptyList() 示例用法: public List<Integer> getNoIntegers() { return Collections.emptyList(); } 与先前的例子不同,这个方法没有参数,那么编译器如何推断T的类型呢?基本上,它将尝试使用一次参数。如果没有起作用,它再次尝试使用返回或赋值类型。在本例中,返回的是List<Integer>,所以T被推断为Integer。 public List<Integer> getNoIntegers() { return x ? Collections.emptyList() : null; } 因为编译器看不到返回上下文,也不能推断T,所以它放弃并采用Object。您将看到一个错误消息,比如:“无法将List<Object>转换为List<Integer>。” return x ? Collections.<Integer>emptyList() : null; 这种情况经常发生的另一个地方是在方法调用中。如果一个方法带一个List<String>参数,并且需要为那个参数调用这个传递的emptyList(),那么也需要使用这个语法。 集合之外这里有三个泛型类型的例子,它们不是集合,而是以一种新颖的方式使用泛型。这三个例子都来自标准的Java库:
泛型最复杂的部分是对通配符的理解。我们将讨论三种类型的通配符以及它们的用途。 首先让我们了解一下数组是如何工作的。可以从一个Integer[]为一个Number[]赋值。如果尝试把一个Float写到Number[]中,那么可以编译,但在运行时会失败,出现一个ArrayStoreException: Integer[] ia = new Integer[5]; Number[] na = ia; na[0] = 0.5; // compiles, but fails at runtime 如果试图把该例直接转换成泛型,那么会在编译时失败,因为赋值是不被允许的: List<Integer> iList = new ArrayList<Integer>(); List<Number> nList = iList; // not allowed nList.add(0.5); 如果使用泛型,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException。 上限通配符 上限 List<Integer> iList = new ArrayList<Integer>(); List<? extends Number> nList = iList; Number n = nList.get(0); nList.add(0.5); // Not allowed 我们可以从列表中得到Number,因为无论列表的确切元素类型是什么(Float、Integer或Number),我们都可以把它赋值给Number。 在下面这个例子中,通配符用于向API的用户隐藏类型信息。在内部,Set被存储为CustomerImpl。而API的用户只知道他们正在获取一个Set,从中可以读取Customer。 此处通配符是必需的,因为无法从Set<CustomerImpl>向Set<Customer>赋值: public class CustomerFactory { private Set<CustomerImpl> _customers; public Set<? extends Customer> getCustomers() { return _customers; } } 通配符和协变返回 public interface NumberGenerator { public List<? extends Number> generate(); } public class FibonacciGenerator extends NumberGenerator { public List<Integer> generate() { ... } } 如果要使用数组,接口可以返回Number[],而实现可以返回Integer[]。 下限 List<? extends Number> readList = new ArrayList<Integer>(); Number n = readList.get(0); List<? super Number> writeList = new ArrayList<Object>(); writeList.add(new Integer(5)); 第一个是可以从中读数的列表。 无界通配符 公共API中的通配符 void removeNegatives(List<? extends Number> list); 构造泛型类型 集合风格(Collection-like)的函数 public final class Pair<A,B> { public final A first; public final B second; public Pair(A first, B second) { this.first = first; this.second = second; } } 这使从方法返回两个项而无需为每个两种类型的组合编写专用的类成为可能。另一种方法是返回Object[],而这样是类型不安全或者不整洁的。 public Pair<File,Boolean> getFileAndWriteStatus(String path){ // create file and status return new Pair<File,Boolean>(file, status); } Pair<File,Boolean> result = getFileAndWriteStatus("..."); File f = result.first; boolean writeable = result.second; 集合之外 public abstract class DBFactory<T extends DBPeer> { protected abstract T createEmptyPeer(); public List<T> get(String constraint) { List<T> peers = new ArrayList<T>(); // database magic return peers; } } 通过实现DBFactory<Customer>,CustomerFactory必须从createEmptyPeer()返回一个Customer: public class CustomerFactory extends DBFactory<Customer>{ public Customer createEmptyPeer() { return new Customer(); } } 泛型方法 <T> List<T> reverse(List<T> list) 具体化 按照泛型教程的惯例,解决方案使用的是“类型令牌”,通过向构造函数添加一个Class<T>参数,可以强制客户端为类的类型参数提供正确的类对象: public class ArrayExample<T> { private Class<T> clazz; public ArrayExample(Class<T> clazz) { this.clazz = clazz; } public T[] getArray(int size) { return (T[])Array.newInstance(clazz, size); } } 为了构造ArrayExample<String>,客户端必须把String.class传递给构造函数,因为String.class的类型是Class<String>。 结束语 |
|