vue项目接口域名动态的获取方法
275
2022-08-05
浅谈Java中的桥接方法与泛型的逆变和协变(java 泛型 协变 逆变)
目录1. 泛型的协变1.1 泛型协变的使用1.2 泛型协变存在的问题1.2.1 java当中桥接方法的来由1.2.2 为什么泛型协变时,不允许添加元素呢1.2.3 从Java字节码的角度去看桥接方法2. 泛型逆变2.1 泛型逆变的使用2.2 泛型逆变会有什么问题3.协变与逆变-PECS原则
泛型的协变和逆变是什么?对应于Java当中,协变对应的就是 extends XXX>,而逆变对应的就是 super XXX>。
1. 泛型的协变
1.1 泛型协变的使用
当我们有一个有方法,方法的签名定义成为如下的方式
public static void test(List
这时,如果我们想要给test方法传入一个List
我们将方法的参数变成如下的这种形式
public static void test(List extends Number> list)
这时,我们的泛型,就只需要传入一个是Number的子类型的泛型即可。因为Integer和Double,它们都是Number的子类,因此很明显是合法的。
test(new ArrayList
test(new ArrayList
在test方法中:
1.如果我们想要去获取集合当中的某个元素时,因为约定了元素的所有类型都得是Number类型极其子类的,因此我们获取的元素一定可以用它们的共同父类Number去进行接收。2.但是当我们想要往集合当中添加元素时,竟然无法往list当中添加元素?奇奇怪怪的!而且关键我们的list,只要求元素的类型是Number或者它的子类类型。但是我们加入的是1,是个Intger类型,很明显是符合规范的呀!
public static void test(List extends Number> list) {
Number number = list.get(0); // right
list.add(1); // error
}
1.2 泛型协变存在的问题
泛型的协变,不能让我们往集合当中添加元素。那么为什么不能添加呢?
要知道为什么,我们首先需要了解Java当中桥接方法的来由。
1.2.1 Java当中桥接方法的来由
我们首先定义如下的自定义ArrayList类,并重写了它的add方法,
public class MyArrayList extends ArrayList
@Override
public boolean add(Double e) {
return super.add(e);
}
}
首先,我们肯定知道ArrayList类中的add方法的原型是下面这样的
public boolean add(E e)
在Java当中,是在编译时去进行类型擦除的,在运行时并无泛型类型一说。也就是说,该原型方法,会被抹掉成为
public boolean add(Object e)
但是,我们定义了自己的ArrayList,我们自己的add方法的原型为
public boolean add(Double e)
这个两个方法的签名并不相同,但是当使用下的代码创建一个ArrayList时:
ArrayList
list.add(1.0);
它实际调用的方法的原型是public boolean add(Object e),但是我们子类中的重写的方法的原型时什么?public booleab add(Double e)。
也就是说,通过父类的方法调用的和子类重写的方法,并不是同一个方法,因为它们连方法签名都不同。这时候,就需要要一个方式,将public booleab add(Object e)转到public booleab add(Double e)当中去执行。这时候,就会涉及到桥接方法的存在了。
Java的实现方式是:通过在Javac编译器编译时,为我们生成一个public boolean add(Object e)这样的方法,而这个方法当中,要做的实际上就是调用public booleab add(Double e)这个方法。
public boolean add(Object o) {
return add((Double) o);
}
通过桥接方法的方式,就可以让我们能在针对泛型方法进行重写时,可以被JVM执行到。
1.2.2 为什么泛型协变时,不允许添加元素呢
当我们使用下面的代码创建了一个我们自定义的MyArrayList对象。
ArrayList
这时,我们调用test方法
test(list)
test方法对于list的泛型定义为 entends Number>,理论上应该是可以往里面放入任何Number子类类型的元素的。但是别忘了,我们MyArrayList中对于方法的定义,是下面这样子的!
public boolean add(Object e) {
return add((Double)e);
}
public boolean add(Double e) {
// ......
}
如果我们往集合当中添加一个Integer类型的1,走到桥接方法当中时会有(Double)e这样的强制类型转换,这不就是抛出了ClassCastException异常了吗?很明显,是不允许我们这样干的。因此Java的做法就是,在编译期就去禁止这种做法,避免产生运行时的ClassCastException。
有的人也许会说
ArrayList
我们创建list时,不是约束了泛型类型为Double了吗,为什么test方法内就不能默认它是Double的泛型呢?问题就是:我写test方法时,我怎么知道你传递的是Double类型的泛型,玩意别人传递的是Integer的泛型呢?所以很明显是行不通的。
1.2.3 从Java字节码的角度去看桥接方法
我们可以看到,Javac编译器,在对Java代码进行编译时,其实针对add方法去生成了两个方法,而它们的访问标识符并不相同。我们自己的方法的访问标识符为0x0001[public],而Javac编译器为我们生成的桥接方法的返回值,为0x1041[pubic synthetic bridge],多了两个访问标识符synthetic和bridge。
我们打开桥接方法的code字节码
我们来分析下字节码
1.aload_0,众所周知,就是从LocalVariableTable(局部变量表)获取this对象的引用,并压栈。2.aload_1,自然就是将传入的元素e的引用压栈。3.checkcast #3
我们通过分析得到的信息和我们之前的分析一致,就是通过桥接方法桥接一下,去调用我们自己实现的方法。我们接下来,尝试使用反射的方式去获取到add方法有几个,方法信息是什么。
Arrays.stream(MyArrayList.class.getMethods()).filter(method -> method.getName().equals("add") && method.getParameterCount() == 1).forEach(method -> {
System.out.printf("方法名为:%s,方法的返回值类型为:%s,方法的参数列表为:%s%n",
method.getName(), method.getReturnType(), Arrays.toString(method.getParameterTypes()));
});
代码的最终执行结果为
方法名为:add,方法的返回值类型为:boolean,方法的参数列表为:[class java.lang.Double]方法名为:add,方法的返回值类型为:boolean,方法的参数列表为:[class java.lang.Object]
也就是说,生成的桥接方法,是我们可以通过反射拿到的,它是一个真实的方法。
通过反射拿到Method之后,我们还可以通过访问标识符判断该方法是否是桥接方法。
method.isBridge()
method.isSynthetic()
判断桥接方法,实际上,在Spring框架当中的反射工具类(ReflectionUtils)当中就有用到,用来判断一个方法是否是用户定义的方法。
2. 泛型逆变
2.1 泛型逆变的使用
泛型逆变的泛型形式是: super XXX>,它的作用是赋值给它的约束容器的泛型类型,只能是XXX以及它的父类。
那么我们可以往容器里放入它的子类吗?也许会说,上面不是都说了需要放入的是XXX以及它的父类吗,那肯定是不能放入它的子类的呀!但是我们需要想到一个问题,那就是XXX的所有子类,其实都是可以隐式转换为XXX类型,或者可以直接说,它的子类就是XXX类型。
我们依次定义三个类
static class Person {
}
static class User extends Person {
}
static class Student extends User {
}
接着,定义一个使用逆变的泛型参数的方法
public static void test(List super User> list)
上面我们说了,可以接收的容器泛型类型是User以及它的父类,也就是说,容器的泛型可以是User也基于是Person。因此,我们可以传入下面这样的容器给test方法。
test(new ArrayList
在test方法当中,我们可以执行下面的才做
list.add(new User()); // 放入User
list.add(new Student()); // 放入User的子类
2.2 泛型逆变会有什么问题
我们需要想想一个问题:我们使用了逆变约定了,接收的容器的泛型类型是User以及User的父类。我们往容器当中放入的元素,可以是User以及User的子类。也就是说,我们获取容器中的元素时,根本不知道是什么类型,只能用Object去接收从容器中获取的元素类型,因为只是约定了容器的泛型为User和User的父类,而Object也是它的父类,因此我们甚至可以传入一个容器类型为ArrayList
final Object object = list.get(0);
现在又有一个问题:之前协变时,会出现因为执行桥接方法时,发生类型转换异常,在逆变当中会出现这种情况吗?
我们仔细想想,接收的容器泛型类型为User以及User的父类,而可以往容器里存放的是User以及User的子类,也就是说,我们放入到容器中的元素类型,比你原来约束的类型还严格,因为:"User以及User的子类"一定是"User以及User的父类"的子类。也就是说,逆变当中,并不会因为桥接方法中进行的类型导致ClassCastException,所以允许add。
3.协变与逆变-PECS原则
对于协变和逆变,有这样的一个原则:称为PECS(Producer Extends Consumer Super)。也就是说:
1.Extends应该用在生产者的情况,也就是要根据泛型类型去返回对象的形式。2.Super应该用在消费者的情况,应该传入一个泛型类型的容器,应该利用该容器对数据进行处理,但是不能根据泛型去进行返回,如果要进行返回,只能返回Object,但是这就失去了泛型的意义。
public static
list.add(...);
}
public static
return list.get(0);
}
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~