Java泛型总结

一、介绍

1.1泛型

看一下使用泛型和不使用泛型的区别:
使用泛型:
List words = new ArrayList();
words.add("Hello ");
words.add("world!");
String s = words.get(0)+words.get(1);
assert s.equals("Hello world!");
同样的代码不使用泛型:
List words = new ArrayList();
words.add("Hello ");
words.add("world!");
String s = ((String)words.get(0))+((String)words.get(1))
assert s.equals("Hello world!");
不使用泛型的版本省略了尖括号里面的参数String,但是每次从words中取出元素时需要显示的类型转换。
由于泛型是基于擦除实现的,上面两段代码的字节码事实上是一样的。简单的说一下擦除,比如List、List、List>代表相同的类型List,这里的擦除也不能简单的理解为清除,因为还会隐式的添加强制类型转换,就如同第二段代码里面显示强制类型转换一样 。而且如果编译时没有提示unchecked warning,泛型擦除保证自动添加的类型转换绝对不会失败。

使用泛型擦除有一些好处:它并有创造一种本质上新的概念,比如List擦除后还是List,这样使得同样的库可以同时适用于泛型和非泛型形式,减轻了进化压力。

另一个泛型擦除带来的结果是,数组类型与参数化类型的不同。比如执行:new String[size],分配一个数组,并且其中保存了表明它元素类型为String的指示信息。而new ArrayList(),分配一个list,但是其中并没有存储关于其元素类型的指示信息。后面会看到这个设计如何减轻进化压力但是又使得强制类型转换(casts)、实例测试(instance tests)和数组创建(array creation)复杂化的

简单提一点,Java泛型与C++模板在语法语义方面是不同的,具体不再介绍。

1.2、装箱和拆箱(Boxing and Unboxing)

Java中的类型要么是引用类型要么是原始类型(primitive type)。每个原始类型都有对应的引用类型,比如byte--Byte,int--Integer等,从原始类型到相应引用类型的转换成为装箱,反过来叫做拆箱。
比如下面代码就涉及到了:
List list = new ArrayList();
list.add(127); //装箱,相当于list.add(Integer.valueOf(127))
int n = list.get(0); //拆箱,相当于list.get(0).intValue()
Integer i = 127;
System.out.println(i==list.get(0));//true

1.3 Foreach

不必详细介绍了,放在这里主要是为了说明它也是JDK1.5的新特性。

1.4、泛型方法和可变参数

泛型方法和可变参数的例子,如下(其中作为声明一种新的参数类型):
class Lists {
public static List toList(T... arr) {
List list = new ArrayList();
for (T elt : arr) list.add(elt);
return list;
}
}
在调用泛型方法的时候,既可以依靠编译器对类型进行推断,也可以显示的给出类型。可以这样调用上面的方法:
List ints = Lists.toList();
List objs = Lists.toList(1, "two");
如果不明确指出类型,第一个因信息太少而无法准确推断,第二个因信息太多(两个参数都继承自Object,同时同实现了Serializable和Comparable接口),也无法准确推断。

1.5 Assertions

If assertions are enabled and the expression evaluates to false, an AssertionError is thrown, including an indication of where the error occurred. Assertions are enabled by invoking the JVM with the -ea or -enableassertions flag。

二、子类和通配符

2.1子类关系

子类父类本身是很容易理解的,比如

Integer is a subtype of Number
Double is a subtype of Number
ArrayList is a subtype of List
List is a subtype of Collection
Collection is a subtype of Iterable

但是一些泛型的情况里面,有些地方就很容易混淆。看下面几种情况:

List nums = new ArrayList();
nums.add(2);
nums.add(3.14);

上面代码中,把2(Integer,涉及到自动装箱boxing)和3.14(Double)加入到nums里面是允许的,因为二者都是Number的子类。把ArrayList实例赋值给List引用nums也是允许的,因为前者是后者的子类,这些都是很容易理解的。

继续看下面的情况:

List ints = new ArrayList();
ints.add(1);
ints.add(2);
List nums = ints; // compile-time error
nums.add(3.14);

上面的代码中出现了编译错误,
为什么?事实上List不是List的子类,不过List是Collection的子类。数组则与之有很大的不同,Integer[]是Number[]的子类。如果希望List也像数组一样,不仅能存放指定类型的元素,也能存放该类型的子类型元素,就要用到通配符。上面标红的两行代码,说明了为什么List不能是List的子类。

2.2、extends通配符

首先来看一下Collection接口中的addAll()方法:

interface Collection {
...
public boolean addAll(Collection c);
...
}

使用通配符?后,方法参数既可以是元素类型为E的容器,也可以是元素类型为E的子类的容器。看个例子:

List nums = new ArrayList();
List ints = Arrays.asList(1, 2);
List dbls = Arrays.asList(2.78, 3.14);
nums.addAll(ints);
nums.addAll(dbls);

上面的代码是正确的,因为Integer和Double都是Number的子类,而addAll()方法使用了通配符。这里其实弥补了我们前面说的List不是List子类的不足。接下来看一个更有意思的例子:

List ints = new ArrayList();
ints.add(1);
ints.add(2);
List nums = ints;
nums.add(3.14);  // compile-time error

前面的时候,是第四行报错,现在第四行正确了(因为ints现在是nums的子类),第五行报错,为什么?这就好比化学实验室有一些试剂瓶,如果一个瓶子我们只知道它是装液体的(),那么我们是否能能把硫酸倒进去(.add(硫酸))?不能!万一它是盛蒸馏水的呢,那不就造成试剂污染了,对吧?那么怎么才能添加呢?需要使用super通配符。

2.3 super通配符

先来看一下Collection中的方法:

public static void copy(List dst, List src) {
for (int i = 0; i < src.size(); i++) {
dst.set(i, src.get(i));
}
}

? super T意味着dst中可能包含任何元素,这些元素是T的父类,而源src中的都是T的子类型。也就是说目的容器中的元素类型肯定是源容器中元素的父类型,前面说过这是可以的。

下面是调用这个函数的一个例子:

List objs = Arrays.asList(2, 3.14, "four");
List ints = Arrays.asList(5, 6);
Collections.copy(objs, ints);

其中类型参数可以隐式的通过编译器推断,也可以显示的指出,下面几种调用方法都是可以的:

Collections.copy(objs, ints);
Collections.copy(objs, ints);
Collections.copy(objs, ints);
Collections.copy(objs, ints);

下面体会一下使用通配符的好处:

public static void copy(List dst, List src)
public static void copy(List dst, List src)
public static void copy(List dst, List src)
public static void copy(List dst, List src)

具体不分析了,记住:“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 = new ArrayList();
ints.add(1);
ints.add(2);
List nums = ints;
nums.add(null); // ok

List objs = Arrays.asList(1,"two");
List ints = objs;
String str = "";
for (Object obj : ints) str += obj.toString();

? extends T <==> [null,T]

? super T    <==> [T,Object]

最后,我们知道String是final类型的,没有子类。那么List与List是不是同一类型呢,答案是否。事实上前者是后者的子类,因为我们可以把前者的引用赋给后者。同时存取原则也告诉我们这两个不是同一类型,因为前者可以存入String对象,后者则不可以。

2.4、数组vs容器 通配符vs类型参数

在Java中,如果A是B的子类,那么A[]也是B[]的子类。看下面的代码:

Integer[] ints = new Integer[] {1,2,3};
Number[] nums = ints;
nums[2] = 3.14; // array store exception

这里的异常是在运行时抛出的,它能通过编译,因为它符合子类对象赋给父类引用的规则。但是前面也说过,数组类型会存储元素类型信息,每次向其中添加元素时,都会进行检查,如果不兼容就会抛出异常。

重写上面的代码:

List ints = Arrays.asList(1,2,3);
List nums = ints; // compile-time error
nums.set(2, 3.14);

第二行出错,因为List不是List的子类,而且这是编译错误。所以,使用容器比使用数组更能及时的发现错误。当然容器相比数组还有其他优势,比如容器支持的操作更多,更加灵活等等。数组在某些情况下相比容器也有优势,比如当元素类型为primitive type的时候,数组会更加高效,因为原始类型没有子类,不涉及类型检查等等

通配符与类型参数:

看一下Java Collection中的方法:

interface Collection {
...
public boolean contains(Object o);
public boolean containsAll(Collection c);
...
}

Collection是Collection的缩写。

这样使用:

Object obj = "one";
List objs = Arrays.asList("one", 2, 3.14, 4);
List ints = Arrays.asList(2, 4);
assert objs.contains(obj);
assert objs.containsAll(ints);
assert !ints.contains(obj);
assert !ints.containsAll(objs);

最后两句(int包含Object)看起来不合常理(这样设计主要是为了兼容泛型使用之前的代码)。而且某种情况下,它还有可能是true,比如

Object obj = 1;
List objs = Arrays.asList(1, 3);
List ints = Arrays.asList(1, 2, 3, 4);
assert ints.contains(obj);
assert ints.containsAll(objs);

还可以这样设计上面的方法:

interface MyCollection { // alternative design
...
public boolean contains(E o);
public boolean containsAll(Collection c);
...
}

这样使用:
Object obj = "one";
MyList objs = MyList.asList("one", 2, 3.14, 4);
MyList ints = MyList.asList(2, 4);
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)。如果是自己实现一个向前兼容不是很重要的类,后者更好一些。

通配符捕获

2.5、通配符限制(Restrictions on Wildcards)

实例创建:在实例创建中,如果类型是参数化类型,那么任何参数化类型都不能是通配符。例如:
List list = new ArrayList(); // compile-time error
Map map= new HashMap(); // compile-time error
根据存取原则,上面创建的实例不能存数,这是没有意义的。但是可以这样:
List nums = new ArrayList();
List sink = nums;
List source = nums;
for (int i=0; i<10; i++) sink.add(i);
double sum=0; for (Number num : source) sum+=num.doubleValue();
内嵌通配符也是允许的,比如
List> lists = new ArrayList>();
lists.add(Arrays.asList(1,2,3));
lists.add(Arrays.asList("four","five"));
assert lists.toString().equals("[[1, 2, 3], [four, five]]");
泛型方法调用
如果泛型方法有参数化类型,如果 显示的指明类型不能使用通配符,同样内嵌通配符是允许的
List list = Lists.factory(); // compile-time error
List> = Lists.>factory(); // ok
超类
class AnyList extends ArrayList {...} // compile-time error
class AnotherList implements List {...} // compile-time error
class NestedList extends ArrayList> {...} // ok

三、比较和约束(Comparison and Bounds)

3.1 Comparable

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就会产生溢出。

===============================================================================================

3.2、Maximum of a Collection

下面是找最大值的代码:

public static > T max(Collection coll) {
T candidate = coll.iterator().next();
for (T elt : coll) {
if (candidate.compareTo(elt) < 0) candidate = elt;
}
return candidate;
}

需要注意的是加粗的部分,有两点需要注意,一是对于类型参数T不管后面是接口还是类都要使用extends,二是只能使用extends不能使用super,这点与通配符有区别。另外,这里对T的约束是一种递归约束。甚至有些情况下,还可以是相互递归约束,比如, U extends D>

3.3、A Fruity Example

看一个例子,有一个水果类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 {...}

现在假设有这样的函数签名 > T max(Collection coll),如果传递List作为参数就会出错,因为Orange没有实现Comparable接口。改为这样> T max(Collection coll)就可以了。

3.4、Comparator

如果需要比较没有实现Comparable的对象,可以通过Comparator接口。
interface Comparator {
public int compare(T o1, T o2);
public boolean equals(Object obj); 
}
compare方法根据第一个对象小于、等于、大于第二个对象分别返回负数、0、正数。

3.5、多重约束(Multiple Bounds)

类型变量或者通配符也可以受多个类或接口约束。比如:
public static T extends Appendable & Closeable>
void copy(S src, T trg, int size)
throws IOException

3.6、桥

我们已经知道泛型是采用擦除策略实现的,也就是说我们写的泛型代码在编译后与没有采用泛型的代码是几乎一样的。当我们实现带类型参数的接口时,编译器会为我们添加额外的方法,这个方法叫做桥。

比如,下面两段代码

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)

3.7、协变重写

在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()

四、声明(Declarations)

这一节主要是如何声明泛型类,包括构造函数、静态成员、内部类、泛型擦除过程。

4.1构造函数

泛型类中,类型参数出现在类的头部而不是构造函数中

class Pair {

public Pair(T first, U second) { this.first=first; this.second=second; }

}

当构造函数被调用的时候,实际的参数会传递进去:Pair pair = new Pair("one",2);

常见的一个错误时,实例化时忘记传递具体类型,如 Pair pair = new Pair("one",2);这样会产生警告信息,但是并不是错误,Pair作为生类型(raw type)可以赋给相应的参数化类型。

4.2静态成员

静态成员是属于类的,独立于任何类型参数。当访问静态属性时,不允许在类后面附带类型参数。比如:
Cell.getCount(); // ok
Cell.getCount(); // compile-time error
Cell.getCount(); // compile-time error
其中getCount()是静态函数。
同样的,下面的也不行
class Cell2 {
private final T value;
private static List values = new ArrayList();// illegal
public Cell(T value) { this.value=value; values.add(value); }
public T getValue() { return value; }
public static List getValues() { return values; }// illegal
}
可做如下修改:
class Cell2 {
private final T value;
private static List values = new ArrayList(); // ok
public Cell(T value) { this.value=value; values.add(value); }
public T getValue() { return value; }
public static List getValues() { return values; } // ok
}

4.3 内部类(Nested Classes)

如果外部类有类型参数,而且内部类不是静态的,那么外部类的类型参数在内部类中是可见的。如果内部类是静态的,那么就是不可见的

4.4、泛型擦除工作方式

“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, List> 擦除为 List

2.List[] 擦除为 List[]

3.List擦除后还是List

4.Integer擦除后为其本身

5.在asList函数声明中类型T擦除为Object,因为它没有约束限制

6.在max函数声明中T被擦除为Comparable,因为T的限制是Comparable

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),然后再调用上面的函数,那么调用那个函数呢?这似使得问题变得麻烦。所以目前最好的方法是禁止这种情况。所以上面的代码会产生编译错误。

五、具体化

5.1 具体化

关于什么是具体化,举个例子比如一个Number类型的数组的具体化形式就是Number[];而ArrayList的具体化类型是ArrayList,而不是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, ArrayList, or Map)
3.有约束的参数类型 (比如 List or Comparable)


具体化与非具体化主要是在测试实例(instanceof),强制类型转换,抛出异常时会涉及到一些问题。

5.2实例测试与强制类型转换

比如下面是测试实例和强制转换的例子:

import java.util.*;
public abstract class AbstractList
extends AbstractCollection implements List
{
public boolean equals(Object o) {
if (o instanceof List) { // compile-time error
Iterator it1 = iterator();
Iterator it2 = ((List)o).iterator(); // unchecked cast
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出现了编译错误,因为List不是具体化的,由于泛型擦除的原因,运行时我们只能测试o是不是List的实例,而无法知道它的类型是否是E。强制类型转化出现 unchecked cast警告,原因也是一样的。

下面修正上面的代码:

import java.util.*;
public abstract class AbstractList
extends AbstractCollection implements List {
public boolean equals(Object o) {
if (o instanceof List) {
Iterator it1 = 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,前面提到List是具体化的。还有一种方法就是直接改为原始类型List和Iterator,因为它也是具体化的。但是建议使用前一种方式,因为它能在编译阶段发现更多的错误。

上面说使用实例测试如果是非具体化类型会报错,但有些情况下也是正确的,比如

public static List asList(Collection c)
throws InvalidArgumentException
{
if (c instanceof List) {
return (List)c;
} else throw new InvalidArgumentException("Argument not a list");
}

之所以正确是因为根据Collection c知道c如果转换为List也一定是List类型的。

关于强制类型转换再看一下下面的代码:

class Promote {
public static List promote(List objs) {
for (Object o : objs)
if (!(o instanceof String))
throw new ClassCastException();
return (List)(List)objs; // unchecked cast
}
public static void main(String[] args) {
List objs1 = Arrays.asList("one","two");
List objs2 = Arrays.asList(1,"two");
List strs1 = promote(objs1);
assert (List)strs1 == (List)objs1;

boolean caught = false;
try {
List strs2 = promote(objs2);
} catch (ClassCastException e) { caught = true; }
assert caught;
}
}

上面的unchecked cast是无法避免的,因为编译器不知道objs里面是否都是String类型的对象。另外上面的代码也提供了传统代码与加入泛型之后新代码之间的兼容性,只要一个List里面保存的全部都是String类型的对象,我们就可以把它转换为List

5.3异常抛出

接下来看一下异常处理时的情况:

Throwable的子类不能是参数化的 。例如:

class ParametricException extends Exception { // compile-time error
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(42);
} catch (ParametricException e) { // compile-time error
assert e.getValue()==42;
}
}
}

这肯定会出现编译错误的,因为ParametricException不是具体化的。

5.4数组创建

不能创建泛型数组,数组创建时关于具体化的问题:

当数组的类型是类型变量或者参数化类型时需要特别注意。例如

import java.util.*;
class Annoying {
public static T[] toArray(Collection c) {
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[] twoLists() {
List a = Arrays.asList(1,2,3);
List b = Arrays.asList(4,5,6);
return new List[] {a, b}; // compile-time error
}
}

上面代码出现编译错误,因为类型参数不是具体化的,Java不允许创建泛型数组。

看一下把泛型转换为数组的情况:

import java.util.*;
class Wrong {
public static T[] toArray(Collection c) {
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 strings = Arrays.asList("one","two");
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 T[] toArray(Collection c, T[] a) {
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 strings = Arrays.asList("one", "two");
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[] toArray(Collection c, Class k) {
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 strings = Arrays.asList("one", "two");
String[] a = toArray(strings, String.class);
assert Arrays.toString(a).equals("[one, two]");
}
}

/*

Applying newInstance to a class token of type Class returns a new array of type T[], with the component type specified by the class token*/




参考自:《Java.Generics.and.Collections》



你可能感兴趣的:(Java,Java泛型与集合,泛型)