无论你是否听过java泛型的协变与逆变,我们直接进入例子,一起来看一下java泛型比较高级的用法。
例子1:copy函数
第1个例子我们实现copy函数,它将List中的元素复制到List中。
Java
List src = Arrays.asList(1,2,3,4,5);
List dst = new ArrayList();
1
2
Listsrc=Arrays.asList(1,2,3,4,5);
Listdst=newArrayList();
不用泛型
copy1实现的参数类型是死的:
Java
private static void copy1(List src, List dst) {
for (Integer obj : src) {
dst.add(obj);
}
}
1
2
3
4
5
privatestaticvoidcopy1(Listsrc,Listdst){
for(Integerobj:src){
dst.add(obj);
}
}
然后这样调用:
Java
copy1(src, dst); // 死板,不通用
1
copy1(src,dst);// 死板,不通用
因为Integer是Number的子类,所以自然可以存储到List中。
问题:copy1函数只能搞定List拷贝给List,无法处理其他类型。
基本泛型
copy2由简单实现改进,引入泛型参数即可:
Java
private static void copy2(List src, List dst) {
for (T1 obj : src) {
dst.add((T2)obj);
}
}
1
2
3
4
5
privatestaticvoidcopy2(Listsrc,Listdst){
for(T1obj:src){
dst.add((T2)obj);
}
}
然后这样调用:
Java
copy2(src, dst); // 能用,但没有编译期约束,运行不安全(比如dst是List类型)
1
copy2(src,dst);// 能用,但没有编译期约束,运行不安全(比如dst是List类型)
我们知道T1(Integer)继承自T2(Number)的子类,所以加入dst时只需要强制向上转型即可通过编译。
问题:T1继承自T2是我们假设的,如果dst是一个List,那么上述代码将试图将Integer obj转型为String,虽然代码通过了编译但是在运行时将失败。
如果可以约束T1与T2之间的父子关系,那么这个函数将更加健壮。
泛型加强约束
copy3在泛型基础上引入约束:
Java
private static void copy3(List extends T> src, List super T> dst) {
for (T obj : src) {
dst.add(obj);
}
}
1
2
3
4
5
privatestaticvoidcopy3(List<?extendsT>src,List<?superT>dst){
for(Tobj:src){
dst.add(obj);
}
}
调用:
Java
copy3(src, dst); // 最灵活,编译期约束,运行一定安全
1
copy3(src,dst);// 最灵活,编译期约束,运行一定安全
我们期望的约束是:src中的元素类型A必须继承自dst中的元素类型B(A与B一样也可以),那么就可以安全的将src中的元素加入到dst中(A向上转型为B)。
既然A类型必须继承自B,那么A与B继承层次中间一定存在某个类型T,所以我们可以做出约束:
src里必须是T的子类:List extends T>,上述例子中 extends T>就是Integer。
dst里必须是T的父类:List super T>,上述例子中 super T>就是Number。
extends T>作为T的子类,当然可以向上转型为 super T>作为T的父类,所以这个约束就是我们想要的效果。
我们可以猜想,编译器也一定会推断T为Number类型,此时可以令上述约束成立,实际上我们并不需要关心T到底是啥,反正我们对T类型也做不了什么事。
此时,我们的copy3函数功能已经很强大,可以支持将List拷贝给List这样的调用,同时也会在编译器就禁止掉将List拷贝给List这样的错误写法。
例子2:map函数
mapper是一个泛型接口:
Java
private static interface Mapper {
R map(T t);
}
1
2
3
privatestaticinterfaceMapper{
Rmap(Tt);
}
接受T类型参数,返回R类型参数。
我们接下来实现map函数,接受1个List与1个Mapper,返回1个新List。
我们固定输入是List类型,返回为List类型。
Java
List list = Arrays.asList(1,2,3,4,5);
1
Listlist=Arrays.asList(1,2,3,4,5);
不用泛型
实现map1函数:
Java
private static List map1(List list, Mapper mapper) {
List l = new ArrayList();
for (Integer t : list) {
Number r = mapper.map(t);
l.add(r);
}
return l;
}
1
2
3
4
5
6
7
8
privatestaticListmap1(Listlist,Mappermapper){
Listl=newArrayList();
for(Integert:list){
Numberr=mapper.map(t);
l.add(r);
}
returnl;
}
调用:
Java
// 写死类型
List ret1 = map1(list, new Mapper() {
@Override
public Number map(Integer integer) {
return integer * 1.2;
}
});
1
2
3
4
5
6
7
// 写死类型
Listret1=map1(list,newMapper(){
@Override
publicNumbermap(Integerinteger){
returninteger*1.2;
}
});
map1函数写死了List元素类型、Mapper输入类型、Mapper返回值类型。
问题:不能支持其他类型,需要引入泛型。
基本泛型
map2将上述Integer换成T泛型参数,Number换成R泛型参数:
Java
private static List map2(List list, Mapper mapper) {
List l = new ArrayList();
for (T t : list) {
R r = mapper.map(t);
l.add(r);
}
return l;
}
1
2
3
4
5
6
7
8
privatestaticListmap2(Listlist,Mappermapper){
Listl=newArrayList();
for(Tt:list){
Rr=mapper.map(t);
l.add(r);
}
returnl;
}
调用:
Java
// 能用,但mapper入参类型必须与list泛型完全一样,返回值类型必须与ret2泛型完全一样
List ret2 = map2(list, new Mapper() {
@Override
public Number map(Integer integer) {
return integer * 1.2;
}
});
1
2
3
4
5
6
7
// 能用,但mapper入参类型必须与list泛型完全一样,返回值类型必须与ret2泛型完全一样
Listret2=map2(list,newMapper(){
@Override
publicNumbermap(Integerinteger){
returninteger*1.2;
}
});
原理很简单,输入T类型的列表,经过mapper处理T类型后返回R类型的列表。
问题:List ret2要求R类型匹配为Number,list要求T类型匹配为Integer,因此就导致Mapper也必须是Mapper,使用起来看不出啥问题,但灵活性是有一定局限的,下面解释。
泛型放宽约束
当map函数遍历List中的每个Integer时,如果Mapper接受Integer当然最准确,但如果Mapper能够接受Number类型作为输入,那么其实把Integer传给Mapper也是可以运行的,因为Integer可以向上转型到Number。
同样道理,让Mapper返回Number类型当然最准确,但如果Mapper返回是Number子类(比如Double),其实Double是可以向上转型插入到List中的,这样也完全正确。
所以,我们应该让Mapper的泛型参数具备灵活性,而不是完全等于T和R。
理解了就非常容易写出map3函数:
Java
private static List map3(List list, Mapper super T,? extends R> mapper) {
List l = new ArrayList();
for (T t : list) {
R r = mapper.map(t);
l.add(r);
}
return l;
}
1
2
3
4
5
6
7
8
privatestaticListmap3(Listlist,Mapper<?superT ,?extendsR>mapper){
Listl=newArrayList();
for(Tt:list){
Rr=mapper.map(t);
l.add(r);
}
returnl;
}
调用:
Java
// 最灵活,mapper入参类型只要是list泛型的父类即可,返回值类型只要是ret3泛型的子类即可
List ret3 = map3(list, new Mapper() {
@Override
public Double map(Number number) {
return number.doubleValue() * 1.2;
}
});
1
2
3
4
5
6
7
// 最灵活,mapper入参类型只要是list泛型的父类即可,返回值类型只要是ret3泛型的子类即可
Listret3=map3(list,newMapper(){
@Override
publicDoublemap(Numbernumber){
returnnumber.doubleValue()*1.2;
}
});
读懂上述泛型必须学会阅读顺序!!
根据实参和返回值,T和R类型先被明确:
List ret3明确了返回值的R就是Number。
List list明确了T就是Integer。
在T和R已明确情况下,我们通过? super T的方式令Mapper的入参类型放宽,只要是T(Integer)的任意父类即可接受T的传入。通过? extends R的方式令Mapper的返回类型放宽,只要是R(Number)的任意子类即可插入到List l中。
总结
你应该听说过java的协变、逆变、PECS原则等概念,我认为理解上述例子应该是不需要死记硬背的:
1,理解这3个符号之间的继承父子关系: extends T> 继承自 T 继承自 super T>
2,结合向上类型转换,一切皆可分析。
如果文章帮助了你,请帮我点击1次谷歌广告,或者微信赞助1元钱,感谢!
知识星球有更多干货内容,对我认可欢迎加入: