i.e. Reflection and Generic
反射与泛型那些事儿……
反射
反射,i.e.Reflection,Java 的反射是指程序在运行期可以拿到一个对象(实例)的所有信息。
正常情况下,如果我们要调用一个对象的方法,或者访问一个对象的字段,通常会传入对象实例。反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。
除了 int
等基本类型外,Java 的其他类型全部都是 class
(包括 interface
), class
的本质是数据类型。
class
在由 JVM 在执行过程中动态加载的。
JVM 在第一次读取到一种 class
类型时,将其加载进内存。每加载一种 class
,JVM 就为其创建一个 Class
类型的实例,并关联起来。
注意:这里的 Class
类型是一个类名为 Class
的 class
(即 Class.class
) ,它长这样:
|
|
以 String
类为例,当 JVM 加载 String
类时,它首先读取 String.class
文件到内存,然后,为 String
类创建一个 Class
实例并关联起来:
|
|
这个 Class
实例是由 JVM 内部创建的,其构造方法是 private
,只有 JVM 能创建 Class
实例,我们自己的 Java 程序是无法创建 Class
实例的。
所以,JVM 持有的每个 Class
实例都指向一个数据类型( class
或 interface
),一个 Class
实例包含了该 class
的所有完整信息:
![[assets/Pasted image 20230525165823.png|400]]
由于 JVM 为每个加载的 class
创建了对应的 Class
实例,并在实例中保存了该 class
的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等。因此,如果获取了某个 Class
实例,我们就可以通过这个 Class
实例获取到该实例对应的 class
的所有信息。
这种通过 Class
实例获取 class
信息的方法称为反射(Reflection)。
如何获取一个 class
的 Class
实例呢?
|
|
JVM 在执行 Java 程序的时候,并不是一次性把所有用到的 class
全部加载到内存,而是第一次需要用到 class
时才加载。
泛型
泛型是一种“代码模板”,可以用一套代码套用各种类型。
在讲解什么是泛型之前,我们先观察 Java 标准提供的 ArrayList
,可以看作“可变长度”的数组,实际上它内部就是一个 Object[]
数组,配合存储一个当前分配的长度。
|
|
如上所示,ArrayList 的 get
方法返回的是一个 Object
类型的数据。如此,当你用它存储 String
类型的时候,获取的结果其实需要强制转型(由 Object
转型为 String
)。如下:
|
|
并且很容易出现误转型 ClassCastException
。
要解决上述问题,我们可以为 String
单独编写一种 ArrayList :
|
|
存取 String
的问题暂时解决了,然而存取 Integer
、 Person
等其他类型呢?当然,我们可以用上述方式为其创建对应的类,然而实际上这实际上是不科学的,太多了。
为了解决这个新的问题,我们必须把 ArrayList
变成一种模板: ArrayList<T>
,代码如下:
|
|
如此,我们在存取 String
类型的时候,就可以用 ArrayList<String>
了,如下:
|
|
:: 看,泛型,其实是一种更高层次上的抽象。
通常来说,泛型类一般用在集合类中,例如 ArrayList<T>
,我们很少需要编写泛型类。那么,Java 语言是如何实现泛型的呢?
擦拭法
Java 语言的泛型实现方式是擦拭法(Type Erasure),虚拟机对泛型其实一无所知,所有的工作都是编译器做的。
例如,我们编写了一个泛型类 Pair<T>
,下面是编译器看到的(我们编写的)代码:
|
|
虚拟机可看不到上述代码!它也不认识!编译器把上面的代码,编译成了下面这样:
|
|
编译器直接把 <T>
擦拭掉了,把所有类型 <T>
视为 Object
!它只是在使用编写的泛型类时,根据 <T>
将取得的值进行安全的强制转型。
比如,我们这样使用编写的泛型类(编译器看到的代码):
|
|
编译器会将上面这段代码中所取的值进行强制转型(虚拟机看到的代码):
|
|
是的,Java 就这样实现了泛型,并不难!对于我们来说,不嫌麻烦的话,你完全可以不使用泛型,自己来做编译器做的这些活。
📣 题外话:Java 引入了泛型,所以,只用 Class
来标识类型已经不够了。实际上,Java 的类型系统结构如下:
![[assets/Pasted image 20231101142648.png]]
通配符
我们在阅读源码的时候,经常会看到类似 <? extends Number>
或是 <? super Integer>
等的代码,它们是什么?有什么作用?
我们称之为通配符。先来看一下,为什么需要它们。
我们仍然使用上个章节中编写的泛型类 Pair<T>
,现在我们要针对 Pair<Number>
类型写了一个静态方法,它接收的参数类型是 Pair<Number>
:
|
|
在使用的时候,我们传入如下代码,运行是没有任何问题的:
|
|
注意,传入的类型是 Pair<Number>
,实际参数类型是 (Integer, Integer)
。那如果我们像下面这样,直接传入 Pair<Integer>
可不可以呢?
|
|
结果编译的时候,就抛出了一个错误:
incompatible types: Pair<Integer> cannot be converted to Pair<Number>
想想也是,Pair<Integer>
不是 Pair<Number>
的子类,因此,add(Pair<Number>)
不接受参数类型 Pair<Integer>
。
但是从 add()
方法的代码可知,传入 Pair<Integer>
是完全符合内部代码的类型规范,因为语句:
|
|
实际类型是 Integer
,引用类型是 Number
,没有问题。
问题在于方法参数类型定死了只能传入 Pair<Number>
!那么,有没有办法使得方法参数接受 Pair<Integer>
?有的 —— 通配符!
下面我们来修改一下上面的 add()
方法,如下:
|
|
如此,给方法传入 Pair<Integer>
类型时,它符合参数 Pair<? extends Number>
类型。
这种使用类似 <? extends Number>
的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型 T
的上界限定在 Number
了。
如果我们考察对 Pair<? extends Number>
类型调用 getFirst()
方法,实际的方法签名变成了:
<? extends Number> getFirst();
即返回值是 Number
或 Number
的子类,因此,可以安全赋值给 Number
类型的变量。那能不能赋值给 Integer
类型的变量呢?不行!因为 <? extens Number>
不是只允许你传入 Integer
类型的 实例,还允许你什么 Double
等其他 Number
的子类(包括 Number
本身)的实例,这就导致上述 getFirst()
方实际返回类型可能是 Integer
,也可能是 Double
或者其他类型。
‘读’是没有问题了,‘写’呢?比如:
|
|
编译错误发生在 p.setFirst()
传入的参数是 Integer
类型。既然 p
的定义是 Pair<? extends Number>
,那么 setFirst(? extends Number)
为什么不能传入 Integer
?
这就是 <? extends Number>
通配符的一个重要限制:方法参数签名 setFirst(? extends Number)
无法传递任何 Number
的子类型给 setFirst(? extends Number)
。
⭐ 一句话,对于 <? extends T>
就是 ‘读可以,写不行’ ,‘读’出来的 能确写都是 T
类型,‘写’的时候编译器不确定你会写个啥类型(不同 T 类型子类实例一起使用的时候很容易就乱套了),所以干脆不让你写,直接报错!
在定义泛型类型
Pair<T>
的时候,也可以使用extends
通配符来限定T
的类型。
那怎么‘写’?
使用 <? super T>
- 下界通配符 (Lower Bounds Wildcards)!
上面说了 <? extends T>
允许调用读方法 T get()
获取 T
的引用,但不允许调用写方法 set(T)
传入 T
的引用(传入 null
除外);
<? super T>
允许调用写方法 set(T)
传入 T
的引用,但不允许调用读方法 T get()
获取 T
的引用(获取 Object
除外)。
为什么呢?一句话,对于 <? super T>
,‘写’的时候,编译器能确定实际类型都兼容 T
(本身或其父类),不会存在什么引用错误,但是‘读’的时候编译器不能确定你会读个啥类型(不同 T 类型父类实例一起使用的时候很容易就乱套了),所以干脆不让你读,直接报错!
:: 好吧,其实还是有点乱,具体使用多了就明了了。