Java反射机制详解

CONTENTS

  • 1. 为什么需要反射
  • 2. Class对象
    • 2.1 类字面量
    • 2.2 泛型类的引用

反射使我们摆脱了只能在编译时执行面向类型操作的限制,并且让我们能够编写一些非常强大的程序。本文将讨论 Java 是如何在运行时发现对象和类的信息的,这通常有两种形式:简单反射,它假定你在编译时就已经知道了所有可用的类型;以及更复杂的反射,它允许我们在运行时发现和使用类的信息。

1. 为什么需要反射

面向对象编程的一个基本目标就是,让编写的代码只操纵基类的引用。我们来看下面这个例子:

package reflection;

import java.util.stream.Stream;

abstract class Shape {
    void draw() {
        System.out.println(this + ".draw()");
    }

    @Override
    public abstract String toString();
}

class Circle extends Shape {
    @Override
    public String toString() { return "Circle"; }
}

class Square extends Shape {
    @Override
    public String toString() { return "Square"; }
}

class Triangle extends Shape {
    @Override
    public 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 类的对象。

从技术上讲,Stream 实际上将所有内容都当作 Object 保存。当一个元素被取出时,它会自动转回 Shape,这是反射最基本的形式,在运行时检查了所有的类型转换是否正确,这就是反射的意思:在运行时,确定对象的类型。

在这里,反射类型转换并不彻底:Object 只是被转换成了 Shape,而没有转换为最终的 CircleSquareTriangle。这是因为我们所能得到的信息就是,Stream 里保存的都是 Shape,在编译时,这是由 Stream 和 Java 泛型系统强制保证的,而在运行时,类型转换操作会确保这一点。

接下来就该多态上场了,Shape 对象实际上执行的代码,取决于引用是属于CircleSquare 还是 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 引用,而你可以对它做进一步查询。

ClassnewInstance() 方法是实现虚拟构造器的一种途径,这相当于声明:我不知道你的确切类型,但无论如何你都要正确地创建自己。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,但是 IntegerClass 对象不是 NumberClass 对象的子类。

如果想创建一个 Class引用,并将其限制为某个类型或任意子类型,可以将通配符与 extends 关键字组合来创建一个界限

package reflection;

public class GenericClassReferences {
    public static void main(String[] args) {
        Class<? extends Number> c = Integer.class;
        c = Double.class;  // 合法
    }
}

将泛型语法添加到 Class 引用的一个原因是提供编译时的类型检查,这样的话,如果你做错了什么,那么很快就能发现。

你可能感兴趣的:(Java,java,开发语言,学习,后端,笔记)