泛型也叫“参数化类型”,是在java1.5之后才有的用法
之所以出现泛型的最主要原因是能提高java程序的类型安全,使得当程序的类型转换错误能在提早得到暴露(将类型错误从运行时暴露提前到编译时)。
假设不使用泛型,那么创建例如ArrayList这种容器类时如果想让该容器支持存储各种类型的数据则需要将ArrayList所能存储的数据定义为Object。
假设存入时同一个容器存入了不同的数据类型,那么使用时很可能因为失误造成低级错误:
public void test(){
ArrayList<Object> list = new ArrayList<>();
list.add(new Person());
list.add("北京");
...
// 后续使用过程中一不小心将类型搞错了写成这样
Person person = (Person) list.get(1);
// 则此时编译时是看不出异常的,但是运行时会抛出ClassCastException 异常
}
而如果不使用泛型又想使ArrayList支持存储多种数据类型显然目前是做不到的,所以只能定义无数种类型的容器比如:StringArrayList、IntegerArrayList…显然不管有多少种类型但是内部的处理逻辑时一模一样的,这样又无端占用了内存。
所以引入泛型的好处在于:
- 提高程序健壮性,避免了强转的操作,在编译器完成类型转化,也就避免了运行的错误。
- 避免了相同逻辑代码因为未来需要支持各种未知数据类型而产生大量冗余代码,使得程序变得臃肿。
泛型基本用法:
泛型主要有3种用法:
- 泛型方法
- 泛型类
- 泛型接口
// 泛型类
public class Adapter<T, R> {
T t;
}
// 继承自泛型类,自身亦可再定义为泛型类,当不指定T,R实际类型时,两者都会被当成Object类型
public class MyAdapter<K> extends Adapter<Integer, String> {
}
// 泛型接口
public interface Service<K> {
}
// 实现了泛型接口自身亦可再声明为泛型类,当不指定K实际类型时,会被当成Object类型
public class MyService<L> implements Service<String> {
L l = null;
}
//泛型方法
public <K> void test(K k) {
}
泛型上下界:
<? extends AbstractList> 关键字声明了类型的上界,表示参数化的类型必须是AbstractList或者AbstractList的子类
<? super AbstractList> 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类
示例:
先定义几个类:
public class Animal {}
public class Cat extends Animal {}
public class MiniCat extends Cat {}
测试:
public void test(){
List<Animal> animals = new ArrayList<>();
List<MiniCat> miniCats = new ArrayList<>();
testTop(animals);//编译报错,因为需要传递的是泛型实际类型为Cat或者Cat子类的列表
testTop(miniCats);
showBottom(animals);
showBottom(miniCats);//编译报错,因为需要传递的是泛型实际类型为Cat或者Cat的父类的列表
}
public void testTop(List<? extends Cat> cats){
for (Object cat : cats) {
System.out.println(cat);
}
}
public void testBottom(List<? super Cat> cats){
for (Object cat : cats) {
System.out.println(cat);
}
}
以上就是泛型的上下界的常规用法,但是如果在java中使用泛型集合,那会有一些约束,具体的约束条件为:
- 定义的上界集合不能调用add(),get()返回类型为上界
- 定义的下界集合没有限制类型,get()返回的类型为Object
以List为例(Set、Map等集合同理):
上边界:
List<? extends Cat> cats = new ArrayList<>();
cats.add(new MiniCat());// 编译报错
cats.add(new Cat()); // 编译报错
cats.add(new Animal()); // 编译报错
Cat cat = cats.get(0);
MiniCat miniCat = (MiniCat) cats.get(0);
以上代码可知,List不支持添加并且取值时系统会以上边界的类型来返回,为什么呢?
假设能add的话,那么add时可以添加的类型是Cat或者Cat的子类,那此时如果我们再定义一个类:
public class MiniCat2 extends Cat{}
cats.add(new MiniCat());
cats.add(new MiniCat2());
MiniCat tom = (MiniCat) cats.get(0);
MiniCat2 jerry = (MiniCat) cats.get(0);//此处报错
这时如代码表现,List以上边界类型返回,那么开发者可以将返回的值强转为任何Cat的子类,那么又回回到没有泛型而使用Object那种情况,不安全。
很多人很蒙蔽既然不能add,那声明成List<? extends Cat>类型的数组岂不是永远都为空,那这种用法有什么用?
上边界数组一般不是用来存值的,而是用来便利的。常规用法是这样:
public void show(List<? extends Cat> cats){
for(Cat cat:cats){
System.out.println(cat);
}
}
ArrayList<MiniCat> miniCats = new ArrayList<>();
miniCats.add(new MiniCats());
show(miniCats);
这样我们就能保证传进来的参数必定是一个Cat或者Cat子类类型的集合,并且每一次的方法时获取到的集合里的数据肯定是同一种类型,避免了强转出现异常。Collection中就有这种实现:
boolean addAll(@RecentlyNonNull Collection<? extends E> var1);
下边界:
List<? super Cat> cats = new ArrayList<>();
cats.add(new MiniCat());
cats.add(new Cat());
cats.add(new Animal()); // 编译报错
for(Object cat : cats){
}
一开始很疑惑既然是上边界数组为什么不能添加父类而只能添加子类呢?
其实是自己把自己搞懵了,这里和泛型机制没什么关系。看下add()的源码就知道了:
boolean add(E var1);
显然在add的时候类型已经确定了,那既然声明的是某个类型,那按照java语法肯定是只能传入该类型或者该类型的子类,不可能声明个String你传递个Object进去。
最终list获取的会以Object形式返回。
上下边界数组还有一种用法是:
List<? extends Cat> cats = new ArrayList<MiniCat>();
List<? super Cat> cats1 = new ArrayList<Animal>();
泛型擦除:
java会在编译时将泛型用于类型检查,在运行时进行泛型擦除。
我们如何验证?
借用以上例子:
System.out.println(cats.class == cats1.class);// 结果为true
这样我们可以看出其实到运行时泛型已经被擦除,声明的cats和cats1都是ArrayList类型。
泛型擦除带来一个问题:
public void show(List<Cat> cats){}
public void show(List<Object> objs){}
这两个方法不能共存,因为经过泛型擦除之后这俩都是:
public void show(List cats){}
java的语法是不允许2个相同方法名的函数具有相同参数类型的重载的。
因为泛型在运行时会被擦除所以例如定义泛型T,代码中根本不会有T这种类型,所以我们不能用new T(), a instanceof T 这种操作,并且不能将静态变量声明成泛型,原因如下:
- 静态成员,会第一时间进入内存的,假如你说的泛型成立,你说此刻它泛型属于什么类型?明白了不?
- 泛型终究还是会被擦除的,只是编译时候帮助不小,这也就等于在说,泛型是在实例化对象的时候,才确定要给出什么类型的参数,懂了没?
- 如果按照惯例静态早就进内存了,泛型早已经确定了不是吗?
具有泛型参数时使用反射的注意事项:
public Class<?>[] getParameterTypes() //只会返回参数类型
public Type[] getGenericParameterTypes() //会返回参数完整类型,包括泛型。
如果没有参数化类型(泛型),则两个函数的返回结果作用相同,如果有参数化类型,则需要调用getGenericParameterTypes()才能获得完整的包含参数化类型的结果
java为什么要在运行时擦除泛型参考这篇文章:
https://blog.csdn.net/u014674862/article/details/105676880