设计模式: Java泛型
不要使用原始类型
原始类型是Java(5之前)的历史遗留问题,之前集合对于类型在编译期间不进行检查,而在运行期进行类型检查。
这导致问题的定位困难,降低效率。
此后采用泛型解决这个问题,保证在编译期间进行检查。
例如List 类型是原始类型,而List< E >是泛型类型,指定了参数化类型为 E 类型。
List表明可以存储任何类型的对象,List< E > 是它的子类型,可以转化为List类型,但是丧失了安全性检查;而List< E > 却不是List< Object > 的子类型。List< Object > 只能是List< Object > 。
如果想实现类似于List等存储任何类型的对象, 可以利用无限制通配符类型(unbounded wildcard types)表示泛型类型,即List< ? > 。
泛型的几个注意点:
1. 类字面常量不允许使用泛型
数组String[].class、基本类型int.class、不带参数化类型的类List.class可以使用。
2. instanceof 只能对无限制通配符类型的参数化类型的类或接口使用。
1 | if(o instanceof Set){ |
术语 | 中文含义 | 举例 |
---|---|---|
Parameterized type | 参数化类型 | List< String > |
Actual type parameter | 实际类型参数 | String |
Generic type | 泛型类型 | List< E > |
Formal type parameter | 形式类型参数 | E |
Unbounded wildcard type | 无限制通配符类型 | List<?> |
Raw type | 原始类型 | List |
Bounded type parameter | 限制类型参数 | < E extends Number> |
Recursive type bound | 递归类型限制 | < T extends Comparable< T >> |
Bounded wildcard type | 限制通配符类型 | List<? extends Number> |
Generic method | 泛型方法 | static < E > List< E > asList(E[] a) |
Type token | 类型令牌 | String.class |
列表优先于数组
即List优先于数组。
因为数组是协变类型的,所以下面代码编译期间是合法的, 但是运行期间是违法的。
1 | Object[] o = new String[1]; |
尽量使用列表来在编译过程中就确保安全性,虽然会损失掉一定的性能与简洁性。
下面代码运行有警告,因为java程序中的泛型信息在编译后会进行擦除(这意味着它们只在编译时执行类型约束,并在运行时丢弃它们的元素类型信息),为了与Java5之前的代码共存。
因此实际在运行中虚拟机并不知道返回数组具体类型,返回的类型是顶层类 Object, 在强制转换为T[]时会有问题。
1 | public Chooser(Collection<T> choices) { |
因此最好采用List替代。
1 | public Chooser(Collection<T> choices) { |
优先考虑泛型
一个简单实现的栈,原始版本 vs 添加泛型版本。
原始版本1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31class Stack{
// 存储数量
private int size = 0;
// 默认长度
private static final int DEFAULT_INITIAL_CAPACITY;
// 元素桶
private Object[] elements;
public Stack{
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e){
ensureCapacity();
elements[size++] = e;
}
public Object pop(){
if(size==0)
throw new EmptyStackException();
Object res = elements[--size];
// 清除无用的引用
elements[size] = null;
return res;
}
private ensureCapacity(){
if(elements.length==size)
elements = Arrays.copyOf(elements, size*2+1);
}
}
这样的版本Stack每次取出元素都要进行强制类型转换。
泛型版本
1 | class Stack<E>{ |
考虑泛型方法
声明类型参数的类型参数列表位于方法的修饰符和返回类型之间。
1 | // Generic method |
限定通配符增加API灵活性
PECS代表: producer-extends,consumer-super。
如果一个参数化类型代表一个 T 生产者,使用 <? extends T>;如果它代表 T 消费者,则使用 <? super T>。 在我们的 Stack 示例中,pushAll 方法的 src 参数生成栈使用的 E 实例,因此 src 的合适类型为 Iterable<? extends E>;popAll 方法的 dst 参数消费 Stack 中的 E 实例,因此 dst 的合适类型是 Collection <? super E>。
所有 Comparable 和 Comparator 都是消费者。
可变参数与泛型
可变参数: 类型 T 加…构成,例如:
1 | public void test(String ... strs){ |
Java用一个数组来保存变长的参数,但是可变参数的类型与泛型要注意混淆。
考虑一个例子:
1 | public <T> T[] toArray(T ... t){ |
上面的会报错,因为泛型传递的是非具体类型,也就是编译时的类型信息要多与运行时信息(由于擦除),因此可变参数的数组用 Object类型存储,当Object转为String时,会报错ClassCastException。
另一个例子:
1 | public void test(List<String>... stringList){ |
可见,可变参数中混淆了泛型,还是很容易产生类型转换的不安全性,但是Java并没有因此抛弃,而在Arrays.asList(T… a),Collections.addAll(Collection<? super T> c, T… elements),EnumSet.of(E first, E… rest)中大量使用,说明只要保证可变参数中的泛型是安全的(以下三点),就可:
1. 可变参数数组不会存储跟修改。
2. 可变参数数组的引用不会转义。
3. 可变参数数组仅用来为方法传递可变参数。
当然,可变参数数组可以变为列表,但同时降低了一定性能与可读性。
1 | public <T> List<T> toArray(List<T> t){ |
可变参数的另一个用法
因为可变参数数组的产生都会产生性能损耗,因此当 95% 的调用是三个或更少的参数的方法,那么声明该方法的五个重载。
1 | public void foo() { } |
异构容器
泛型中规定的可变类型数量总是有限的,例如单个类型约束< T >以及Map的< K , V>。
异构容器可以通过参数化键的方式,实现泛型设定数量的灵活性。例如在数据库查询中任意列值。
下面是一个异构容器的示例:
1 | public class Favorites{ |
可以使用 ColumnClass</font> 对象作为此类型安全异构容器的键。 以这种方式使用的 Class 对象称为类型令牌。 也可以使用自定义键类型。 例如,可以有一个表示数据库行(容器)的 DatabaseRow 类型和一个泛型类型 Column< T > 作为其键。