序
最近开发中遇到一个奇怪的现象,在对某一类bean
下面的方法进行扫描,获取当前方法上面的注解对应的值的时候,意外发现某一个继承自父类的方法竟然被扫描了两次,由于我在代码中设置了每一个方法允许且只允许被扫描一次,所以程序运行过程中也自然报错了。通过调试以及综合之前所了解的知识,发现是泛型导致的问题,在此记录,以便学习。
泛型与java
泛型
泛型对于大家来说,应该不是一个没有接触过的概念,我们在开发过程中,或多或少都会接触到,常见的List
、Map
等在使用过程中都会用到泛型,但是,我们可能没有去认真考虑,为什么需要用到泛型,以及泛型带来的好处是什么。
泛型主要是将类型参数话,使得可以将类型像参数传递到方法内部那样传递到数据结构内部,这样编译器在编译期间就可以针对类型进行筛选和检查,以提高类型安全,减少在运行时由于类型不匹配而引发的异常。
为什么要引入泛型
在面向对象的语言中,程序中会有很多类型的对象,对象多了,就需要一些用来存储某些对象的容器,常见的比如Array
、List
、Map
等,由于这些容器不仅仅用来存储对象,还提供了一些方法用来操作容器内部的对象,这时,问题就来了…容器需要知道容器中存放的是什么类型才能进行set
、return
。
进行java
开发的哥们可能会想到,我们使用Object
类型啊,这个类是所有的类的根类。那么一个容器类的内部就变成了下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/**
* @author yinan
* @date 2020/1/22
*/
public class Container {
private Object obj;
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
}
|
如果真的是这样的话,那么问题就来了,这货是一个能够装任何类型的对象,然后取出的时候,如果是自己需要的,就要去手动转成实际的类型,这样会不会显得很麻烦呢?除此之外,也会引起类型不安全,由于编译器无法检查的缘故,如果实际返回的对象无法强转成我们需要的类型,就会抛出异常。所以只能由开发人员来把关,记住对象属于什么类型避免类型转换错误。
这时候,泛型就有必要出场了…
java的泛型
首先,我们将上面的容器类改写一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/**
* @author yinan
* @date 2020/1/22
*/
public class Container<T> {
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
|
上面便是java
的泛型写法,当我们想要向容器中添加指定类型的对象时,便可以使用Container<T> container = new Container<>()
进行指定,然后再向其中添加对应的类型对象即可,这样如果添加的对象不是指定类型的,编译器就会直接报错,相对于非范型来说,可谓是从农业时代到工业时代的一大跨越。
真假泛型
我们都知道,java
泛型实际上和C#
、C++
等还是不太一样的,在编译之后的java
的class
文件中,对应的泛型类都会被擦出,也就是说,我们无法从类文件中去了解当前容器中所真正包含的对象类型,只有在运行时才会去实时解析出当前存入或返回的对象类型。那么java
中的泛型是怎样进行擦出的呢?我们还是来举一个例子。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public static void main(String[] args) {
try {
ArrayList<Integer> arr=new ArrayList<Integer>();
//存储int型
arr.add(1);
//利用反射存储String
arr.getClass().getMethod("add", Object.class).invoke(arr, "asd");
for (int i=0;i<arr.size();i++) {
System.out.println(String.format("泛型擦除 %s", arr.get(i)));
}
} catch (Exception e) {
e.printStackTrace();
}
}
|
运行结果
我们可以看到,在定义的Integer
类型的容器中,竟然存入了字符串类型的对象,仿佛泛型消失了一样,没错,的确是消失了,程序在编译之后,将Integer
类型擦除了,而反射避开了程序的编译过程,因此编译之后的程序可以存储任何Object
类型的子类。这也就是类型擦除。
那么为什么java
不像其它语言那样,考虑使用真的泛型,即编译期也不擦除类型呢?这其实是一个历史原因,也是java
不得已而为之,泛型是在java5
才被引入的,因此为了兼容之前的List list = new ArrayList()
这种写法的代码,java
不得已而为之。
泛型与注解
上面说明那么多,其实还仅仅是介绍一下泛型的历史,下面我们来进入正题,考虑一下如下的代码能否运行,以及运行结果。
1
2
3
4
5
6
7
8
9
10
11
|
package para.test;
/**
* @author yinan
* @date 2020/1/20
*/
public class ParaFather<T> {
public void say(T mes) {
System.out.println("para father says: " + mes);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
|
package para.test;
/**
* @author yinan
* @date 2020/1/20
*/
public class ParaSon extends ParaFather<String> {
@Override
public void say(String mes) {
System.out.println("para son says " + mes);
}
}
|
1
2
3
4
|
public static void main(String[] args) {
ParaFather<String> obj = new ParaSon();
obj.say("111");
}
|
我想大部分人都会能够正确的说出答案,这里也不卖关子,运行结果是para son says 111
;但是我们需要去看看原因,为什么是这样的结果,因为上面我们也说到了,泛型最终会被擦除,所以父类中的方法会变成如下所示:
而在其子类ParaSon
中的实现方法其参数类型却是一个String
类型,这样好像和面向对象中的继承性有点不太一样,我们明明是重载了say()
方法,而非重写该方法。这其中一定有一些猫腻。
桥接方法
反编译ParaSon
类文件之后,我们可以看到,类文件中多了一个方法:public void say(java.lang.Object)
;,反编译结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
yinans-MacBook-Pro:test yinan$ javap -c ParaSon.class
Compiled from "ParaSon.java"
public class para.test.ParaSon extends para.test.ParaFather<java.lang.String> {
public para.test.ParaSon();
Code:
0: aload_0
1: invokespecial #1 // Method para/test/ParaFather."<init>":()V
4: return
public void say(java.lang.String);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: ldc #5 // String para son says
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_1
16: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: return
public void say(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #9 // class java/lang/String
5: invokevirtual #10 // Method say:(Ljava/lang/String;)V
8: return
}
|
这说明,在编译阶段,子类中的确有生成一个父类的方法,而这个方法就是一个桥接方法(Bridge,可以使用isBridge()方法来判断),其实桥接方法的内部是调用了另外一个参数类型为String
的say()
方法,因此在子类中其实是有两个方法名为say
的方法。这也解释了为什么我刚开始说到的,有扫描到两次注解。
但是其实这里我有不明白的地方,明明桥接方法会调用真正的执行方法,那么为什么还需要在桥接方法上生成相关注解呢?
这里仅仅作为一个问题来记录,目前没有看到相关文档有说明,想着有时间再看一看桥接方法的实现原理,或许可以得到回答……
总结
本文主要从最近遇到的问题入手,解释泛型以及桥接方法相关知识,最后得出桥接方法上的注解导致的扫描出现问题,另外针对桥接方法上添加注解还存在着一些不理解的地方,期望在此记录后续翻看时可以有机会再弄懂。