[翻译]Java 范型与
集合类 :演化 ,而不是革命 (第一部分)
原文地址:http://www.onjava.com/pub/a/onjava/excerpt/javagenerics_chap05/index.html
Java 范型与集合类 :演化 ,而不是革命(第一部分)
作者:Maurice Naftalin 和Philip Wadler
Editor's Note: In their new book Java Generics and Collections, authors Maurice Naftalin and Philip Wadler offer the thorough introduction to the syntax and semantics of Java 5.0 generics that you'd expect from an in-depth book on the topic. But they go a step further by considering the real-world, practical concerns of using generics in your work. Unless you're starting a
project from scratch in Java 5.0, odds are you have a legacy code-base that does not currently use generics. Is bulk-converting it to use generics in one release a realistic option? Assuming it's not, you need to consider your options for a gradual introduction. Fortunately, the implementation of generics makes this eminently practical, as they describe in Chapter 5, "Evolution, Not Revolution," which we are excerpting over the course of the next two weeks on ONJava.
编者的话:作者Maurice Naftalin和Philip Wadler在
他们的新书《Java Generics and Collections》中完整的介绍了Java 5.0 中范型的语法和语义,这些内容正是读者们所期待的关于这个话题的深入的资料。但是此外他们还考虑了读者们在日常工作中使用范型的实际的顾虑。除非你是用Java 5.0 从头开发一个项目,否则的话你将会有很大的可能遇到一堆没有使用范型的旧的代码库。难道在一个release
版本中将这样多的代码全部改进为使用范型的是一个现实的选择吗?如果它不是,你就需要考虑逐渐的改进你的代码。幸运的是,Java中范型的实现使得这样的选择是相当可行的,正如两位作者在该书第五章中提到的“演化,而不是革命”,这就是OnJava网站在接下来两周的课程中将不断重复提到的。
One motto underpinning the design of generics for Java is evolution, not revolution. It must be possible to migrate a large, existing body of code to use generics gradually (evolution) without requiring a radical, all-at-once change (revolution). The generics design ensures that old code compiles against the new Java libraries, avoiding the unfortunate situation in which half of your code needs old libraries and half of your code needs new libraries.
在Java中范型的设计过程中,不断提到的设计思想就是“演化,而不是革命”。必须使得人们可以将大量的现存的代码体逐步转移到使用范型上(演化),而不需要突然的变动(革命)。范型的设计原则保证了那些旧的代码可以在新的Java类库下正常编译,避免那种一半代码需要旧的类库而一半代码需要新的类库的情况。
The requirements for evolution are much stronger than the usual backward compatibility. With simple backward compatibility, one would supply both legacy and generic versions for each application; this is exactly what happens in C#, for example. If you are building on top of code supplied by multiple suppliers, some of whom use legacy collections and some of whom use generic collections, this might rapidly lead to a versioning nightmare.
演化的要求比通常的“向下兼容”的要求高了许多。在简单的“向下兼容”情况下,只需要为每一个应用提供遗留的版本和范型的版本,例如在C#语言中就是这种情况。如果需要在多个提供商提供的代码的基础上来构建一个项目,而一些提供商使用旧有的集合类而其他一些使用范型的集合类,这样迅速导致一个关于版本的噩梦。
What we require is that the same client code works with both the legacy and generic versions of a library. This means that the supplier and clients of a library
can make completely independent choices about when to move from legacy to generic code. This is a much stronger requirement than backward compatibility; it is called migration compatibility or platform compatibility.
我们所要求的就是相同的客户代码可以同时在旧的和新的范型的库的基础上裕兴。这就意味这提供商和客户可以完全自主的选择何时从旧的类库转移到新的范型类库的使用上;这被叫做“迁移兼容性”或者“平台兼容性”
Java implements generics via erasure, which ensures that legacy and generic versions usually generate identical class
files, save for some auxiliary information about types. It is possible to replace a legacy class file by a generic class file without changing, or even recompiling, any client code; this is called binary compatibility.
Java通过“擦除”技术来实现了范型,“擦除”意味这旧的类库和范型的版本一般都生成同样的class文件(除了一些关于类型的辅助信息的不同)。这样就可以用一个范型的class文件来替换就的class文件而不需要做任何变化,甚至是重新编译任何代码;这叫做“二进制文件兼容”
We summarize this with the motto binary compatibility ensures migration compatibility—or, more concisely, erasure eases evolution.
我们用名词“二进制兼容保证了迁移兼容性-或者更确切的说是,擦除简化了演化”来概括这个特性。
This section shows how to add generics to existing code; it considers a small example, a library for stacks that extends the Collections
Framework, together with an associated client. We begin with the legacy stack library and client (written for Java before generics), and then present the corresponding generic library and client (written for Java with generics). Our example code is small, so it is easy to update to generics all in one go, but in practice the library and client will be much larger, and we may want to evolve them separately. This is aided by raw types, which are the legacy counterpart of parameterized types.
这个部分将介绍如何向已有的代码中引入范型;我们来看一个小
例子,一个继承了集合框架的
堆栈库,以及一个相应的客户端代码。我们从这个旧的堆栈的库和代码(用引入范型以前的Java写的)开始,然后展现对应的使用了范型的库和代码(用引入了范型的Java写的)。我们的样例代码很小,所以很容易就可以一次性的更新到范型上,但是在实际中类库和客户端代码都会很大,所以我们可能向要分开的改进他们。这是在raw类型的帮助下完成的,raw类型就是参数类型的旧的对应。
The parts of the program may evolve in either order. You may have a generic library with a legacy client; this is the common case for anyone that uses the Collections Framework in Java 5 with legacy code. Or you may have a legacy library with a generic client; this is the case where you want to provide generic signatures for the library without the need to rewrite the entire library. We consider three
ways to do this: minimal changes to the source, stub files, and wrappers. The first is useful when you have access to the source and the second when you do not; we recommend against the third.
程序的各个部分可能以不同的顺序演化。我们可能是拥有一个范型的库和一个旧的客户端,这种情况对于那些使用java5用的范型框架以及旧的代码的人来说是很常见的。或者你可能有一个旧的库和范型的客户端,这种情况是你
打算为库提供范型的签名而不用重写整个库。我们考虑以下三种情况:对源代码做最小的变动,存根文件和包装器。第一种情况当你能够获取源代码的时候是很有用的,第二种是你不能获取源代码时候。我们推荐不使用第三种。
In practice, the library and client may involve
many interfaces and classes, and there may not even be a clear distinction between library and client. But the same principles discussed here still apply, and may be used to evolve any part of a program independently of any other part.
在实际中,库和客户端代码可能拥有许多的
接口和类,并且在库和客户端直面没有明显的区分。但是在这种情形下,我们讨论的原则仍然是适用的,并且可以用于演化程序的一部分,而不影响其他的部分。
Legacy Library with Legacy Client
旧的库和旧的客户端
We begin with a simple library of stacks and an associated client, as presented in Example 5.1. This is legacy code, written for Java 1.4 and its version of the Collections Framework. Like the Collections Framework, we structure the library as an interface Stack (analogous to List), an implementation class ArrayStack (analogous to ArrayList), and a utility class Stacks (analogous to Collections). The interface Stack provides just three methods: empty, push, and pop. The implementation class ArrayStack provides a single constructor with no arguments, and implements the methods empty, push, and pop using methods size, add, and remove on lists. The body of pop could be shorter—instead of assigning the value to the variable, it could be returned directly—but it will be interesting to see how the type of the variable changes as the code evolves. The utility class provides just one method, reverse, which repeatedly pops from one stack and pushes onto another.
我们从一个简单的堆栈的库和相应的客户端来开始,如例5-1所示。这是用java1.4以及其包含的集合类来写的旧的代码。和集合框架一样,我们把类库分为一个Stack接口(和List很相似),以及一个实用类Stacks(和Collections相似)。Stack接口只提供了三个方法:emtpy,push和pop.实现类ArrayStack提供了一个不带参数的
构造函数,并实用lists里面的size,add,remove函数实现了empty,pop和push函数。Pop函数体可以更短些的(可以直接返回值的,而不是把值赋值给一个变量),但是现在的这种写法更有趣,我们可以看到随着代码的演化,变量的类型是如何的变化的。工具类只提供了一个方法reverse,这个在一个堆栈上重复pop操作并把值push到另外一个里面去。
Example 5-1. Legacy library with legacy client
l/Stack.java:
interface Stack {
public boolean empty();
public void push(Object elt);
public Object pop();
}
l/ArrayStack.java:
import java.util.*;
class ArrayStack implements Stack {
private List list;
public ArrayStack() { list = new ArrayList(); }
public boolean empty() { return list.size() == 0; }
public void push(Object elt) { list.add(elt); }
public Object pop() {
Object elt = list.remove(list.size()-1);
return elt;
}
public String toString() { return "stack"+list.toString(); }
}
l/Stacks.java:
class Stacks {
public static Stack reverse(Stack in) {
Stack out = new ArrayStack();
while (!in.empty()) {
Object elt = in.pop();
out.push(elt);
}
return out;
}
}
l/Client.java:
class Client {
public static void main(String[] args) {
Stack stack = new ArrayStack();
for (int i = 0; i<4; i++) stack.push(new Integer(i));
assert stack.toString().equals("stack[0, 1, 2, 3]");
int top = ((Integer)stack.pop()).intValue();
assert top == 3 && stack.toString().equals("stack[0, 1, 2]");
Stack reverse = Stacks.reverse(stack);
assert stack.empty();
assert reverse.toString().equals("stack[2, 1, 0]");
}
}
The client allocates a stack, pushes a few integers onto it, pops an integer off, and then reverses the remainder into a fresh stack. Since this is Java 1.4, integers must be explicitly boxed when passed to push, and explicitly unboxed when returned by pop.
客户端代码定义了一个堆栈,将一些int数值放进去,然后将pop一些出来,最后将剩下的数reverse到一个新的Stack中。因为是在java1.4下写的代码,interger变量在push之前必须显式的用Integer包装下,然后在pop之前显式的解包装。
Generic Library with Generic Client
范型的库和范型的客户端
Next, we update the library and client to use generics, as presented in Example 5.2. This is generic code, written for Java 5 and its version of the Collections Framework. The interface now takes a type parameter, becoming Stack<E> (analogous to List<E>), and so does the implementing class, becoming ArrayStack<E> (analogous to ArrayList<E>), but no type parameter is added to the utility class Stacks (analogous to Collections). The type Object in the signatures and bodies of push and pop is replaced by the type parameter E. Note that the constructor in ArrayStack does not require a type parameter. In the utility class, the reverse method becomes a generic method with argument and result of type Stack<T>. Appropriate type parameters are added to the client, and boxing and unboxing are now implicit.
接下来,我们使用范型来更新我们的库和客户端,如例5-2所示。这是用Java 5及其所包含的集合框架来写的代码。现在的Stack接口带了一个类型参数,变成了Stack<E>(和List<E>相似),因此其实现类变成了ArrayStack<E>(和ArrayList<E>相似),但是工具类Stacks没有增加范型参数的(和Collections类相似)。在pop和push函数的申明和体中的Object对象也用类型参数E代替了。注意,ArrayStack的构造函数不需要一个类型参数。在实用类中,reverse方法也成为了一个带类型参数的范型的方法,返回值变成了Stack<T>。在客户端代码中也加入了合适的范型参数,并且int数值的包装和解包装不用显式的写出来了。
Example 5-2. Generic library with generic client
g/Stack.java:
interface Stack<E> {
public boolean empty();
public void push(E elt);
public E pop();
}
g/ArrayStack.java:
import java.util.*;
class ArrayStack<E> implements Stack<E> {
private List<E> list;
public ArrayStack() { list = new ArrayList<E>(); }
public boolean empty() { return list.size() == 0; }
public void push(E elt) { list.add(elt); }
public E pop() {
E elt = list.remove(list.size()-1);
return elt;
}
public String toString() { return "stack"+list.toString(); }
}
g/Stacks.java:
class Stacks {
public static <T> Stack<T> reverse(Stack<T> in) {
Stack<T> out = new ArrayStack<T>();
while (!in.empty()) {
T elt = in.pop();
out.push(elt);
}
return out;
}
}
g/Client.java:
class Client {
public static void main(String[] args) {
Stack<Integer> stack = new ArrayStack<Integer>();
for (int i = 0; i<4; i++) stack.push(i);
assert stack.toString().equals("stack[0, 1, 2, 3]");
int top = stack.pop();
assert top == 3 && stack.toString().equals("stack[0, 1, 2]");
Stack<Integer> reverse = Stacks.reverse(stack);
assert stack.empty();
assert reverse.toString().equals("stack[2, 1, 0]");
}
}
In short, the conversion process is straightforward: just add a few type parameters and replace occurrences of Object by the appropriate type variable. All differences between the legacy and generic versions can be spotted by comparing the highlighted portions of the two examples. The implementation of generics is designed so that the two versions generate essentially equivalent class files. Some auxiliary information about the types may differ, but the actual bytecodes to be executed will be identical. Hence, executing the legacy and generic versions yields the same results. The fact that legacy and generic sources yield identical class files eases the process of evolution, as we discuss next.
简而言之,这个转换的过程是直接的:仅仅增加了一些类型参数,并把原先的Object用合适的类型变量代替了。
Generic Library with Legacy Client
范型的类库和旧的客户端
Now let's consider the case where the library is updated to generics while the client remains in its legacy version. This may occur because there is not enough time to convert everything all at once, or because the library and client are controlled by different organizations. This corresponds to the most important case of backward compatibility, where the generic Collections Framework of Java 5 must still work with legacy clients written against the Collections Framework in Java 1.4.
现在让我们考虑这样的一种情况:类库被更新到范型了,但是客户端仍然是旧的版本。这种情况的发生可能是因为没有足够的时间同时转换所有的代码,或是因为类库和客户端是由不同的公司控释的。这种情况就是向后兼容中最重要的一种情形了,即Java 5的范型集合框架必须仍然能够与用Java 1.4中的集合框架写的旧的客户端代码一起工作。
In order to support evolution, whenever a parameterized type is defined, Java also recognizes the corresponding unparameterized version of the type, called a raw type. For instance, the parameterized type Stack<E> corresponds to the raw type Stack, and the parameterized type ArrayStack<E> corresponds to the raw type ArrayStack.
为了能够支持演化,无论何时定义一个参数化的类型,Java必须能够识别出对应的未参数化版本的类型,叫做raw类型。例如,参数化的类型Stack<E>对应的raw类型Stack,以及ArrayStack<E>对应的ArrayStack。
Every parameterized type is a subtype of the corresponding raw type, so a value of the parameterized type can be passed where a raw type is expected. Usually, it is an error to pass a value of a supertype where a value of its subtype is expected, but Java does permit a value of a raw type to be passed where a parameterized type is expected—however, it flags this circumstance by generating an unchecked conversion warning. For instance, you can assign a value of type Stack<E> to a variable of type Stack, since the former is a subtype of the latter. You can also assign a value of type Stack to a variable of type Stack<E>, but this will generate an unchecked conversion warning.
每一个参数化的类型都有一个对应的raw类型的子类型(subtype),一次老公一个参数化类型的值可以传递到任何期待raw类型的地方。通常,向一个期待subtype的地方传递一个supertype的值的时候会发生
错误,但是Java的确允许向一个其他参数化类型的地方传递一个raw类型的值。然而,在这种情况下需要产生一个“unchecked conversion”的警告。例如,可以将一个Stack<E>的值赋给Stack类型,因为前者是后者的一个子类型。同样可以将一个Stack类型的值赋给类型为Stack<E>的变量,但是这将产生一个“unchecked conversion”的警告。
To be specific, consider compiling the generic source for Stack<E>, ArrayStack<E>, and Stacks from Example 5.2 (say, in directory g) with the legacy source for Client from Example 5.1 (say, in directory l). Sun's Java 5 compiler yields the following message:
具体来说,我们来看一下在编译例5-2(在目录g下)中Stack<E>,ArrayStack<E>和Stacks类的范型代码以及例5-1(在目录l中)中的旧的客户端代码这样一种情形。Sun的Java 5编译器将会产生如下的信息:
% javac g/Stack.java g/ArrayStack.java g/Stacks.java l/Client.java
Note: Client.java uses unchecked or unsafe
operations.
Note: Recompile with -Xlint:unchecked for details.
The unchecked warning indicates that the compiler cannot offer the same safety guarantees that are possible when generics are used uniformly throughout. However, when the generic code is generated by updating legacy code, we know that equivalent class files are produced from both, and hence (despite the unchecked warning) running a legacy client with the generic library will yield the same result as running the legacy client with the legacy library. Here we assume that the only change in updating the library was to introduce generics, and that no change to the behavior was introduced, either on purpose or by mistake.
Unchecked警告指出编译器在范型没有一致的使用时不能提供相同的安全保证。然而,当范型的代码是通过更新旧的代码来产生的时候,我们知道相应的class文件是新的和旧的代码同时产生的,因此(尽管有unchecked警告),在范型的库上运行旧的客户端代码将会和在旧的库上运行旧的代码产生相同的结果。在这里,我们假设在更新类库的过程中,我们只是引入了范型,而没有对函数做任何功能上的改变,不管是故意的还是犯了错误。
If we follow the suggestion above and rerun the compiler with the appropriate switch enabled, we get more details:
% javac -Xlint:unchecked g/Stack.java g/ArrayStack.java \
% g/Stacks.java l/Client.java
l/Client.java:4: warning: [unchecked] unchecked call
to push(E) as a member of the raw type
Stack
for (int i = 0; i<4; i++) stack.push(new Integer(i));
^
l/Client.java:8: warning: [unchecked] unchecked conversion
found : Stack
required: Stack<E>
Stack reverse = Stacks.reverse(stack);
^
l/Client.java:8: warning: [unchecked] unchecked method invocation:
<E>reverse(Stack<E>) in Stacks is applied to (Stack)
Stack reverse = Stacks.reverse(stack);
^
3 warnings
Not every use of a raw type gives rise to a warning. Because every parameterized type is a subtype of the corresponding raw type, but not conversely, passing a parameterized type where a raw type is expected is safe (hence, no warning for getting the result from reverse), but passing a raw type where a parameterized type is expected issues a warning (hence, the warning when passing an argument to reverse); this is an instance of the Substitution Principle. When we invoke a method on a receiver of a raw type, the method is treated as if the type parameter is a wildcard, so getting a value from a raw type is safe (hence, no warning for the invocation of pop), but putting a value into a raw type issues a warning (hence, the warning for the invocation of push); this is an instance of the Get and Put Principle.
不是所有使用raw类型的地方都回产生一个警告。因为每一个参数化的类型都是对应的raw类型的一个子类型,但是相反的情况不不是一样的。向期待一个raw类型的地方传递一个参数化的类型是安全的(因此reverse函数的结果不会产生警告),但是向期待一个参数化类型的地方那个传递一个raw类型将会产生一个警告(因此,向reverse函数传递一个参数将会产生警告);这就是置换原则的一个例子。当我们调用一个需要raw类型的函数,这个方法将被这样处理:类型参数是一个wildcard,因此从一个raw类型获取一个值是安全的(因此调用pop函数没有产生任何警告),但是将一个值放到raw类型中将会产生一个警告(因此调用push函数将会产生一个警告)。这就是“Get/Put”原则的一个例子。
Even if you have not written any generic code, you may still have an evolution problem because others have generified their code. This will affect everyone with legacy code that uses the Collections Framework, which
has been generified by Sun. So the most important case of using generic libraries with legacy clients is that of using the Java 5 Collections Framework with legacy code written for the Java 1.4 Collections Framework.
即使你没有写任何范型的代码,你仍然可能碰到演化的问题,因为其他人可能范型了他们的代码。这样影响任何使用Sun公司范型化了的集合框架书写的代码。因此使用范型的类库和旧的客户端代码的情形中最重要的就是使用Java 5的集合框架和用Java 1.4集合框架书写的旧的代码。
In particular, applying the Java 5 compiler to the legacy code in Example 5.1 also issues unchecked warnings, because of the uses of the generified class ArrayList from the legacy class ArrayStack. Here is what happens when we compile legacy versions of all the files with the Java 5 compiler and libraries:
特别的,用java 5的编译器来编译例5.1中的旧的代码也会产生unchecked警告,因为在旧的ArrayStack类中使用了范型的ArrayList。下面就是我们用Java 5编译器来编译旧版本的所有文件时候产生的信息:
% javac -Xlint:unchecked l/Stack.java l/ArrayStack.java \
% l/Stacks.java l/Client.java
l/ArrayStack.java:6: warning: [unchecked] unchecked call to add(E)
as a member of the raw type java.util.List
public void push(Object elt) list.add(elt);
^
1 warning
Here the warning for the use of the generic method add in the legacy method push is issued for reasons similar to those for issuing the previous warning for use of the generic method push from the legacy client.
在这儿,使用旧的push方法中增加的范型的方法产生的警告和从旧的客户端代码中使用范型的push方法产生的警告是相似的。
It is poor practice to configure the compiler to repeatedly issue warnings that you intend to ignore. It is distracting and, worse, it may lead you to ignore warnings that require attention—just as in the fable of the little boy who cried wolf. In the case of pure legacy code, such warnings can be turned off by using the -source 1.4 switch:
配置编译器来反复的产生希望忽略的警告是一个不好的实践。这是一种发疯的行为,更糟的是,这可能导致你忽略那些需要注意的警告,就像那个“狼来了”的寓言。在纯旧的代码的情况下,可以通过使用“-source 1.4”来关闭这样的警告。
% javac -source 1.4 l/Stack.java l/ArrayStack.java \
% l/Stacks.java l/Client.java
This compiles the legacy code and issues no warnings or errors. This method of turning off warnings is only applicable to true legacy code, with none of the features introduced in Java 5, generic or otherwise. One can also turn off unchecked warnings by using annotations, as described in the next section, and this works even with features introduced in Java 5.
这样我们就可以编译旧的代码,并且不产生任何警告或者错误了。这种关闭警告的方法只使用与纯旧的代码,没有使用Java 5中新引入的范型或者其他任何新特性的时候。我们还可以通过使用annotations来关闭unchecked警告,这种方法将在下一个章节中介绍,并且这个方法可以在使用了Java 5特性的时候使用。
Maurice Naftalin is Director of Software Development at Morningside Light Ltd., a software consultancy in the United Kingdom.
Maurice Naftalin是
英国的软件咨询公司Morningside Light有限公司的软件开发主管
Philip Wadler is a professor of theoretical computer science at the University of Edinburgh, Scotland, where his research focuses on
functional and logic programming.
Philip Wadler是苏格兰爱丁堡大学的理论计算机科学的一个教授,他主要关注functional和logic编程。