泛型,即“参数化类型”。顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
例如:
private static <T> T m(T t){
return t;
}
泛型参数分为两种
参数类型 | 无限定 | 上界限定extends | 下界限定super | 多重限定 |
---|---|---|---|---|
类型参数T | <T> | <T extends A> | 不支持 | <T extends A&B> |
通配符? | <?> | <? extends A> | <? super B> | 不支持 |
通配符无限定<?>和通配符上界限定<? extends A>不具有写的权限,只有读的权限;通配符下界限定<? super B>具有读和写的权限。
泛型根据使用可以分为三种:
- 泛型类
public class G<T,E> {
private T t;
private E e;
}
- 泛型方法
public <U> U m(U u){
return u;
}
- 泛型接口
public interface B<K> {
void m(K k);
}
泛型的类型参数只能是类类型,不能是简单类型
泛型类可以和泛型方法并存:
public class P<T> {
public void s(T t){
System.out.println(t.getClass().getName());
}
public <U> void r(U b){
System.out.println(b.getClass().getName());
}
public static void main(String[] args) {
new P<A>().s(new A());
new P<A>().r(new B());
}
}
class A{
}
class B{
}
注意:s方法和r方法是不同的,s是泛型类中的普通方法,而r是一个泛型方法,主函数中对两个方法的调用可以看出两个方法的区别。同时可以看出泛型方法的类型参数和泛型类的类型参数是没有相应的联系的,泛型方法始终以自己定义的参数类型为准。
类型系统
引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于 List<String>和List<Object>这样的情况,类型参数String是继承自Object的。而第二种指的是 List接口继承自Collection接口。对于这个类型系统,有如下的一些规则:
- 相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。即List<String>是Collection<String> 的子类型,List<String>可以替换Collection<String>。这种情况也适用于带有上下界的类型声明。
- 当泛型类的类型声明中使用了通配符的时候, 其子类型可以在两个维度上分别展开。如对Collection<? extends Number>来说,其子类型可以在Collection这个维度上展开,即List<? extends Number>和Set<? extends Number>等;也可以在Number这个层次上展开,即Collection<Double>和 Collection<Integer>等。如此循环下去,ArrayList<Long>和 HashSet<Double>等也都算是Collection<? extends Number>的子类型。
- 如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。
Java的泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换成它们非泛型上界。
类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
泛型擦除
什么是泛型擦除?
Java中的泛型擦除是指在编译后的字节码文件中类型信息被擦除,变为原生类型(raw type)。即泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,虚拟机会以签名的形式保留这些泛型实参类型。
Java语言的泛型采用的是擦除法实现的伪泛型,泛型信息(类型变量、参数化类型)编译之后通通被除掉了。Java选取这种方法是一种折中,因为Java最开始的版本是不支持泛型的,为了兼容以前的库而不得不使用擦除法。
擦除过程
类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般是Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。同时去掉出现的类型声明,即去掉<>的内容。比如T get()方法声明就变成了Object get();List<String>就变成了List。接下来就可能需要生成一些桥接方法(bridge method)。这是由于擦除了类型之后的类可能缺少某些必须的方法。
为了更加理解泛型,接下来看几个例子:
1. 通配符限定的泛型不能写
List<?> list = new ArrayList<>();
对于上面的这个集合list,不能调用list.add()方法
2. 类型检查只针对与引用
public void t() {
List l1 = new ArrayList();
List l2 = new ArrayList<String>();
List<String> l3 = new ArrayList();
l1.add(1);
l2.add(1);
l3.add("1");
}
3.泛型只存在于编译阶段
static void o() throws Exception {
List<Integer> list = new ArrayList<>();
list.add(3);
Method add = list.getClass().getMethod("add", Object.class);
add.invoke(list,"wang");
add.invoke(list,true);
for (Object o : list){
System.out.println(o);
}
}
正常情况下集合list由于编译期间进行类型检查只能添加Integer类型的对象,但是通过反射使得其在运行期间可以添加String类型和Boolean类型的对象。这是因为在运行阶段,泛型信息被擦除了,list的类型变成了List<Object>,而List<Object>是允许添加String和Boolean类型的对象的。
4.不能初始化具体类型的泛型数组
List<?>[] array1 = new List<?>[1];
List<Integer>[] array2 = new ArrayList[3];
这两种数组的初始化都可以,但是不能写成new ArrayList<String>[3]。理解这个之前我们先区分两个概念:定义数组和初始化数组。等于号左边的属于数组的定义;等号右边的属于数组的初始化。数组的定义是对数组的一个引用,还记得吗,泛型的引用会在编译期间进行类型检查,运行期间就会被擦除,所以在运行期间array1和array2的类型都会变成List<Object>,如果数组的初始化指定了具体类型,则引用类型和实际类型就会不一致。
网友评论