2023-05-25    2023-11-01    3402 字  7 分钟

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 ) ,它长这样:

1
2
3
public final class Class {
	private Class() {}
}

以 String 类为例,当 JVM 加载 String 类时,它首先读取 String.class 文件到内存,然后,为 String 类创建一个 Class 实例并关联起来:

1
Class cls = new Class(String);

这个 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 实例呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 方法一
// 直接通过一个 class 的静态变量 class 获取
Class cls = String.class;

// 方法二
// 通过该实例变量(如果有一个实例变量)提供的 getClass() 方法获取
String s = "Hello";
Class cls = s.getClass();

// 方法三
// 通过静态方法(如果知道一个 class 的完整类名) Class.forName() 获取
Class cls = Class.forName("java.lang.String");

JVM 在执行 Java 程序的时候,并不是一次性把所有用到的 class 全部加载到内存,而是第一次需要用到 class 时才加载。

泛型

泛型是一种“代码模板”,可以用一套代码套用各种类型。

在讲解什么是泛型之前,我们先观察 Java 标准提供的 ArrayList ,可以看作“可变长度”的数组,实际上它内部就是一个 Object[] 数组,配合存储一个当前分配的长度。

1
2
3
4
5
6
7
public class ArrayList {
	private Object[] array;
	private int size;
	public void add(Object e) { ... }
	public void remove(int index) { ... }
	public Object get(int index) { ... }
}

如上所示,ArrayList 的 get 方法返回的是一个 Object 类型的数据。如此,当你用它存储 String 类型的时候,获取的结果其实需要强制转型(由 Object 转型为 String )。如下:

1
2
3
4
5
6
7
8
ArrayList list = new ArrayList();
list.add("Hello");
// 获取到 Object ,必须强制转型为 String
String first = (String) list.get(0);

list.add(new Interger(123));
// ERROR: ClassCastException
String second = (String) list.get(1);

并且很容易出现误转型 ClassCastException

要解决上述问题,我们可以为 String 单独编写一种 ArrayList :

1
2
3
4
5
6
7
public class StringArrayList {
	private String[] array;
	private int size;
	public void add(String e) { ... }     // 存 String
	public void remove(int index) { ... }
	public String get(int index) { ... }  // 取 String
}

存取 String 的问题暂时解决了,然而存取 IntegerPerson 等其他类型呢?当然,我们可以用上述方式为其创建对应的类,然而实际上这实际上是不科学的,太多了。

为了解决这个新的问题,我们必须把 ArrayList 变成一种模板: ArrayList<T> ,代码如下:

1
2
3
4
5
6
7
public class ArrayList<T> {
	private T[] array;
	private int size;
	public void add(T e) { ... }
	public void remove(int index) { ... }
	public T get(int index) { ... }
}

如此,我们在存取 String 类型的时候,就可以用 ArrayList<String> 了,如下:

1
2
3
4
5
6
ArrayList<String> strList = new ArrayList<String>();

strList.add("hello");           // ✔
String s = strList.get(0);      // ✔
strList.add(new Integer(123));  // ✘ compile error
Integer n = strList.get(0);     // ✘ compile error

:: 看,泛型,其实是一种更高层次上的抽象。

通常来说,泛型类一般用在集合类中,例如 ArrayList<T>,我们很少需要编写泛型类。那么,Java 语言是如何实现泛型的呢?

擦拭法

Java 语言的泛型实现方式是擦拭法(Type Erasure),虚拟机对泛型其实一无所知,所有的工作都是编译器做的。

例如,我们编写了一个泛型类 Pair<T> ,下面是编译器看到的(我们编写的)代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setLast(T last) {
        this.last = last;
    }
}

虚拟机可看不到上述代码!它也不认识!编译器把上面的代码,编译成了下面这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class Pair {
    private Object first;
    private Object last;
    public Pair(Object first, Object last) {
        this.first = first;
        this.last = last;
    }
    public Object getFirst() {
        return first;
    }
    public Object getLast() {
        return last;
    }
    public void setFirst(Object first) {
        this.first = first;
    }
    public void setLast(Object last) {
        this.last = last;
    }
}

编译器直接把 <T> 擦拭掉了,把所有类型 <T> 视为 Object !它只是在使用编写的泛型类时,根据 <T> 将取得的值进行安全的强制转型。

比如,我们这样使用编写的泛型类(编译器看到的代码):

1
2
3
Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();

编译器会将上面这段代码中所取的值进行强制转型(虚拟机看到的代码):

1
2
3
Pair p = new Pair("Hello", "world");
String first = (String) p.getFirst();
String last = (String) p.getLast();

是的,Java 就这样实现了泛型,并不难!对于我们来说,不嫌麻烦的话,你完全可以不使用泛型,自己来做编译器做的这些活。

📣 题外话:Java 引入了泛型,所以,只用 Class 来标识类型已经不够了。实际上,Java 的类型系统结构如下:

![[assets/Pasted image 20231101142648.png]]

通配符

我们在阅读源码的时候,经常会看到类似 <? extends Number> 或是 <? super Integer> 等的代码,它们是什么?有什么作用?

我们称之为通配符。先来看一下,为什么需要它们。

我们仍然使用上个章节中编写的泛型类 Pair<T> ,现在我们要针对 Pair<Number> 类型写了一个静态方法,它接收的参数类型是 Pair<Number>

1
2
3
4
5
6
7
public class PairHelper {
    static int add(Pair<Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();
        return first.intValue() + last.intValue();
    }
}

在使用的时候,我们传入如下代码,运行是没有任何问题的:

1
int sum = PairHelper.add(new Pair<Number>(1, 2));

注意,传入的类型是 Pair<Number>,实际参数类型是 (Integer, Integer)。那如果我们像下面这样,直接传入 Pair<Integer> 可不可以呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class PairHelper {
    public static void main(String[] args) {
         // 生成一个 Pair<Integer> 实例,然后传入 add 方法使用它
        Pair<Integer> p = new Pair<>(123, 456);
        int n = add(p); // ❌ 编译会报错
        System.out.println(n);
    }

    static int add(Pair<Number> p) { // ❌
        Number first = p.getFirst();
        Number last = p.getLast();
        return first.intValue() + last.intValue();
    }
}

结果编译的时候,就抛出了一个错误:

incompatible types: Pair<Integer> cannot be converted to Pair<Number>

想想也是,Pair<Integer> 不是 Pair<Number> 的子类,因此,add(Pair<Number>) 不接受参数类型 Pair<Integer>

但是从 add() 方法的代码可知,传入 Pair<Integer> 是完全符合内部代码的类型规范,因为语句:

1
2
Number first = p.getFirst();
Number last = p.getLast();

实际类型是 Integer,引用类型是 Number,没有问题。

问题在于方法参数类型定死了只能传入 Pair<Number>!那么,有没有办法使得方法参数接受 Pair<Integer>?有的 —— 通配符!

下面我们来修改一下上面的 add() 方法,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class PairHelper {
    Pair<Integer> p = new Pair<>(123, 456);
    int n = add(p); // ✔️ 通过了
    System.out.println(n);
        
    static int add(Pair<? extends Number> p) { // ✔️ <? extends Number>
        Number first = p.getFirst();
        Number last = p.getLast();
        return first.intValue() + last.intValue();
    }
}

如此,给方法传入 Pair<Integer> 类型时,它符合参数 Pair<? extends Number> 类型。

这种使用类似 <? extends Number> 的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型 T 的上界限定在 Number 了。

如果我们考察对 Pair<? extends Number> 类型调用 getFirst() 方法,实际的方法签名变成了:

<? extends Number> getFirst();

即返回值是 NumberNumber 的子类,因此,可以安全赋值给 Number 类型的变量。那能不能赋值给 Integer 类型的变量呢?不行!因为 <? extens Number> 不是只允许你传入 Integer 类型的 实例,还允许你什么 Double 等其他 Number 的子类(包括 Number 本身)的实例,这就导致上述 getFirst() 方实际返回类型可能是 Integer,也可能是 Double 或者其他类型。

‘读’是没有问题了,‘写’呢?比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class PairHelper {
    Pair<Integer> p = new Pair<>(123, 456);
    int n = add(p);
    System.out.println(n);
        
    static int add(Pair<? extends Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();
        p.setFirst(new Integer(first.intValue() + 100)); // ❌
        p.setLast(new Integer(last.intValue() + 100)); 
        return p.getFirst().intValue() + p.getFirst().intValue();
    }
}

编译错误发生在 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 类型父类实例一起使用的时候很容易就乱套了),所以干脆不让你读,直接报错!

:: 好吧,其实还是有点乱,具体使用多了就明了了。