泛型(generic)在Java1.5中被引入
术语 | 示例 | 描述 |
---|---|---|
参数化类型(parameterized type) | List | 首先是类或接口,然后用<> 把对应泛型形式类型参数的实际类型参数括起来, |
泛型 | List<E> |
具有一个或多个类型参数(type paramseter)的类或者接口 |
实际类型参数 | List<String>中的String |
String是与形式类型参数E对于的实际类型参数 |
形式类型参数 | E | |
无限制通配符类型 | List | |
原生形态(raw Type) | List | 类型或接口声明中删除所有的泛型信息 |
有限制类型参数 | <E extends Number> |
|
递归类型参数 | <T extends Comparable<T>> |
|
有限制通配符类型 | List<? extends Number> |
|
泛型方法 | static <E> List<E> asList(E[] a) |
|
类型令牌 | String.class |
泛型是是一种安全机制
所谓泛型,就是允许子定义类,接口,方法时使用类型参数,这个类型参数将在声明变量,创建对象,调用方法时动态的指定(即传入实际的类型参数)
java中允许使用原生形态是因为需要为引入泛型之前的旧代码提供兼容性
List与List<Object>
有什么区别,至少后者逃避了类型检查,后者告诉编译器,它可以存储所有类型的对象。
泛型有子类化(subtyping)的规则,List<String>
是原生态类型List的一个子类型,而不是List的子类型,即泛型不具有协变性,
如果使用泛型,但不确定或者不关心实际的类型参数,就可以使用一个问号代替,例如Set的无限制类型通配符的类型为Set<?>
(读作某个类型的集合),它可以持有任何类型。
List
和List<?>
有什么区别,通配符是类型安全的,原生态类型则不安全。
但是,不能将任何元素(null除外)放入Collections<?>中
。
1: 在类文字(class literal)中使用原生态类型。规范不允许使用参数化类型,例如List.class
,int.class
,String[].class
都合法,但是List<String.class>
和List<?>.class
不合法
2: 与instance操作符相关的操作
if(o instanceof Set){//raw type
Set<?> m = (Set<?>)o;//wildcard type
}
总是不要在新代码中使用原生态类型,保留原生态类型只是为了进行兼容和互用
非受检强制类型转化警告:unchecked cast warnings
@suppressWarnings("unchecked")
注解来禁止这警告SuppresWranings
,它尽可能是变量声明,或者非常简短的方法或者构造器,永远不要再整个类上使用SuppresWranings
,这可能会掩盖重要的警告SuppresWranings
时,都要添加一条注释数组与列表有来两个重要的不同点:
1: 数组是协变的(covariant),即比如Sub继承Super,则Sub[]也是Super[]的子类。而泛型是不可变的
List<Object> ol = new ArrayList<Long>不合法
2:数组是具体化的,因此数组会在运行时才知道并检查他们的元素类型约束,相比之下泛型则是通过擦除来实现的,因此泛型只在编译时期强化他们的类型信息,并在运行时丢弃他们的元素类型信息,擦除就是为了使泛型可以与没有使用泛型的代码随意进行互用
因此,数组和泛型不能很好的混合使用,例如创建泛型,参数化类型数组都是非法的。为什么?
因为它不是类型安全的,如果它合法,编译器在其正确的程序中发生的转换就会在运行时失败:
List<String>[] stringLists = new List<String>[1];
List<Integer> intLists = Arrays.toList(42);
Object[] objects = stringLists;
objects[0] = intList;
String s = stringLists[0].get(0);
假设第一行是合法的,则后面的代码也应该是合法的,然而在运行时,它肯定会抛出ClassCastException。
不可具体化类型:其运行时包含的信息比编译时表示发包含的信息更少的类型,如:List<E>,List<String>
但是List<?>[] a = new List[]{};
是合法的
一般来说,数组和泛型不能很好的混用,如果你发现自己将他们混合起来使用,并且得到编译时期的警告,你的第一反应就应该是利用列表代替数组
考虑下面类:
public static class Stack {
private Object[] objects;
private int size;
public static final int DEFAULT_SIZE = 16;
public Stack() {
objects = new Object[DEFAULT_SIZE];
}
public void push(Object o) {
ensureCapacity();
objects[size++] = o;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object o = objects[--size];
objects[size] = null;
return o;
}
private void ensureCapacity() {
if (size == objects.length) {
objects = Arrays.copyOf(objects, 2 * size + 1);
}
}
}
上面类是泛型化的主要备选对象,泛型化可以适当的强化这个类易用性,例如:
public static class Stack<E>{
private E[] objects;
private int size;
public static final int DEFAULT_SIZE = 16;
public Stack() {
objects = new E[DEFAULT_SIZE];
}
public void push(E o) {
ensureCapacity();
E[size++] = o;
}
public E pop() {
if (size == 0) {
throw new EmptyStackException();
}
E o = E[--size];
E[size] = null;
return o;
}
private void ensureCapacity() {
if (size == objects.length) {
objects = Arrays.copyOf(objects, 2 * size + 1);
}
}
}
但是这样无法通过编译,objects = new E[DEFAULT_SIZE];
只剩声明一个泛型数组,而无法创建一个不可具体化类型的数组
解放方法:
1:直接绕过创建泛型数组的禁令
objects = (E[])new Object[DEFAULT_SIZE];
2:将objects的域从E[]变为Object[],这么做需要修改单个元素的转换
E e =(E)objects[--size]
但是:禁止数组类型的未受检比禁止标量类型的更加危险,所以建议采用第二种,但是使用第二种会有很多地方需要从数组读取元素,然后进行强转,因此第一种更加常用。
class DelayQueue<E extends Delayed> implements BlockingQueue<E>
类型参数列表<E extends Delayed>
要求实际类型参数必须是Delayed
的子类,类型参数称作有限制类型参数。但是记住每个类型都是他本身的子类型
方法也一样可以使用泛型,静态工具尤其适合于泛型化
泛型方法有一个显著的特定,无需指明类型参数的值,编译器通过检查方法的类型来计算类型参数的值,这称之为类型推导,泛型方法化的方法适用性更广。
通过某个包含该类型参数本身的表达式来限制类型参数是允许的,这就是递归类型限制,如
public static <T extends COmparable<T>> T max(List<T> list>)
类型限制<T extends COmparable<T>>
可以读作“针对可以与自身比较的每个类型T”
泛型类型是不可变的。
对于泛型
public static class Stack<E>{
private E[] objects;
private int size;
public static final int DEFAULT_SIZE = 16;
......
}
假如有一个方法需要添加:
public void pushAll(Iterable<E> src){
for(E e:src){
push(e)
}
}
这个方法可以正常编程运行,但是假如有一个参数化类型Stack<Number>
调用pushAll添加类型为Iterable<Integer>
的Iterable集合,则编译无法通过,需要的是Iterable<Number>
而不是Iterable<Integer>
。
但是如果使用限制类型通配符:
public void pushAll(Iterable<? extends E> src){
for(E e:src){
push(e)
}
}
则可以正常编译运行了
如果试着添加一个popAll的方法,弹出栈中所有元素,加入到集合中:
public void popAll(Collection<E> dst){
while(!isEmpty){
dst.add(pop());
}
}
这样也有一些局限性,它要求集合的参数化类型必须与Stack的参数化类型保持一致,当类型为Stack<Number>
时,则无法使用Collection<Object>
类型做法popAll作为参数
解决方法是:popAll的输入类型不应该限制为E的集合,而应该是E的超类集合,所以使用Collection<? surper E>
,
public void popAll(Collection<? surper E> det){
while(!isEmpty){
det.add(pop());
}
}
这是popAll方法灵活性适用性更高
结论:为了获取最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型
有一个助记符便于记住要使用哪种通配符类型:
PECS表示producer-extends,consumer-super
即:如果参数化类型表示一个T生产者,就是用<? extends T>
;如果表示一个T消费者,就使用<? super T>
因此,对于消费者Comparable和Comparator
Comparable<? super T>优先于Comparable<T>
Comparator<? super T>优先于Comparator<T>
记住Comparable和Comparator都是消费者
参数类型和通配符类型具有双重性,许多方法都可以用其种一种声明,对于类型参数只在方法声明总出现一次就可以用通配符取代参数类型
当一个类的字面文字被用在方法中,来传达编译时和运行时的类型信息时,就被称作type token。String.class 属于 Class类型。
集合API说明了泛型的一般用法,限制你每个容器只能有固定数目的类型参数,你可以通过将类型参数放在键上而不是容器上来避开这一个限制,对于这种类型安全的异构容器,可以使用Class对象作为键,以这种方式使用Class对象称作类型令牌。如:
public class Favorites {
private Map<Class<?>, Object> mFavorites = new HashMap<Class<?>, Object>();
public <T> void putFavorites(Class<?> type, T instance) {
mFavorites.put(type, instance);
}
public <T> T getFavorites(Class<T> type) {
return type.cast(mFavorites.get(type));
}
Class.cast:cast方法是java cast操作的动态模拟
class.asSubclass:调用Class对象转换成用其产生表示的类的一个子类。
但是Favorites有两种局限性:
1:客户端可以很轻松的破坏Favorites实例的类型安全,只要以原生态形式使用Class对象,确保Favorites永远不违背它的类型约束条件方式是:
//这对于追溯谁把错误的类型添加到集合中很有帮助
public <T> void putFavorites(Class<?> type, T instance) {
mFavorites.put(type, type.cast(instance));
}
客户端如要使用必须遵守使用约定,
2:不能用在不可具体化的类型中,例如他无法保存List<String>
类型的对象,因为无法为List<String>
获取的一个class。