CONTENTS
- 1. 为什么需要反射
- 2. Class对象
- 2.1 类字面量
- 2.2 泛型类的引用
- 2.3 cast()方法
反射使我们摆脱了只能在编译时执行面向类型操作的限制,并且让我们能够编写一些非常强大的程序。本文将讨论 Java 是如何在运行时发现对象和类的信息的,这通常有两种形式:简单反射,它假定你在编译时就已经知道了所有可用的类型;以及更复杂的反射,它允许我们在运行时发现和使用类的信息。
1. 为什么需要反射
面向对象编程的一个基本目标就是,让编写的代码只操纵基类的引用。我们来看下面这个例子:
package reflection;import java.util.stream.Stream;abstract class Shape {void draw() {System.out.println(this + ".draw()");}@Overridepublic abstract String toString();
}class Circle extends Shape {@Overridepublic String toString() { return "Circle"; }
}class Square extends Shape {@Overridepublic String toString() { return "Square"; }
}class Triangle extends Shape {@Overridepublic String toString() { return "Triangle"; }
}public class Shapes {public static void main(String[] args) {Stream.of(new Circle(), new Square(), new Triangle()).forEach(Shape::draw);/** Circle.draw()* Square.draw()* Triangle.draw()*/}
}
Shape
接口中的方法 draw()
是可以动态绑定的,因此客户程序员可以通过泛化的 Shape
引用来调用具体的 draw()
方法。在所有子类中,draw()
都被重写,并且因为它是一个动态绑定的方法,即使通过泛化的 Shape
引用来调用它,也会产生正确的行为,这就是多态。
基类里包含一个 draw()
方法,它通过将 this
传递给 System.out.println()
,间接地使用了 toString()
方法来显示类的标识符(toString()
方法被声明为 abstract
的,这样就可以强制子类重写该方法,并防止没什么内容的 Shape
类被实例化)。
在此示例中,将一个 Shape
的子类对象放入 Stream<Shape>
时,会发生隐式的向上转型,在向上转型为 Shape
时,这个对象的确切类型信息就丢失了,对于流来说,它们只是 Shape
类的对象。
从技术上讲,Stream<Shape>
实际上将所有内容都当作 Object
保存。当一个元素被取出时,它会自动转回 Shape
,这是反射最基本的形式,在运行时检查了所有的类型转换是否正确,这就是反射的意思:在运行时,确定对象的类型。
在这里,反射类型转换并不彻底:Object
只是被转换成了 Shape
,而没有转换为最终的 Circle
、Square
或 Triangle
。这是因为我们所能得到的信息就是,Stream<Shape>
里保存的都是 Shape
,在编译时,这是由 Stream
和 Java 泛型系统强制保证的,而在运行时,类型转换操作会确保这一点。
接下来就该多态上场了,Shape
对象实际上执行的代码,取决于引用是属于Circle
、Square
还是 Triangle
。一般来说,这是合理的:你希望自己的代码尽可能少地知道对象的确切类型信息,而只和这类对象的通用表示(在本例中为Shape
)打交道。这样的话我们的代码就更易于编写、阅读和维护,并且设计也更易于实现、理解和更改。所以多态是面向对象编程的一个基本目标。
2. Class对象
要想了解 Java 中的反射是如何工作的,就必须先了解类型信息在运行时是如何表示的。这项工作是通过叫作 Class
对象的特殊对象来完成的,它包含了与类相关的信息。事实上,Class
对象被用来创建类的所有“常规”对象,Java 使用 Class
对象执行反射,即使是类型转换这样的操作也一样。Class
类还有许多其他使用反射的方式。
程序中的每个类都有一个 Class
对象,也就是说,每次编写并编译一个新类时,都会生成一个 Class
对象(并被相应地存储在同名的 .class
文件中)。为了生成这个对象,Java 虚拟机(JVM)使用被称为类加载器(class loader)的子系统。
类加载器子系统实际上可以包含一条类加载器链,但里面只会有一个原始类加载器,它是 JVM 实现的一部分。原始类加载器通常从本地磁盘加载所谓的可信类,包括 Java API 类。
类在首次使用时才会被动态加载到 JVM 中。当程序第一次引用该类的静态成员时,就会触发这个类的加载(构造器是类的一个静态方法,尽管没有明确使用 static
关键字)。因此,使用 new
操作符创建类的新对象也算作对该类静态成员的引用,构造器的初次使用会导致该类的加载。
所以,Java 程序在运行前并不会被完全加载,而是在必要时加载对应的部分,这与许多传统语言不同,这种动态加载能力使得 Java 可以支持很多行为。
类加载器首先检查是否加载了该类型的 Class
对象,如果没有,默认的类加载器会定位到具有该名称的 .class
文件(例如,某个附加类加载器可能会在数据库中查找对应的字节码)。当该类的字节数据被加载时,它们会被验证,以确保没有被损坏,并且不包含恶意的 Java 代码(这是 Java 的众多安全防线里的一条)。
一旦该类型的 Class
对象加载到内存中,它就会用于创建该类型的所有对象:
package reflection;class Cookie {static {System.out.println("Loading Cookie");}
}class Gum {static {System.out.println("Loading Gum");}
}class Candy {static {System.out.println("Loading Candy");}
}public class SweetShop {public static void main(String[] args) {System.out.println("Inside main");new Cookie();System.out.println("After creating Cookie");try {Class<?> c = Class.forName("reflection.Gum"); // 一定要完整类名,即包名+类名System.out.println("c.getName(): " + c.getName());System.out.println("After Class.forName(reflection.Gum)");} catch (ClassNotFoundException e) {System.out.println(e);}new Candy();System.out.println("After creating Candy");/** Inside main* Loading Cookie* After creating Cookie* Loading Gum* c.getName(): reflection.Gum* After Class.forName(reflection.Gum)* Loading Candy* After creating Candy*/}
}
我们创建了三个具有静态代码块的类,该静态代码块会在第一次加载类时执行,输出的信息会告诉我们这个类是什么时候加载的。输出结果显示了 Class
对象仅在需要时才加载,并且静态代码块的初始化是在类加载时执行的。
所有的 Class
对象都属于 Class
类,Class
对象和其他对象一样,因此你可以获取并操作它的引用(这也是加载器所做的)。静态的 forName()
方法可以获得 Class
对象的引用,该方法接收了一个包含所需类的文本名称(注意拼写和大小写,且需要是类的完全限定名称,即包括包名称)的字符串,并返回了一个 Class
引用。
不管什么时候,只要在运行时用到类型信息,就必须首先获得相应的 Class
对象的引用,这时 Class.forName()
方法用起来就很方便了,因为不需要对应类型的对象就能获取 Class
引用。但是,如果已经有了一个你想要的类型的对象,就可以通过 getClass()
方法来获取 Class
引用,这个方法属于 Object
根类,它返回的 Class
引用表示了这个对象的实际类型。
Class
类有很多方法,下面是其中的一部分:
package reflection;interface Waterproof {}
interface Shoots {}class Toy {public Toy() {System.out.println("Creating Toy");}public Toy(int i) {}
}class FancyToy extends Toy implements Waterproof, Shoots {public FancyToy() { super(1); }
}public class ClassMethods {static void printInfo(Class c) {System.out.println("getName(): " + c.getName());System.out.println("isInterface(): " + c.isInterface());System.out.println("getSimpleName(): " + c.getSimpleName());System.out.println("getCanonicalName(): " + c.getCanonicalName());}public static void main(String[] args) {Class<?> c = null;try {c = Class.forName("reflection.FancyToy");} catch (ClassNotFoundException e) {System.out.println(e);System.exit(1);}printInfo(c);for (Class iface: c.getInterfaces()) {System.out.println("--------------------");printInfo(iface);}Class sc = c.getSuperclass();Object obj = null;System.out.println("--------------------");try {obj = sc.newInstance(); // 对应类要有public的无参构造器} catch (Exception e) {throw new RuntimeException("Can't instantiate");}printInfo(obj.getClass()); // obj.getClass()即为sc/** getName(): reflection.FancyToy* isInterface(): false* getSimpleName(): FancyToy* getCanonicalName(): reflection.FancyToy* --------------------* getName(): reflection.Waterproof* isInterface(): true* getSimpleName(): Waterproof* getCanonicalName(): reflection.Waterproof* --------------------* getName(): reflection.Shoots* isInterface(): true* getSimpleName(): Shoots* getCanonicalName(): reflection.Shoots* --------------------* Creating Toy* getName(): reflection.Toy* isInterface(): false* getSimpleName(): Toy* getCanonicalName(): reflection.Toy*/}
}
printInfo()
方法使用 getName()
来生成完全限定的类名,使用 getSimpleName()
和 getCanonicalName()
分别生成不带包的名称和完全限定的名称,isInterface()
可以告诉你这个 Class
对象是否表示一个接口,getInterfaces()
方法返回了一个 Class
对象数组,它们表示所调用的 Class
对象的所有接口。还可以使用 getSuperclass()
来查询 Class
对象的直接基类,它将返回一个 Class
引用,而你可以对它做进一步查询。
Class
的 newInstance()
方法是实现虚拟构造器的一种途径,这相当于声明:我不知道你的确切类型,但无论如何你都要正确地创建自己。sc
只是一个 Class
引用,它在编译时没有更多的类型信息,当创建一个新实例时,你会得到一个 Object
引用,但该引用指向了一个 Toy
对象,你可以给它发送 Object
能接收的消息,但如果想要发送除此之外的其他消息,就必须进一步了解它,并进行某种类型转换。此外,使用 Class.newInstance()
创建的类必须有一个无参构造器。
注意,此示例中的 newInstance()
在 Java 8 中还是正常的,但在更高版本中已被弃用,Java 推荐使用 Constructor.newInstance()
来代替。
2.1 类字面量
Java 还提供了另一种方式来生成 Class
对象的引用:类字面量。它看起来像这样:
FancyToy.class
这更简单也更安全,因为它会进行编译时检查(因此不必放在 try
块中),另外它还消除了对 forName()
方法的调用,所以效率也更高。
注意,使用 .class
的形式创建 Class
对象的引用时,该 Class
对象不会自动初始化。实际上,在使用一个类之前,需要先执行以下三个步骤:
- 加载:这是由类加载器执行的,该步骤会先找到字节码(通常在类路径中的磁盘上,但也不一定),然后从这些字节码中创建一个
Class
对象。 - 链接:链接阶段会验证类中的字节码,为静态字段分配存储空间,并在必要时解析该类对其他类的所有引用。
- 初始化:如果有基类的话,会先初始化基类,执行静态初始化器和静态初始化块。
其中,初始化会被延迟到首次引用静态方法(构造器是隐式静态的)或非常量静态字段时:
package reflection;class A {static final int STATIC_FINAL = 1;static int x = 2;static {System.out.println("Initializing A");}
}class B {static {System.out.println("Initializing B");}
}public class ClassInitialization {public static void main(String[] args) throws ClassNotFoundException {System.out.println("Inside main");Class a = A.class; // 不会初始化System.out.println("After creating A ref");System.out.println("A.STATIC_FINAL: " + A.STATIC_FINAL); // 还是不会初始化System.out.println("A.x: " + A.x); // 初始化Class b = Class.forName("reflection.B"); // 初始化System.out.println("After creating B ref");/** Inside main* After creating A ref* A.STATIC_FINAL: 1* Initializing A* A.x: 2* Initializing B* After creating B ref*/}
}
仅使用 .class
语法来获取对类的引用不会导致初始化,而 Class.forName()
会立即初始化类以产生 Class
引用。如果一个 static final
字段的值是编译时常量,比如 A.STATIC_FINAL
,那么这个值不需要初始化 A
类就能读取。
2.2 泛型类的引用
Class
引用指向的是一个 Class
对象,该对象可以生成类的实例,并包含了这些实例所有方法的代码,它还包含该类的静态字段和静态方法,所以一个 Class
引用表示的就是它所指向的确切类型:Class
类的一个对象。
我们可以使用泛型语法来限制 Class
引用的类型:
package reflection;public class GenericClassReferences {public static void main(String[] args) {Class c1 = int.class;c1 = double.class; // 合法Class<Integer> c2 = int.class;c2 = Integer.class; // 合法c2 = Double.class; // 不合法}
}
泛化的类引用 c2
只能分配给其声明的类型,通过使用泛型语法,可以让编译器强制执行额外的类型检查。
如果想放松使用泛化的 Class
引用时的限制,需要使用通配符 ?
,它是 Java 泛型的一部分,表示任何事物:
package reflection;public class GenericClassReferences {public static void main(String[] args) {Class<?> c = Integer.class;c = Double.class; // 合法}
}
我们不能这么写:
Class<Number> c = Integer.class;
即使 Integer
继承自 Number
,但是 Integer
的 Class
对象不是 Number
的 Class
对象的子类。
如果想创建一个 Class
引用,并将其限制为某个类型或任意子类型,可以将通配符与 extends
关键字组合来创建一个界限:
package reflection;public class GenericClassReferences {public static void main(String[] args) {Class<? extends Number> c = Integer.class;c = Double.class; // 合法}
}
将泛型语法添加到 Class
引用的一个原因是提供编译时的类型检查,这样的话,如果你做错了什么,那么很快就能发现。
下面是一个使用了泛型类语法的示例,它存储了一个类引用,然后使用 newInstance()
来生成对象:
package reflection;import java.util.function.Supplier;
import java.util.stream.Stream;class People {private static long counter;private final long id = counter++;public People() {} // 需要有public的无参构造器才能调用newInstance()@Overridepublic String toString() {return "People " + id;}
}public class DynamicSupplier<T> implements Supplier<T> {private Class<T> c;public DynamicSupplier(Class<T> c) { this.c = c; }@Overridepublic T get() {try {return c.getConstructor().newInstance();} catch (Exception e) {throw new RuntimeException(e);}}public static void main(String[] args) {Stream.generate(new DynamicSupplier<>(People.class)).skip(5).limit(5).forEach(System.out::println);/** People 5* People 6* People 7* People 8* People 9*/}
}
DynamicSupplier
会强制要求它使用的任何类型都有一个 public
的无参构造器,如果不符合条件,就会抛出一个异常。在上面的例子中,People
类自动生成的无参构造器不是 public
的,因为 People
类不是 public
的,所以我们必须显式定义它。
对 Class
对象使用泛型语法时,newInstance()
会返回对象的确切类型,而不仅仅是简单的 Object
,但它也会受到一些限制:
package reflection;class Cat {public Cat() {}
}class Kitty extends Cat {public Kitty() {}
}public class GenericCat {public static void main(String[] args) throws Exception {Class<Kitty> kittyClass = Kitty.class;Kitty kitty = kittyClass.getConstructor().newInstance();System.out.println(kitty.getClass().getName()); // reflection.Kitty// Class<Cat> kittySuper = kittyClass.getSuperclass(); // 不合法Class<? super Kitty> kittySuper = kittyClass.getSuperclass();Object obj = kittySuper.getConstructor().newInstance();System.out.println(obj.getClass().getName()); // reflection.Cat}
}
如果你得到了 Kitty
的基类,那么编译器只允许你声明这个基类引用是 Kitty
的某个基类,即 Class<? super Kitty>
,而不能被声明成 Class<Cat>
,因为 getSuperclass()
返回了基类(不是接口),而编译器在编译时就知道这个基类是什么,在这里就是 Cat.class
,而不仅仅是 Kitty
的某个基类。因为存在这种模糊性,所以 kittySuper.getConstructor().newInstance()
的返回值不是一个确切的类型,而只是一个 Object
。
2.3 cast()方法
cast()
方法是用于 Class
引用的类型转换:
package reflection;class House {}class BigHouse extends House {}public class ClassCast {public static void main(String[] args) {House h = new BigHouse();Class<BigHouse> bigHouseClass = BigHouse.class;BigHouse bh = bigHouseClass.cast(h);
// BigHouse bh = (BigHouse)h; // 这种方式更简洁}
}
cast()
方法接收参数对象并将其转换为 Class
引用的类型,在你不能使用普通类型转换(最后一行)的情况下很有用,如果你正在编写泛型代码并且存储了一个用于转型的 Class
引用,就可能会遇到这种情况,不过这很罕见。