引言
这篇文章是对 The Java Programming Language 一书中第11章的总结。这本书是 Java 语言之父写的,每个 Java 从业人员都应该去看看这本书。
为什么引入泛型
举个例子就明白了!没有引入泛型之前,我们用单链表实现队列的代码如下,可以看到每个结点内部的元素是一个 Object 的引用。这给队列提供了很大的自由。然而,这样做会有2个缺陷。第一、假如你这个队列只存 String 对象,那么你每次从队列中取元素时都需要强转成 String 对象,这很不方便!第二、一个程序员很有可能不小心将其它类型的对象,比如 Integer,放到队列中,而这样的错误会一直运行到转换类型时才会被发现。
class Node {
private Node next;
private Object element;
public Node(Object element) {
this.element = element;
}
public Node(Object element, Node next) {
this.element = element;
this.next = next;
}
public Object getElement() {
return element;
}
public void setElement(Object element) {
this.element = element;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
public class SingleLinkQueue {
protected Node head;
protected Node tail;
public void add(Object item) { /* ... */ }
public Object remove() { /* ... */ }
}
为了解决这样的问题,你可能会为每个特定的类型写一个只存储这种类型的队列。但这会导致大量重复的代码,大量基本上一样的类(除了参数和返回类型),且这样做也容易出错。因此,泛型的出现正是解决这样的问题。有了泛型以后,代码就可以写成下面的样子了:
class Node < E > {
private Node < E > next;
private E element;
public Node(E element) {
this.element = element;
}
public Node(E element, Node < E > next) {
this.element = element;
this.next = next;
}
public E getElement() {
return element;
}
public void setElement(E element) {
this.element = element;
}
public Node < E > getNext() {
return next;
}
public void setNext(Node < E > next) {
this.next = next;
}
}
因此,泛型的出现不仅提供了编译期的类型安全,从而让开发人员更少地出错,而且也大大简化了代码量。在 Java 集合类框架中泛型被广泛应用。
自定义泛型
泛型类
下面的代码定义了一个泛型类。从代码中可以看出,Box
类可以得到复用,我们可以将T替换成任何我们想要的类型。这与上面介绍的好处是一样的。代码中的 <T> 叫做 type parameter,而 Box 类叫做 parameterized classes or parameterized types.
public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
Box<String> stringBox = new Box<String>();
integerBox.add(new Integer(10));
stringBox.add(new String("Hello World"));
System.out.printf("Integer Value :%d\n\n", integerBox.get());
System.out.printf("String Value :%s\n", stringBox.get());
}
}
泛型方法
下面是一个泛型方法的代码。这就是制造一个泛型方法的语法,没什么可多说的。这样做的好处也一目了然。
public class GenericMethodTest {
// generic method printArray
public static < E > void printArray( E[] inputArray ) {
// Display array elements
for(E element : inputArray) {
System.out.printf("%s ", element);
}
System.out.println();
}
public static void main(String args[]) {
// Create arrays of Integer, Double and Character
Integer[] intArray = { 1, 2, 3, 4, 5 };
Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };
System.out.println("Array integerArray contains:");
printArray(intArray); // pass an Integer array
System.out.println("\nArray doubleArray contains:");
printArray(doubleArray); // pass a Double array
System.out.println("\nArray characterArray contains:");
printArray(charArray); // pass a Character array
}
}
Bounded Type Parameters
There may be times when you'll want to restrict the kinds of types that are allowed to be passed to a type parameter. For example, a method that operates on numbers might only want to accept instances of Number or its subclasses. This is what bounded type parameters are for. 为了让某个函数只接收某种类型的参数,我们用边界符来定义泛型。下面代码中的 maximum 方法就只能接收 Comparable 及其子类的类型了。
public class MaximumTest {
// determines the largest of three Comparable objects
public static <T extends Comparable<T>> T maximum(T x, T y, T z) {
T max = x; // assume x is initially the largest
if(y.compareTo(max) > 0) {
max = y; // y is the largest so far
}
if(z.compareTo(max) > 0) {
max = z; // z is the largest now
}
return max; // returns the largest object
}
public static void main(String args[]) {
System.out.printf("Max of %d, %d and %d is %d\n\n",
3, 4, 5, maximum( 3, 4, 5 ));
System.out.printf("Max of %.1f,%.1f and %.1f is %.1f\n\n",
6.6, 8.8, 7.7, maximum( 6.6, 8.8, 7.7 ));
System.out.printf("Max of %s, %s and %s is %s\n","pear",
"apple", "orange", maximum("pear", "apple", "orange"));
}
}
下面最后一行代码会被编译器报错。很多人可能会认为 Integer 是 Number 的子类型,根据多态的原理,将子类型传到 sum 中不应该出错呀!但不幸的是,List<Integer> 并不是 List<Number> 的子类型,你可以这样理解,Integer[] 数组 也不是 Number[] 数组的子类型。
static double sum(List <Number> list) {
double sum = 0.0;
for (Number n: list)
sum += n.doubleValue();
return sum;
}
List<Integer> l = new ArrayList<Integer>();
l.add(1);
double sum = sum(l); // INVALID: won't compile
将上面的方法用通配符的方式去声明就可以了。
static double sum(List < ? extends Number > list) {
double sum = 0.0;
for (Number n: list)
sum += n.doubleValue();
return sum;
}
实际上,它们的继承关系如下图所示:
Java 泛型的类型擦除带来的问题
Java 泛型中的类型参数只保留在编译期间,保证代码的类型是正确的。但是在编译成 class 文件过后,类型参数就被擦除了,也就是说,在程序运行时,你不能知道具体是哪个类型参数。通过下面的代码就可以验证类型擦除的事实。一旦编译过后,无论是 ArrayList<Integer> 还是 ArrayList<Float>,最终都是 ArrayList,所以下面的 if 判断是 true.
ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if (li.getClass() == lf.getClass()) { // evaluates to true
System.out.println("Equal");
}
问题一:generic class cannot extend the Throwable class in any way, directly or indirectly
比如下面的第1行代码就是错误的。 Due to type erasure, the runtime will not know which catch block to execute, so this is prohibited by the compiler.
public class GenericException<T> extends Exception
try {
throw new GenericException<Integer>();
}
catch(GenericException<Integer> e) {
System.err.println("Integer");
}
catch(GenericException<String> e) {
System.err.println("String");
}
问题二:不能创建实例
下面有代码是错误的。Java generics generate only one compiled version of a generic class or function regardless of the number of parameterizing types used. Therefore, instantiating a Java class of a parameterized type is impossible because instantiation requires a call to a constructor, which is unavailable if the type is unknown. Java泛型详解 中如何用反射实例化的方案。
<T> T instantiateElementType(List<T> arg) {
return new T(); //causes a compile error
}
问题三:type parameter cannot be used in the declaration of static variables or in static methods
Because there is only one copy per generic class at runtime, static variables are shared among all the instances of the class, regardless of their type parameter.
参考资料