使用泛型擦除有一些好处:它并有创造一种本质上新的概念,比如List
另一个泛型擦除带来的结果是,数组类型与参数化类型的不同。比如执行:new String[size],分配一个数组,并且其中保存了表明它元素类型为String的指示信息。而new ArrayList
简单提一点,Java泛型与C++模板在语法语义方面是不同的,具体不再介绍。
子类父类本身是很容易理解的,比如
Integer is a subtype of Number
Double is a subtype of Number
ArrayList
List
Collection
但是一些泛型的情况里面,有些地方就很容易混淆。看下面几种情况:
List
nums.add(2);
nums.add(3.14);
上面代码中,把2(Integer,涉及到自动装箱boxing)和3.14(Double)加入到nums里面是允许的,因为二者都是Number的子类。把ArrayList
继续看下面的情况:
List
ints.add(1);
ints.add(2);
List
nums.add(3.14);
上面的代码中出现了编译错误,
首先来看一下Collection接口中的addAll()方法:
interface Collection
...
public boolean addAll(Collection extends E> c);
...
}
使用通配符?后,方法参数既可以是元素类型为E的容器,也可以是元素类型为E的子类的容器。看个例子:
List
List
List
nums.addAll(ints);
nums.addAll(dbls);
上面的代码是正确的,因为Integer和Double都是Number的子类,而addAll()方法使用了通配符。这里其实弥补了我们前面说的List
List
ints.add(1);
ints.add(2);
List extends Number> nums = ints;
nums.add(3.14); // compile-time error
前面的时候,是第四行报错,现在第四行正确了(因为ints现在是nums的子类),第五行报错,为什么?这就好比化学实验室有一些试剂瓶,如果一个瓶子我们只知道它是装液体的( extends 液体>),那么我们是否能能把硫酸倒进去(.add(硫酸))?不能!万一它是盛蒸馏水的呢,那不就造成试剂污染了,对吧?那么怎么才能添加呢?需要使用super通配符。
先来看一下Collection中的方法:
public static
for (int i = 0; i < src.size(); i++) {
dst.set(i, src.get(i));
}
}
? super T意味着dst中可能包含任何元素,这些元素是T的父类,而源src中的都是T的子类型。也就是说目的容器中的元素类型肯定是源容器中元素的父类型,前面说过这是可以的。
下面是调用这个函数的一个例子:
List
List
Collections.copy(objs, ints);
其中类型参数可以隐式的通过编译器推断,也可以显示的指出,下面几种调用方法都是可以的:
Collections.copy(objs, ints);
Collections.
Collections.
Collections.
下面体会一下使用通配符的好处:
public static
public static
public static
public static
具体不分析了,记住:“Always use wildcards where you can in a signature, since this permits the widest range of calls.”
至于什么时候使用super什么时候使用extends,或者何时两者都不适用,有个原则:
存取原则:如果只是取数,用extend;如果只是存数,用super;如果既要取数又要存数就用特定类型T。
注意一点:使用?extend的也不是任意元素都不能放,像是null就可以;使用?super的也不是任何元素都可以不可以取,比如Object类型的就可以取。
例如:
List
ints.add(1);
ints.add(2);
List extends Number> nums = ints;
nums.add(null); // ok
List
List super Integer> ints = objs;
String str = "";
for (Object obj : ints) str += obj.toString();
? extends T <==> [null,T]
? super T <==> [T,Object]
最后,我们知道String是final类型的,没有子类。那么List
在Java中,如果A是B的子类,那么A[]也是B[]的子类。看下面的代码:
Integer[] ints = new Integer[] {1,2,3};
Number[] nums = ints;
nums[2] = 3.14; // array store exception
这里的异常是在运行时抛出的,它能通过编译,因为它符合子类对象赋给父类引用的规则。但是前面也说过,数组类型会存储元素类型信息,每次向其中添加元素时,都会进行检查,如果不兼容就会抛出异常。
重写上面的代码:
List
List
nums.set(2, 3.14);
第二行出错,因为List
通配符与类型参数:
看一下Java Collection中的方法:
interface Collection
...
public boolean contains(Object o);
public boolean containsAll(Collection> c);
...
}
Collection>是Collection extends Object>的缩写。
这样使用:
Object obj = "one";
List
List
assert objs.contains(obj);
assert objs.containsAll(ints);
assert !ints.contains(obj);
assert !ints.containsAll(objs);
最后两句(int包含Object)看起来不合常理(这样设计主要是为了兼容泛型使用之前的代码)。而且某种情况下,它还有可能是true,比如
Object obj = 1;
List
List
assert ints.contains(obj);
assert ints.containsAll(objs);
还可以这样设计上面的方法:
interface MyCollection
...
public boolean contains(E o);
public boolean containsAll(Collection extends E> c);
...
}
这样使用:
Object obj = "one";
MyList
MyList
assert objs.contains(obj);
assert objs.containsAll(ints)
assert !ints.contains(obj); // compile-time error
assert !ints.containsAll(objs); // compile-time error
最后两句编译错误。
现在,哪个更好呢,第一个适合更多的情况,而第二个在编译阶段发现更多的错误。Java的设计者选择了第一种,主要是考虑到了兼容性,因为泛型加入之前的用户可能会写这样的代码:ints.containsAll(objs)。如果是自己实现一个向前兼容不是很重要的类,后者更好一些。
通配符捕获
Comparable
interface Comparable
public int compareTo(T o);
}
一个类实现了Comparable
比较的时候只能在同一个类的对象之间比较,比如
Integer int0 = 0;
Integer int1 = 1;
assert int0.compareTo(int1) < 0;
String str0 = "zero";
String str1 = "one";
assert str0.compareTo(str1) > 0;
但是如果整数与字符串比较,就会编译错误
Integer i = 0;
String s = "one";
assert i.compareTo(s) < 0; // compile-time error
下面是一些注意事项:
compareTo要与equal一致:一般情况下我们要求两个对象相等当且仅当他们经过比较是相同的,即x.equals(y) if and only if x.compareTo(y) == 0。特别是使用SortedSet和SortedMap接口的时候。这两个接口都使用自然序比较元素,所以如果两个元素尽管通过equals比较不同但是使用compareTo比较相同,也仅有一个能得到存储。
Java核心类库中的大多数类的自然序和equals都是一致的,但是也有例外,比如java.math.BigDecimal,只要两个数的数值一样(精度可能不同),compareTo就返回相同,像是3.0与3.00。
还要注意,如果x不为空,x.equals(null)一定返回false;x.compareTo(null)抛出NullPointerException.
两个数比较的时候尽量使用标准形式 x.compareTo(y) <= 0 而不是 x<=y。
Comparable的约定:
1.反对称性:sgn(x.compareTo(y)) == -sgn(y.compareTo(x))(其中sgn是符号函数),也就是说x < y 当且仅当 y > x,同时x.compareTo(y)抛出异常当且仅当y.compareTo(x)抛出异常。
2.传递性:if x.compareTo(y) < 0 and y.compareTo(z) < 0 then x.compareTo(z) < 0
3.比较的一致性:if x.compareTo(y) == 0 then sgn(x.compareTo(z)) == sgn(y.compareTo(z))
强烈建议的是 x.equals(y) 当且仅当 x.compareTo(y) == 0。由上面的讨论也知道x.compareTo(x)==0.
在实现Comparable接口的时候,有些地方尤其要注意,看下面的代码:
class Integer implements Comparable
...
public int compareTo(Integer that) {
return this.value < that.value ? -1 :
this.value == that.value ? 0 : 1 ;
}
...
}
这段代码是正确的,再看下面的代码:
class Integer implements Comparable
...
public int compareTo(Integer that) {
// bad implementation -- don't do it this way!
return this.value - that.value;
}
...
}
这段代码在大多数情况下也是正确的,但是当发生溢出的时候,就不正确了。比如一个负的很大值-正的很大的值,结果超过Integer.MAX_VALUE就会产生溢出。
===============================================================================================
下面是找最大值的代码:
public static
T candidate = coll.iterator().next();
for (T elt : coll) {
if (candidate.compareTo(elt) < 0) candidate = elt;
}
return candidate;
}
需要注意的是加粗的部分,有两点需要注意,一是对于类型参数T不管后面是接口还是类都要使用extends,二是只能使用extends不能使用super,这点与通配符有区别。另外,这里对T的约束是一种递归约束。甚至有些情况下,还可以是相互递归约束,比如
看一个例子,有一个水果类Fruit,两个子类Apple,Orange。如果想只能苹果之间比较,橘子之间比较,可以这样实现(其中比较大小是根据水果的大小)
class Fruit {...}
class Apple extends Fruit implements Comparable
class Orange extends Fruit implements Comparable
如果想苹果、橘子之间也可以比较,可以这样
class Fruit implements Comparable
class Apple extends Fruit {...}
class Orange extends Fruit {...}
现在假设有这样的函数签名
我们已经知道泛型是采用擦除策略实现的,也就是说我们写的泛型代码在编译后与没有采用泛型的代码是几乎一样的。当我们实现带类型参数的接口时,编译器会为我们添加额外的方法,这个方法叫做桥。
比如,下面两段代码
interface Comparable{
public int compareTo(Object o);
}
class Integer implements Comparable{
private final int value;
public Integer(int value) { this.value = value; }
public int compareTo(Integer i) {
return (value < i.value) ? -1 : (value == i.value) ? 0 : 1;
}
public int compareTo(Object o) {
return compareTo((Integer)o);
}
}
我们必须提供compareTo(Object o)方法,因为要覆盖接口中的方法,要求两个方法的签名完全一样。在使用泛型之后,看下面的代码:
interface Comparable {
public int compareTo(T o);
}
class Integer implements Comparable {
private final int value;
public Integer(int value){ this.value = value; }
public int compareTo(Integer i){
return (value < i.value) ? -1 :(value == i.value) ? 0 : 1;
}
}
上面代码中没有显示的声明compareTo(Object o),但是由于泛型擦除的原因,编译器会为他添加额外的方法,使用反射可以查看他的方法:
for (Method m : Integer.class.getMethods())
if (m.getName().equals("compareTo"))
System.out.println(m.toGenericString());
执行结果:
public int Integer.compareTo(Integer)
public bridge int Integer.compareTo(java.lang.Object)
在Java1.4及之前,重载一个方法要求参数列表和返回值类型完全一致。Java1.5中只要参数列表一致,返回值可以是被重载方法返回值的子类型。
下面使用clone()方法说明它的优势:
class Object {
...
public Object clone() { ... }
}
Java1.4之前,必须这样重载clone()
class Point {
public int x;
public int y;
public Point(int x, int y) { this.x=x; this.y=y; }
public Object clone() { return new Point(x,y); }
}
使用的时候要强制转换,
Point p = new Point(1,2);
Point q = (Point)p.clone();
Java1.5中,可以这样重载clone()
class Point {
public int x;
public int y;
public Point(int x, int y) { this.x=x; this.y=y; }
public Point clone() { return new Point(x,y); }
}
使用时不需要强制转换:
Point p = new Point(1,2);
Point q = p.clone();
协变重写也是使用桥技术来实现的,我们可以使用反射查看Point中的所有clone方法:
for (Method m : Point.class.getMethods())
if (m.getName().equals("clone"))
System.out.println(m.toGenericString());
执行结果:
public Point Point.clone()
public bridge java.lang.Object Point.clone()
这一节主要是如何声明泛型类,包括构造函数、静态成员、内部类、泛型擦除过程。
泛型类中,类型参数出现在类的头部而不是构造函数中
class Pair
public Pair(T first, U second) { this.first=first; this.second=second; }
}
当构造函数被调用的时候,实际的参数会传递进去:Pair
常见的一个错误时,实例化时忘记传递具体类型,如 Pair
“The erasure of a type is defined as follows: drop all type parameters from parameterized types, and replace any type variable with the erasure of its bound, or with Object if it has no bound, or with the erasure of the leftmost bound if it has multiple bounds.”
下面是一些例子:
1.List> 擦除为 List
2.List
3.List擦除后还是List
4.Integer擦除后为其本身
5.在asList函数声明中类型T擦除为Object,因为它没有约束限制
6.在max函数声明中T被擦除为Comparable,因为T的限制是Comparable super T>
7.max函数第二个版本的声明中,T被擦除为Object,因为T的限制是Object & Comparable
8.copy函数中的S,T分别被擦除为Readable 和 Appendable,因为S的约束是Readable & Closeable,T的约束是Appendable & Closeable
而且我们知道在Java中两个不同的函数不能有相同的函数签名,结合到擦除的知识,有的函数声明就是不正确的,比如:
class Overloaded2 {
// compile-time error, cannot overload two methods with same erasure
public static boolean allZero(List ints) {
for (int i : ints) if (i != 0) return false;
return true;
}
public static boolean allZero(List strings) {
for (String s : strings) if (s.length() != 0) return false;
return true;
}
}
错误原因是两个函数擦除后具有相同的签名 boolean allZero(List)。
最后看一种稍微特殊一点的情况:
class Integer implements Comparable, Comparable {
// compile-time error, cannot implement two interfaces with same erasure
private final int value;
public Integer(int value) { this.value = value; }
public int compareTo(Integer i) {
return (value < i.value) ? -1 : (value == i.value) ? 0 : 1;
}
public int compareTo(Long l) {
return (value < l.value) ? -1 : (value == l.value) ? 0 : 1;
}
}
根据前面擦除过程的介绍,这里擦除后函数签名是不同的,但是根据上篇文章中桥的介绍,编译器会添加一个额外的函数public int compareTo(Object o),然后再调用上面的函数,那么调用那个函数呢?这似使得问题变得麻烦。所以目前最好的方法是禁止这种情况。所以上面的代码会产生编译错误。
关于什么是具体化,举个例子比如一个Number类型的数组的具体化形式就是Number[];而ArrayList
看一下具体化类型有哪些:
1.基本类型 (比如 int)
2.非参数化的类和接口类型 (比如Number, String, Runnable)
3.类型参数是没有约束的通配符 (比如List>, ArrayList>, or Map, ?>)
4.原始类型 (比如List, ArrayList, or Map)
5.元素类型是具体化的数组 (比如 int[], Number[], List>[], List[], or int[][])
下面的是非具体化类型:
1.类型变量 (比如 T)
2.实际参数的参数类型(比如 List
3.有约束的参数类型 (比如 List extends Number> or Comparable super String>)
具体化与非具体化主要是在测试实例(instanceof),强制类型转换,抛出异常时会涉及到一些问题。
比如下面是测试实例和强制转换的例子:
import java.util.*;
public abstract class AbstractList
extends AbstractCollection
{
public boolean equals(Object o) {
if (o instanceof List
Iterator
Iterator
while (it1.hasNext() && it2.hasNext()) {
E e1 = it1.next();
E e2 = it2.next();
if (!(e1 == null ? e2 == null : e1.equals(e2)))
return false;
}
return !it1.hasNext() && !it2.hasNext();
} else return false;
}
...
}
其中o instanceof List
下面修正上面的代码:
import java.util.*;
public abstract class AbstractList
extends AbstractCollection
public boolean equals(Object o) {
if (o instanceof List>) {
Iterator
Iterator> it2 = ((List>)o).iterator();
while (it1.hasNext() && it2.hasNext()) {
E e1 = it1.next();
Object e2 = it2.next();
if (!(e1 == null ? e2 == null : e1.equals(e2)))
return false;
}
return !it1.hasNext() && !it2.hasNext();
} else return false;
}
...
}
用List>代替List
上面说使用实例测试如果是非具体化类型会报错,但有些情况下也是正确的,比如
public static
throws InvalidArgumentException
{
if (c instanceof List>) {
return (List
} else throw new InvalidArgumentException("Argument not a list");
}
之所以正确是因为根据Collection
关于强制类型转换再看一下下面的代码:
class Promote {
public static List
for (Object o : objs)
if (!(o instanceof String))
throw new ClassCastException();
return (List
}
public static void main(String[] args) {
List
List
List
assert (List>)strs1 == (List>)objs1;
boolean caught = false;
try {
List
} catch (ClassCastException e) { caught = true; }
assert caught;
}
}
上面的unchecked cast是无法避免的,因为编译器不知道objs里面是否都是String类型的对象。另外上面的代码也提供了传统代码与加入泛型之后新代码之间的兼容性,只要一个List里面保存的全部都是String类型的对象,我们就可以把它转换为List
接下来看一下异常处理时的情况:
Throwable的子类不能是参数化的 。例如:
class ParametricException
private final T value;
public ParametricException(T value) { this.value = value; }
public T getValue() { return value; }
}
上面的代码中定义异常时使用了参数化类型,编译错误。为什么呢,我们可能会这样使用上面的自定义异常对象:
class ParametricExceptionTest {
public static void main(String[] args) {
try {
throw new ParametricException
} catch (ParametricException
assert e.getValue()==42;
}
}
}
这肯定会出现编译错误的,因为ParametricException
不能创建泛型数组,数组创建时关于具体化的问题:
当数组的类型是类型变量或者参数化类型时需要特别注意。例如
import java.util.*;
class Annoying {
public static
T[] a = new T[c.size()]; // compile-time error
int i=0; for (T x : c) a[i++] = x;
return a;
}
}
上面代码中出现编译错误,因为T不是具体化类型。
import java.util.*;
class AlsoAnnoying {
public static List
List
List
return new List
}
}
上面代码出现编译错误,因为类型参数不是具体化的,Java不允许创建泛型数组。
看一下把泛型转换为数组的情况:
import java.util.*;
class Wrong {
public static
T[] a = (T[])new Object[c.size()]; // unchecked cast
int i=0; for (T x : c) a[i++] = x;
return a;
}
public static void main(String[] args) {
List
String[] a = toArray(strings); // class cast error
}
}
前面的时候采用new T[c.size()]编译错误,现在的代码中是 unchecked cast。但是下面的class
cast error是怎么产生的呢?事实上,T会被擦除为Object,然后在toArray方法前面加上适当的强制类型转换。等价于下面的代码:
import java.util.*;
class Wrong {
public static Object[] toArray(Collection c) {
Object[] a = (Object[])new Object[c.size()]; // unchecked cast
int i=0; for (Object x : c) a[i++] = x;
return a;
}
public static void main(String[] args) {
List strings = Arrays.asList(args);
String[] a = (String[])toArray(strings); // class cast error
}
}
所以,toArray返回的具体类型是Object[],强制转换为String[]自然会出错,尽管它里面存放的全部是String,但是编译器是不知道的。
为了避免这样的问题,有一条原则:
The Principle of Truth in Advertising:具体化类型必须是其擦除类型的子类型。
下面是可以实现上面功能的一种代码:
import java.util.*;
class Right {
public static
if (a.length < c.size())
a = (T[])java.lang.reflect.Array. // unchecked cast
newInstance(a.get Class().getComponentType(), c.size());
int i=0; for (T x : c) a[i++] = x;
if (i < a.length) a[i] = null;
return a;
}
public static void main(String[] args) {
List
String[] a = toArray(strings, new String[0]);
assert Arrays.toString(a).equals("[one, two]");
String[] b = new String[] { "x","x","x","x" };
toArray(strings, b);
assert Arrays.toString(b).equals("[one, two, null, x]");
}
}
一个更好的选择是使用类Class的实例,它持有一个类运行时的信息。代码如下:
import java.util.*;
class RightWithClass {
public static
T[] a = (T[])java.lang.reflect.Array. // unchecked cast
newInstance(k, c.size());
int i=0; for (T x : c) a[i++] = x;
return a;
}
public static void main(String[] args) {
List
String[] a = toArray(strings, String.class);
assert Arrays.toString(a).equals("[one, two]");
}
}
/*
Applying newInstance to a class token of type Class
参考自:《Java.Generics.and.Collections》