[TOC]
什么是泛型,为什么使用
泛型是将类型作为参数,像方法一样在使用时能够传入对象的类型,这样即约束了对象的类型,也无需使用Object作为通用类型带来的强制转换和很差的可读性。
泛型的使用一方面能让人一眼看出来对象所属的类型,可读性大大提高,另外对应编译器,参数化类型的静态约束,使得对集合这些容器的元素类型检查在编译期就能判断,增加了代码的安全性。
如在泛型出现之前的ArrayList类,底层的存储元素的数组是一个Object数组,这意味着所有的java对象都可以放入此容器中,在取出时需要类型强制转换,如果在add的时候添加了不符合预期的类型,可能就会抛ClassCastException。且在运行时才会抛出。
如何使用泛型
定义简单泛型类
设计一个泛型栈,代码不完整,且非并发安全类。
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
public void ensureCapacity(){}
}
定义泛型方法
泛型方法可以定义在泛型类中,也可以定义在普通类中
上面泛型类已经有泛型方法
// 普通类定义泛型方法
class Example{
// <T>必须
public static <T> T get() {
return null;
}
}
class TestG<T> {
// error ,静态方法不能使用类的泛型参数
public static T get() {
return null;
}
}
类型变量的限定
限定类型的边界,使用extends和super关键字声明接受的类型的范围。
public <T extends Number> T find(T a){
}
上面使用extends
表明类型T是Number
类的子类。如果有多个限定,使用T extends A & B
,AB为具体类或者接口;
泛型运行时擦除
使用泛型的类在编译后,泛型是被擦除的,有限定的泛型使用限定类替换,没有限定的使用Object类替换。所以泛型的主要作用就是静态类型的检查。
需要注意的是,在编译器擦除类型后,可能会使得重载方法产生冲突,因为T被擦除成Object,会生成Object返回值或者参数的方法,这可能与原本类中的实现方法重载了,如:
PS: 重载是方法名相同,入参类型不同,和返回值没有关系
class People<T> {
private T t;
public T get() {
return null;
}
public void setT(T t) {
this.t = t;
}
}
class Man extends People<Integer> {
@Override
public Integer get() {
return null;
}
@Override
public void setT(Integer t) {
}
}
擦除之后(反编译):
class People
{
People()
{
}
public Object get()
{
return null;
}
public void setT(Object obj)
{
t = obj;
}
private Object t;
}
class Man extends People
{
Man()
{
}
public Integer get()
{
return null;
}
public void setT(Integer integer)
{
}
public volatile void setT(Object obj)
{
setT((Integer)obj);
}
public volatile Object get()
{
return get();
}
}
方法签名由方法名和参数组成,返回值类型不参与,所以get方法重载冲突了,set方法在重载时有类型匹配,虽然不冲突。虚拟机使用了桥方法来解决重载冲突问题。
类型擦除有以下几个事实:
- 虚拟机中没有泛型,只有普通类和方法;
- 所有类型参数都用它们的限定类型替换,没有就是Object;
- 桥方法被用来解决重载冲突;
- 为保证类型安全,必要时会插入强制类型转换。
约束与局限
大部分都是因为类型擦除:
- 不能使用基本类型作为类型参数
- 运行时类型检查只适用于原始类型
instanceof判断对象类型,泛型类型在运行时是擦除的,所以不能使用这种方式来检查类型
- 不能创建参数化类型的数组
如:
People<Integer>[] p = new People<Integer>();
如果需要这种限定元素类型的集合,可以使用ArrayList<People<Integer>>
- 方法的泛型可变参数警告
- 不能实例化类型变量
T t = new T();
- 不能构造泛型数组
T[] ta = new T[1];
- 泛型类的静态上下文中类型变量无效(不能在静态域和方法中使用类型参数)
- 不能抛出或捕获泛型类的实例
- 可以消除对受检查异常的检查
泛型类型的继承规则
无论S和T是什么关系,Pair<S>
和Pair<T>
之间没有关系。
通配符类型
?
代表泛型的通配符,Pair<? extends Employee>,表示任何泛型Pair类型,它的参数类型是Employee的子类。指明了类型的上边界。
Pair<? super Manager>表面任何泛型Pair类型,它的参数类型是Manager的超类,这指明了类型的下边界;
<T>与<?>的区别
- <T>:泛型标识符,用于泛型定义(类、接口、方法等)时,可以想象成形参。 在使用时T是一个类型参数,需要传入实际的类型变量才能使用。T相当于一个方法的参数声明,也就是形参,需要传入实际值的。
- <?>:通配符,用于泛型实例化时,可以想象成实参,这个不是一个类型变量,就是一个实在的类型,使用时不需要传入类型。可以看成就是一个固定的类型,只是这个类型不确定,并不是需要在使用的时候再传入类型参数。
<T>与<?>的区别很重要,是理解通配符限制的基础。
向上、向下转换类型
向上转型:子类型通过类型转换成为父类型(隐式的)。
向下转型:父类型通过类型转换成为子类型(显式的,有风险需谨慎)。
上边界限定通配符
利用 <? extends Employee> 形式的通配符,可以实现泛型的向上转型:
例如:
- 定义了一个泛型类,里面含有两个泛型方法:
class Gen<T> {
public void add(T t) {
}
public T get() {
return null;
}
}
- 传类型参数的时候,传入一个通配类型的的类型参数
@Test
public void test3() {
Gen<? extends Super> gen = new Gen<>();
}
则此时对应的gen对象内的实例方法就是:
public void add(? extends Super t) {
}
public ? extends Super get() {
return null;
}
看一下add方法,add(? extends Super t)
,意思是这个方法接受一个通配类型的参数,这个参数的类型是什么不知道,只知道是Super类的子类。 编译器不知道具体的类型,所以它拒绝传递任何类型的参数,所以add()都会编译时报错,只能add(null);
再看get方法,? extends Super get()
,意思是这个方法返回的对象类型不确定,只知道是Super的子类,这样就可以使用Super obj = get();
来接受返回值,因为不管是哪个子类,都可以安全地转换成父类。
所以上边界通配符适合用来接收,而不能传入数据。
下边界限定通配符
通配符的另一个方向是 “超类型的通配符“: ? super T,T 是类型参数的下界。使用这种形式的通配符,我们就可以 ”传递对象” 了。
Gen<? super Sub> gen2 = new Gen<>();
gen2.add(new Sub());
Object o = gen2.get();//只能Object接收,因为Object是所有对象的父类
下边界类型通配符可以确定子类型,回顾向上转型与向下的概念:
在获取数据时 [ ? super Sub get(){ return null;} ]:只能知道返回值的类型的子类是Sub,不知道对象的具体类型,因为向下转型有风险,所以不能使用Sub来接收结果,需要接收只能使用安全的父类:Object。
在写入数据时 [ void add(? super Sub t){ } ],只知道参数类型的子类是Sub,所以写入Sub类或者Sub类的子类(它们能安全转为Sub类)是安全的。
所以下边界通配符适合用来写入数据,限制读取。
无边界通配符
无边界类型通配符(<?>) 等同于 上边界通配符<? extends Object>
因为可以确定父类型是Object,所以可以以Object去获取数据(向上转型)。但是不能写入数据。
总结
通配符 | 说明 |
---|---|
上边界类型通配符(<? extends 父类型>) | 因为可以确定父类型,所以可以以父类型去获取数据(向上转型)。但是不能写入数据。 |
下边界类型通配符(<? super 子类型>) | 因为可以确定最小类型,所以可以以最小类型去写入数据(向上转型)。只能以Object类去获取数据,但意义不大。 |
无边界类型通配符(<?>) | 等同于 上边界通配符<? extends Object>,所以可以以Object类去获取数据,但意义不大。 |
关于通配符,类型安全转换是编译器判断的依据。另外通配符表示是符合要求的任意类型,而非要传入类型,记忆时可以记成 “不确定的某个类”。
参考资料
[3] Java核心技术卷 I
网友评论