19反射

反射可以在程序运行时发现并使用对象的类型信息。

反射使我们摆脱了只能在编译时执行面向类型操作的限制,并且让我们能够编写一些非常强大的程序。对反射的需要,揭示了面向对象设计中大量有趣(且复杂)的问题,并引发了我们对一些基本问题的思考,例如程序如何构建。

本章将讨论Java是如何在运行时发现对象和类的信息的。这通常有两种形式:简单反射,它假定你在编译时就已经知道了所有可用的类型;以及更复杂的反射,它允许我们在运行时发现和使用类的信息。

19.1 为什么需要反射

这里我们使用一个已经很熟悉的示例,它使用了多态并展示了类的层次结构。它的泛化类型是基类Shape,具体的子类型包括CircleSquareTriangle(见图19-1)。

这是一个典型的类层次结构图,基类在顶部,子类向下扩展。面向对象编程的一个基本目标就是,让编写的代码只操纵基类(本例中为Shape)的引用,因此如果你决定添加新类(例如继承了ShapeRhomboid),大部分的代码不会受到影响。在这个例子中,Shape接口中的方法draw()是可以动态绑定的,因此客户程序员可以通过泛化的Shape引用来调用具体的draw()方法。在所有子类中,draw()都被重写,并且因为它是一个动态绑定的方法,即使通过泛化的Shape引用来调用它,也会产生正确的行为。这就是多态。

因此,通常来说你会创建一个特定的对象(CircleSquareTriangle),将其向上转型为Shape(忽略对象的特定类型),这样就可以在后续的程序里一直使用这个Shape引用,而不需要知道其具体类型。

你可以像下面这样,对Shape的层次结构进行编程:

package reflection;

import java.util.stream.Stream;

/**
 * @author: Caldarius
 * @date: 2023/2/10
 * @description:
 */
abstract class Shape {
    void draw() {
        //这里看到 this + string,直接调this.toString()
        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);
    }
}
/*Output:
Circle.draw()
Square.draw()
Triangle.draw()
*/

基类里包含一个draw()方法,它通过将this传递给System.out.println(),间接地使用了toString()方法来显示类的标识符**(toString()方法被声明为abstract的,这样就可以强制子类重写该方法,并防止没什么内容的Shape类被实例化)。**如果一个对象出现在字符串拼接表达式中(该表达式需要包含+String对象),这个对象的toString()方法就会被自动调用,来生成一个代表它自身的字符串。每个子类都重写了toString()方法(从Object继承而来),所以draw()最终(多态地)在不同的情况下打印出了不同的内容。

在此示例中,将一个Shape的子类对象放入Stream<Shape>时,会发生隐式的向上转型。在向上转型为Shape时,这个对象的确切类型信息就丢失了。对于流来说,它们只是Shape类的对象。

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

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

接下来就该多态上场了,Shape对象实际上执行的代码,取决于引用是属于CircleSquare还是Triangle。一般来说,这是合理的:**你希望自己的代码尽可能少地知道对象的确切类型信息,而只和这类对象的通用表示(在本例中为Shape)打交道。**这样的话,我们的代码就更易于编写、阅读和维护,并且设计也更易于实现、理解和更改。所以多态是面向对象编程的一个基本目标。

但是,假设你遇到了一个特殊的编程问题,只要知道这个泛化引用的确切类型,就可以很容易地解决,这样的话你又该怎么办呢?例如,假设我们允许用户可以将某种特定类型的所有形状都标记为一种特殊的颜色,以突出显示它们。这样,用户就可以找到屏幕上所有突出显示的三角形。或者,你的方法需要“旋转”一系列的形状,但旋转圆形是没有意义的,因此你想跳过圆形。通过反射,你可以查询到某个Shape引用所指的确切类型,从而选择并隔离特殊情况。

19.2 Class对象

要想了解Java中的反射是如何工作的,就必须先了解类型信息在运行时是如何表示的。这项工作是通过叫作**Class对象**的特殊对象来完成的,它包含了与类相关的信息。事实上,Class对象被用来创建类的所有“常规”对象。Java使用Class对象执行反射,即使是类型转换这样的操作也一样。Class类还有许多其他使用反射的方式。

程序中的每个类都有一个Class对象。也就是说,每次编写并编译一个新类时,都会生成一个Class对象(并被相应地存储在同名的.class文件中)。为了生成这个对象,Java虚拟机(JVM)使用被称为类加载器(class loader)的子系统。

类加载器子系统实际上可以包含一条类加载器链,但里面只会有一个原始类加载器(即启动类加载器),它是JVM实现的一部分。原始类加载器通常从本地磁盘加载所谓的可信类,包括Java API类。通常来说我们不需要加载器链中的额外加载器,但对于特殊需要(例如以某种方式加载类以支持Web服务器应用程序,或通过网络来下载类),你可以引入额外的类加载器来实现。

类在首次使用时才会被动态加载到JVM中。当程序第一次引用该类的静态成员时,就会触发这个类的加载。构造器是类的一个静态方法,尽管没有明确使用static关键字。因此,使用new操作符创建类的新对象也算作对该类静态成员的引用,构造器的初次使用会导致该类的加载。

所以,Java程序在运行前并不会被完全加载,而是在必要时加载对应的部分。这与许多传统语言不同。这种动态加载能力使得Java可以支持很多行为,而它们在静态加载语言(如 C++)中很难复制,或根本不可能复制。

类加载器首先检查是否加载了该类型的Class对象。如果没有,默认的类加载器会定位到具有该名称的.class文件(例如,某个附加类加载器可能会在数据库中查找对应的字节码)。当该类的字节数据被加载时,它们会被验证,以确保没有被损坏,并且不包含恶意的Java代码(这是Java的众多安全防线里的一条)。

一旦该类型的Class对象加载到内存中,它就会用于创建该类型的所有对象。下面这个程序可以证明这一点:

package reflection;


/**
 * @author: Caldarius
 * @date: 2023/2/10
 * @description:
 */
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 Candy();
        System.out.println("After creating Candy");
        try {
            Class.forName("Gum");
        } catch(ClassNotFoundException e) {
            System.out.println("Couldn't find Gum");
        }
        System.out.println("After Class.forName(\"Gum\")");
        new Cookie();
        System.out.println("After creating Cookie");
    }
}
/*
Output:
inside main
Loading Candy
After creating Candy
Couldn't find Gum
After Class.forName("Gum")
Loading Cookie
After creating Cookie
*/

CandyGumCookie这三个类都有一个静态代码块,该静态代码块在类第一次加载时执行。输出的信息会告诉我们这个类是什么时候加载的。在main()方法中,对象的创建被置于打印语句之间,以方便我们判断类加载的时间。

输出结果显示了Class对象仅在需要时才加载,并且静态代码块的初始化是在类加载时执行的。

下面这一行代码特别有趣:

Class.forName("Gum");

所有的Class对象都属于Class类。Class对象和其他对象一样,因此你可以获取并操作它的引用(这也是加载器所做的)。静态的forName()方法可以获得Class对象的引用,该方法接收了一个包含所需类的文本名称(注意拼写和大小写!)的字符串,并返回了一个Class引用,上面示例中的返回值被忽略;**我们对forName()的调用只是为了它的副作用:如果类Gum尚未加载,则加载它。**在加载过程中,会执行Gum的静态代码块。

在前面的例子中,如果Class.forName()因为找不到试图加载的类而失败,它会抛出一个ClassNotFoundException。在这里,我们只是简单地报告了问题并继续执行,但在更复杂的程序中,你可能会尝试在异常处理流程中修复这个问题(在进阶卷第8章中有个示例)。

注意,传递给forName()的字符串参数必须是类的完全限定名称(包括包名称)。

**不管什么时候,只要在运行时用到类型信息,就必须首先获得相应的Class对象的引用。**这时Class.forName()方法用起来就很方便了,因为不需要对应类型的对象就能获取Class引用。但是,如果已经有了一个你想要的类型的对象,就可以通过getClass()方法来获取Class引用,这个方法属于Object根类。它返回的Class引用表示了这个对象的实际类型。Class类有很多方法,下面是其中的一部分:

package reflection.toys;

/**
 * @author: Caldarius
 * @date: 2023/2/10
 * @description:
 */
interface HasBatteries {}
interface Waterproof {}
interface Shoots {}

class Toy {
    // 可以将下面这个无参构造器注释掉来看一下NoSuchMethodError
    public Toy() {}
    public Toy(int i) {}
}
//FancyToy继承了类Toy并实现了接口HasBatteries、Waterproof和Shoots。
class FancyToy extends Toy
        implements HasBatteries, Waterproof, Shoots {
    public FancyToy() { super(1); }
}

public class ToyTest {
    //printInfo()方法使用getName()来生成完全限定的类名
    static void printInfo(Class cc) {
        //isInterface()可以告诉你这个Class对象是否表示一个接口
        System.out.println("Class name: " + cc.getName() +
                " is interface? [" + cc.isInterface() + "]");
        //使用getSimpleName()和getCanonicalName()分别生成不带包的名称和完全限定的名称。
        System.out.println(
                "Simple name: " + cc.getSimpleName());
        System.out.println(
                "Canonical name : " + cc.getCanonicalName());
    }
    @SuppressWarnings("deprecation")
    public static void main(String[] args) {
        Class c = null;
        try {
            //注意,传递给forName()的字符串参数必须是类的完全限定名称(包括包名称)。
            c = Class.forName("reflection.toys.FancyToy");
        } catch(ClassNotFoundException e) {
            System.out.println("Can't find FancyToy");
            System.exit(1);
        }
        printInfo(c);
        //在main()中调用的Class.getInterfaces()方法返回了一个Class对象数组,它们表示你感兴趣的这个Class对象的所有接口。
        for(Class face : c.getInterfaces())
            printInfo(face);
        //还可以使用getSuperclass()来查询Class对象的直接基类。它将返回一个Class引用,而你可以对它做进一步查询。这样你就可以在运行时获取一个对象的完整类层次结构。
        Class up = c.getSuperclass();
        Object obj = null;
        try {
            //Class的newInstance()方法是实现“虚拟构造器”的一种途径,这相当于声明“我不知道你的确切类型,但无论如何你都要正确地创建自己”。在前面的例子中,up只是一个Class引用,它在编译时没有更多的类型信息。当创建一个新实例时,你会得到一个Object引用。但该引用指向了一个Toy对象。你可以给它发送Object能接收的消息,但如果想要发送除此之外的其他消息,就必须进一步了解它,并进行某种类型转换。此外,使用Class.newInstance()创建的类必须有一个public的无参构造器。在本章后面,你将看到如何通过Java反射API,使用任意构造器来动态创建类的对象。
            //注意,此示例中的newInstance()在Java 8中还是正常的,但在更高版本中已被弃用,Java推荐使用Constructor.newInstance()来代替。示例中我们使用了@SuppressWarnings("deprecation")来抑制那些更高版本的弃用警告。
            obj = up.newInstance();
        } catch(Exception e) {
            throw new
                    RuntimeException("Cannot instantiate");
        }
        printInfo(obj.getClass());
    }
}
/*
Output:
Class name: reflection.toys.FancyToy is interface? [false]
Simple name: FancyToy
Canonical name : reflection.toys.FancyToy
Class name: reflection.toys.HasBatteries is interface? [true]
Simple name: HasBatteries
Canonical name : reflection.toys.HasBatteries
Class name: reflection.toys.Waterproof is interface? [true]
Simple name: Waterproof
Canonical name : reflection.toys.Waterproof
Class name: reflection.toys.Shoots is interface? [true]
Simple name: Shoots
Canonical name : reflection.toys.Shoots
Class name: reflection.toys.Toy is interface? [false]
Simple name: Toy
Canonical name : reflection.toys.Toy
*/

19.2.1 类字面量

Java还提供了另一种方式来生成Class对象的引用:类字面量(class literal)。对前面的程序而言,它看起来像这样:

FancyToy.class;

这更简单也更安全,因为它会进行编译时检查(因此不必放在try块中)。另外它还消除了对forName()方法的调用,所以效率也更高。

类字面量适用于常规类以及接口、数组和基本类型。

此外,每个基本包装类都有一个名为TYPE的标准字段。TYPE字段表示一个指向和基本类型对应的Class对象的引用。

上面的意思并不是说包装类不能用 .class,而是说包装类.TYPE会获取到一个指向基本类型的Class对象的引用。简单的例子:

package reflection;

/**
 * @author: Caldarius
 * @date: 2023/2/13
 * @description:
 */
public class BasicPackingClass {
    public static void main(String[] args) {
        System.out.println(Integer.class.getSimpleName());
        System.out.println(Integer.TYPE);
    }
}
/*
Output:
Integer
int
*/

如表19-1所示。

表19-1

类字面量 等价于
boolean.class Boolean.TYPE
char.class Character.TYPE
byte.class Byte.TYPE
short.class Short.TYPE
int.class Integer.TYPE
long.class Long.TYPE
float.class Float.TYPE
double.class Double.TYPE
void.class Void.TYPE

我建议尽可能用“.class”的形式,因为它与常规类更一致。

请注意,使用“.class”的形式创建Class对象的引用时,该Class对象不会自动初始化。实际上,在使用一个类之前,需要先执行以下3个步骤。

  1. 加载。这是由类加载器执行的。该步骤会先找到字节码(通常在类路径中的磁盘上,但也不一定),然后从这些字节码中创建一个Class对象。
  2. 链接。链接阶段会验证类中的字节码,为静态字段分配存储空间,并在必要时解析该类对其他类的所有引用。
  3. 初始化。如果有基类的话,会先初始化基类,执行静态初始化器和静态初始化块。

初始化被延迟到首次引用静态方法(构造器是隐式静态的)或非常量静态字段时:

package reflection;

import java.util.Random;

/**
 * @author: Caldarius
 * @date: 2023/2/10
 * @description:
 */
class Initable {
    static final int STATIC_FINAL = 47;
    static final int STATIC_FINAL2 =
            ClassInitialization.rand.nextInt(1000);
    static {
        System.out.println("Initializing Initable");
    }
}

class Initable2 {
    static int staticNonFinal = 147;
    static {
        System.out.println("Initializing Initable2");
    }
}

class Initable3 {
    static int staticNonFinal = 74;
    static {
        System.out.println("Initializing Initable3");
    }
}
public class ClassInitialization {

    public static Random rand = new Random(47);
    public static void
    main(String[] args) throws Exception {
        // 不会触发初始化
        Class initable = Initable.class;
        System.out.println("After creating Initable ref");
        // 不会触发初始化。如果一个static final字段的值是“编译时常量”,比如Initable.staticFinal,那么这个值不需要初始化Initable类就能读取。
        System.out.println(Initable.STATIC_FINAL);
        // 触发初始化。但是把一个字段设置为static和final并不能保证这种行为:对Initable.staticFinal2的访问会强制执行类的初始化,因为它不是编译时常量。
        System.out.println(Initable.STATIC_FINAL2);
        // 触发初始化
        System.out.println(Initable2.staticNonFinal);
        Class initable3 = Class.forName("reflection.Initable3");
        System.out.println("After creating Initable3 ref");
        System.out.println(Initable3.staticNonFinal);
    }
}
/*
Output:
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74
*/

实际上,初始化会“尽可能懒惰”。从initable引用的创建过程中可以看出,仅使用.class语法来获取对类的引用不会导致初始化。而Class.forName()会立即初始化类以产生Class引用,如initable3的创建所示。

如果一个static final字段的值是“编译时常量”,比如Initable.staticFinal,那么这个值不需要初始化Initable类就能读取。但是把一个字段设置为staticfinal并不能保证这种行为:对Initable.staticFinal2的访问会强制执行类的初始化,因为它不是编译时常量。

如果static字段不是final的,那么访问它时,如果想要正常读取,总是需要先进行链接(为字段分配存储)和初始化(初始化该存储),正如在对Initable2.staticNonFinal的访问中看到的那样。

19.2.2 泛型类的引用

Class引用指向的是一个Class对象,该对象可以生成类的实例,并包含了这些实例所有方法的代码。它还包含该类的静态字段和静态方法。所以一个Class引用表示的就是它所指向的确切类型:Class类的一个对象。

你可以使用泛型语法来限制Class引用的类型。在下面的示例中,这两种语法都是正确的:

Class intClass = int.class;
intClass = double.class;

Class<Integer> genericIntClass = int.class;
genericIntClass = Integer.class; // 一样
// genericIntClass = double.class; // 不合法

intClass可以重新赋值为任何其他的Class对象,例如double.class,而不会产生警告。泛化的类引用genericIntClass只能分配给其声明的类型。通过使用泛型语法,可以让编译器强制执行额外的类型检查。

如果你想稍微放松一下这种限制,那该怎么办?乍一看,好像可以执行下面这样的操作:

Class<Number> genericNumberClass = int.class;

这似乎是有道理的,因为Integer继承了Number。但实际上这段代码无法运行,因为IntegerClass对象不是NumberClass对象的子类(这里的区别看起来好像很微妙,我们将在第20章对此进行深入讨论)。

要想放松使用泛化的Class引用时的限制,请使用通配符?,它是Java泛型的一部分,表示“任何事物”。所以我们可以在上面的例子中为普通的Class引用加上通配符,这样就可以产生相同的结果:

Class<?> intClass = int.class;
    intClass = double.class;

尽管如我们看到的那样,普通的Class并不会产生编译器警告,但是和普通的Class相比,我们还是倾向于Class<?>,即使它们是等价的。Class<?>的好处在于,它表明了你不是偶然或无意识地使用了非具体的类引用。你就是选择了这个非具体的版本。

如果想创建一个Class引用,并将其限制为某个类型或任意子类型,可以将通配符与extends关键字组合来创建一个界限(bound)。因此,与其使用Class<Number>,不如像下面这样做:

Class<? extends Number> bounded = int.class;
    bounded = double.class;
    bounded = Number.class;
    // 或者任何继承了Number的类

将泛型语法添加到Class引用的一个原因是提供编译时的类型检查。这样的话,如果你做错了什么,那么很快就能发现。使用普通的Class引用时,你可能确实不会误入歧途,但是如果你犯了一个错误,直到运行时才发现,那就可能会给你带来不便,甚至导致问题。

下面是一个使用了泛型类语法的示例。它存储了一个类引用,然后使用newInstance()来生成对象:

package reflection;

import java.util.function.Supplier;
import java.util.stream.Stream;

/**
 * @author: Caldarius
 * @date: 2023/2/10
 * @description:
 */
class ID {
    //这个字段用于计数;
    private static long counter;
    //用于标记this是第几个id;
    private final long id = counter++;
    @Override public String toString() {
        return "ID" + Long.toString(id);
    }
    // 如果想要调用getConstructor().newInstance(),就需要提供一个public的无参构造器:ID自动生成的无参构造器不是public的,因为ID类不是public的,所以我们必须显式定义它。
    public ID() {}
}
public class DynamicSupplier<T> implements Supplier<T> {
    //标记这个DynamicSupplier对象实际上会生成什么类型的对象组,实际上是final,因为没有提供set方法;
    private Class<T> type;
    public DynamicSupplier(Class<T> type) {
        this.type = type;
    }
    @Override
    public T get() {
        try {
            //调用newInstance()方法创建对应类型的对象;
            return type.getConstructor().newInstance();
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }
    public static void main(String[] args) {
        Stream.generate(
                //传入需要生成的对象的类型
                        new DynamicSupplier<>(ID.class))
                .skip(10)
                .limit(5)
                .forEach(System.out::println);
    }
}
/*
Output:
10
11
12
13
14
*/

Class对象使用泛型语法时,newInstance()会返回对象的确切类型,而不仅仅是简单的Object,就像在ToyTest.java示例中看到的那样。但它也会受到一些限制:

//class FancyToy extends Toy
Class<FancyToy> ftc = FancyToy.class;
// 生成确切的类型:
FancyToy fancyToy =
  ftc.getConstructor().newInstance();
//up可以是FancyToy及其任意父类型
Class<? super FancyToy> up = ftc.getSuperclass();
// 下面的代码无法通过编译:
// Class<Toy> up2 = ftc.getSuperclass();
// 只能生成Object
Object obj = up.getConstructor().newInstance();

如果你得到了基类,那么编译器只允许你声明这个基类引用是“FancyToy的某个基类”,就像表达式Class<? super FancyToy>所声明的那样。它不能被声明为Class<Toy>。这看起来有点儿奇怪,因为getSuperclass()返回了基类(不是接口),而编译器在编译时就知道这个基类是什么——在这里就是Toy.class,而不仅仅是“FancyToy的某个基类”。不管怎么样,因为存在这种模糊性,所以up.getConstructor().newInstance()的返回值不是一个确切的类型,而只是一个Object

也就是说,虽然我知道这个基类是Toy,但是返回的类型只能是“某个基类”,而不能是确定的某个类。所以getConstructor().newInstance()的返回值不是一个确切的类型,而只是一个Object

19.2.3 cast()方法

还有一个用于Class引用的类型转换语法,即cast()方法:

class Building {}
class House extends Building {}

public class ClassCasts {
  public static void main(String[] args) {
    Building b = new House();
    Class<House> houseType = House.class;
    //在这里将Building转换为了House
    House h = houseType.cast(b);
    // 或者直接这样进行转型
    h = (House)b; 
  }
}

cast()方法接收参数对象并将其转换为Class引用的类型。但是,如果观察上面的代码,你会发现,与完成了相同工作的main()的最后一行相比,这种方式似乎做了很多额外的工作。

cast()在你不能使用普通类型转换的情况下很有用。如果你正在编写泛型代码(你将在第20章中学习),并且存储了一个用于转型的Class引用,就可能会遇到这种情况。不过这很罕见——我发现在整个Java库中只有一个地方使用了cast()(也就是在com.sun.mirror.util.DeclarationFilter中)。

另一个在Java库中没有使用到的特性是Class.asSubclass()。它会将类对象转换为更具体的类型。

19.3 转型前检查

到目前为止,你已经学习了以下内容。

  1. 传统的类型转换。比如“(Shape)”,它使用反射来确保转型是正确的。如果你执行了错误的转型,它会抛出一个ClassCastException
  2. 代表对象类型的Class对象。你可以查询Class对象来获取有用的运行时信息。

在C++中,传统的类型转换“(Shape)”并执行反射[在C++中,这称为运行时类型识别(RTTI)]。它只是告诉编译器将对象视为新类型。在Java中,它的确会执行类型检查,这种转型通常称为“类型安全向下转型”(type-safe downcast)。之所以会有术语“向下转型”,是因为类层次结构图从来就是这么排列的。如果将Circle转型为Shape是向上转型,那么将Shape转型为Circle就是向下转型。不过,编译器知道一个Circle也是一个Shape,所以它允许自由地做向上转型的赋值操作,而不需要任何额外的语法。但编译器无法知道一个给定的Shape实际上是什么——它可能就是一个Shape,也可能是Shape的子类型,例如CircleSquareTriangle或其他类型。在编译时,编译器只知道这是一个Shape因此,如果不使用显式的类型转换来告诉编译器这是一个特定的类型,编译器就不会允许执行向下转型赋值操作(编译器会检查该向下转型操作是否合理,因此它不会让你向下转型为实际上不是其子类的类型)。

Java中还有第三种形式的反射。这就是关键字instanceof,它返回一个boolean值,表明一个对象是否是特定类型的实例。因此你可以像下面这样,以问题的形式来使用它:

if(x instanceof Dog)
  ((Dog)x).bark();

在将x转换为Dog之前,你可以用if语句检查一下对象x是否属于类Dog。当没有其他信息可以告诉你对象类型的时候,在向下转型之前使用instanceof很重要。否则,你会得到一个ClassCastException

通常,即使只想寻找确切的类型(例如可以变成紫色的三角形),你也可以使用instanceof来轻松识别所有的对象。例如,假设有一系列描述Pet的类(以及它们的主人,这个特性在后面的例子中会派上用场)。层次结构中的每个Individual都有一个id和一个可选的名称。虽然以下类都继承自Individual,但Individual类比较复杂,因此在进阶卷附录C中进行了说明和解释。

这里其实没必要看Individual的代码——只需要知道你可以创建它的具名或不具名的对象就可以了,而且每个Individual都有一个id()方法来返回其唯一标识符(通过计算生成对象的个数获得)。还有一个toString()方法——如果你没有为Individual提供一个名字,toString()就会生成简单的类型名称。

下面是继承自Individual的类层次结构:

接下来,我们需要一种方法来随机地创建Pet对象。为了使这个工具有不同的实现,我们将它定义为一个抽象类:

package reflection.pets;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author: Caldarius
 * @date: 2023/2/13
 * @description:
 */
public abstract class Creator implements Supplier<Pet> {
    private Random rand = new Random(34);
    //创建不同类型的Pet;
    //抽象的types()方法需要在Creator的子类里实现,以生成一个包含了Class对象的List。这是模板方法(Template Method)设计模式的一个例子。注意,List的泛型参数被指定为“继承了Pet的任意子类”,因此newInstance()无须类型转换即可生成一个Pet。
    public abstract List<Class<? extends Pet>> types();

    //get()会查找List的索引来生成一个Class对象。getConstructor()会生成一个Constructor对象,而newInstance()使用该Constructor来创建一个对象。
    @Override
    public Pet get() {
        int n = rand.nextInt(types().size());
        try {
            return types().get(n)
                    .getConstructor().newInstance();
        //调用newInstance()时可能会得到四种异常。你可以在try块后面的catch子句里看到对它们的处理。这些异常的名称本身很好地解释了它们所代表的错误内容(IllegalAccessException表示违反了Java的安全机制,在本例中,如果无参构造器是private的,就会抛出这种异常)。
        } catch(InstantiationException |
                NoSuchMethodException |
                InvocationTargetException |
                IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
    public Stream<Pet> stream() {
        return Stream.generate(this);
    }
    public Pet[] array(int size) {
        return stream().limit(size).toArray(Pet[]::new);
    }
    public List<Pet> list(int size) {
        return stream().limit(size)
                .collect(Collectors.toCollection(ArrayList::new));
    }
}

在实现Creator的子类时,必须提供一个Pet类型的List,这样才可以调用get()方法来获取Pet对象。types()方法一般来说只需要返回一个静态List的引用就可以了。下面是一个使用forName()实现的示例:

package reflection.pets;

import java.util.ArrayList;
import java.util.List;

/**
 * @author: Caldarius
 * @date: 2023/2/13
 * @description:
 */
public class ForNamePetCreator extends Creator{
    private static List<Class<? extends Pet>> types =
            new ArrayList<>();
    // 你想随机生成的类型:
    private static String[] typeNames = {
            "reflection.pets.Mutt",
            "reflection.pets.Pug",
            "reflection.pets.EgyptianMau",
            "reflection.pets.Manx",
            "reflection.pets.Cymric",
            "reflection.pets.Rat",
            "reflection.pets.Mouse",
            "reflection.pets.Hamster"
    };
    //loader()方法使用Class.forName()来创建一个Class对象的列表,可能会抛出ClassNotFoundException。这是合理的,因为你传递给它的是一个在编译时无法验证的字符串。Pet对象在reflection包中,所以必须使用包名来引用这些类。
    @SuppressWarnings("unchecked")
    private static void loader() {
        try {
            for(String name : typeNames)
                types.add(
                        //这里经行了强制类型转换,会产生编译时警告
                        (Class<? extends Pet>)Class.forName(name));
        } catch(ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
    //要生成具有实际类型的Class对象的列表,就需要进行强制类型转换,这会产生编译时警告。我们单独定义了loader()方法,然后在静态初始化块中调用了它,这是因为@SuppressWarnings("unchecked")注解不能直接用于静态初始化块。
    static { loader(); }
    @Override public List<Class<? extends Pet>> types() {
        return types;
    }
}

如果想要知道Pet有多少,我们需要一个工具来跟踪各种不同类型的Pet的数量。此时采用Map就非常适合:键可以是Pet类型的名称,而值则是保存了Pet数量的Integer。这样,你可以查询“有多少个Hamster对象”,使用instanceof来获得对应Pet的数量:

package reflection.pets;

import java.util.HashMap;

/**
 * @author: Caldarius
 * @date: 2023/2/13
 * @description:
 */
public class PetCounter {
    //这个计数器通过继承完成了对HashMap的功能拓展。
    static class Counter extends HashMap<String,Integer> {
        public void count(String type) {
            Integer quantity = get(type);
            if(quantity == null)
                put(type, 1);
            else
                put(type, quantity + 1);
        }
    }
    private Counter counter = new Counter();
    private void countPet(Pet pet) {
        System.out.print(
                pet.getClass().getSimpleName() + " ");
        if(pet instanceof Pet)
            counter.count("Pet");
        if(pet instanceof Dog)
            counter.count("Dog");
        if(pet instanceof Mutt)
            counter.count("Mutt");
        if(pet instanceof Pug)
            counter.count("Pug");
        if(pet instanceof Cat)
            counter.count("Cat");
        if(pet instanceof EgyptianMau)
            counter.count("EgyptianMau");
        if(pet instanceof Manx)
            counter.count("Manx");
        if(pet instanceof Cymric)
            counter.count("Cymric");
        if(pet instanceof Rodent)
            counter.count("Rodent");
        if(pet instanceof Rat)
            counter.count("Rat");
        if(pet instanceof Mouse)
            counter.count("Mouse");
        if(pet instanceof Hamster)
            counter.count("Hamster");
    }
    public void count(Creator creator) {
        creator.stream().limit(20)
                .forEach(pet -> countPet(pet));
        System.out.println();
        System.out.println(counter);
    }
    public static void main(String[] args) {
        new PetCounter().count(new ForNamePetCreator());
    }
}
/*
Output:
Rat Hamster EgyptianMau Rat Rat Mutt Mutt EgyptianMau Rat Rat Cymric EgyptianMau Rat Mutt Rat Mutt Hamster Rat Mouse Cymric
{EgyptianMau=3, Rat=8, Cymric=2, Mouse=1, Cat=5, Manx=2, Rodent=11, Mutt=4, Dog=4, Pet=20, Hamster=2}
*/

countPet()中,我们使用instanceof来对数组里的每个Pet进行测试和计数。

instanceof有一个相当严格的限制:只能将其与命名类型进行比较,而不能与一个Class对象进行比较。在前面的例子中,你可能认为像这样写一大堆的instanceof表达式很乏味,的确是这样的。但是如果你想创建一个Class对象数组,并将其与那些对象进行比较,从而将instanceof巧妙地自动化,这是不可能的(不过稍后你会看到另一个替代方案)。这个限制其实并不像你想象的那么严重,因为最终你会明白,如果代码里有许多的instanceof表达式,那么这个设计可能是存在缺陷的。

19.3.1 使用类字面量

如果我们使用类字面量重新实现Creator,那么最终结果在许多方面都会显得更清晰:

package reflection.pets;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * @author: Caldarius
 * @date: 2023/2/13
 * @description:
 */
public class PetCreator extends Creator{
    //这一次,types的创建代码并不需要放在try块里,因为它在编译时被检查,所以不会抛出任何异常,这和Class.forName()不一样。
    public static final List<Class<? extends Pet>> ALL_TYPES =
        Collections.unmodifiableList(Arrays.asList(
                Pet.class, Dog.class, Cat.class, Rodent.class,
                Mutt.class, Pug.class, EgyptianMau.class,
                Manx.class, Cymric.class, Rat.class,
                Mouse.class, Hamster.class));
    // 这里的types列表是ALL_TYPES(使用List.subList()创建)的一部分,它包含了确切的宠物类型,因此可以用来生成随机的Pet。
    //在即将出现的PetCounter3.java示例中,我们会预先加载一个包含所有Pet类型(不仅仅是那些随机生成的)的Map,因此这个ALL_TYPES的List是必要的
    private static final List<Class<? extends Pet>> TYPES =
            ALL_TYPES.subList(
                    ALL_TYPES.indexOf(Mutt.class),
                    ALL_TYPES.size());
    @Override
    public List<Class<? extends Pet>> types() {
        return TYPES;
    }
    public static void main(String[] args) {
        System.out.println(TYPES);
        List<Pet> pets = new PetCreator().list(7);
        System.out.println(pets);
        //测试PetCreator
        System.out.println("测试PetCreator");
        new PetCounter().count(new PetCreator());
    }
}
/*
Output:
[class reflection.pets.Mutt, class reflection.pets.Pug, class reflection.pets.EgyptianMau, class reflection.pets.Manx, class reflection.pets.Cymric, class reflection.pets.Rat, class reflection.pets.Mouse, class reflection.pets.Hamster]
[Rat, Hamster, EgyptianMau, Rat, Rat, Mutt, Mutt]
测试PetCreator
Rat Hamster EgyptianMau Rat Rat Mutt Mutt EgyptianMau Rat Rat Cymric EgyptianMau Rat Mutt Rat Mutt Hamster Rat Mouse Cymric 
{EgyptianMau=3, Rat=8, Cymric=2, Mouse=1, Cat=5, Manx=2, Rodent=11, Mutt=4, Dog=4, Pet=20, Hamster=2}
*/

19.3.2 动态的instanceof

Class.isInstance()方法提供了一种动态验证对象类型的方式。因此,那些乏味的instanceof语句就都可以从PetCounter.java中删除了:

package reflection.pets;

import onjava.Pair;

import java.util.*;
import java.util.stream.*;

/**
 * @author: Caldarius
 * @date: 2023/2/13
 * @description:
 */
public class PetCounter3 {
    static class Counter extends
            HashMap<Class<? extends Pet>, Integer> {
        Counter() {
            super(PetCreator.ALL_TYPES.stream()
                    .map(type -> Pair.make(type, 0))
                    .collect(
                            Collectors.toMap(Pair::key, Pair::value)));
        }
        public void count(Pet pet) {
            // Class.isInstance()消除了大量的instanceof:
            entrySet().stream()
                    .filter(pair -> pair.getKey().isInstance(pet))
                    .forEach(pair ->
                            put(pair.getKey(), pair.getValue() + 1));
        }
        @Override public String toString() {
            String result = entrySet().stream()
                    .map(pair -> String.format("%s=%s",
                            pair.getKey().getSimpleName(),
                            pair.getValue()))
                    .collect(Collectors.joining(", "));
            return "{" + result + "}";
        }
    }
    public static void main(String[] args) {
        Counter petCount = new Counter();
        new PetCreator().stream()
                .limit(20)
                .peek(petCount::count)
                .forEach(p -> System.out.print(
                        p.getClass().getSimpleName() + " "));
        System.out.println("\n" + petCount);
    }
}
/*
Output:Rat Hamster EgyptianMau Rat Rat Mutt Mutt EgyptianMau Rat Rat Cymric EgyptianMau Rat Mutt Rat Mutt Hamster Rat Mouse Cymric
{Mutt=4, Manx=2, Dog=4, Rat=8, Cat=5, EgyptianMau=3, Rodent=11, Cymric=2, Hamster=2, Pet=20, Pug=0, Mouse=1}
*/

为了对所有不同类型的Pet进行计数,Counter继承了HashMap并预加载了PetCreator.ALL_TYPES里的类型。如果不预加载Map里的数据,你最终就只能对随机生成的类型进行计数,而不能包括诸如PetCat这样的基类型。

isInstance()方法使我们不再需要instanceof表达式。此外,这还意味着,如果想添加新的Pet类型,只需要更改PetCreator.types数组就可以,程序的其余部分不需要修改(但在使用instanceof表达式时就不可以)。

我们重写了toString()方法来提供更易于阅读的输出,该输出与打印Map时看到的典型输出相似。

19.3.3 递归计数

PetCounter3.Counter中的Map预先加载了所有不同的Pet类。我们还可以使用Class.isAssignableFrom()方法代替Map的预加载,来创建一个并不仅限于对Pet进行计数的通用工具:

package onjava;

import java.util.HashMap;
import java.util.stream.Collectors;

/**
 * @author: Caldarius
 * @date: 2023/2/13
 * @description:
 */
public class TypeCounter extends HashMap<Class<?>, Integer> {
    private Class<?> baseType;
    public TypeCounter(Class<?> baseType) {
        this.baseType = baseType;
    }
    public void count(Object obj) {
        Class<?> type = obj.getClass();
        // isAssignableFrom()在运行时验证传递的对象实际上在不在我们希望的层次结构里。
        if(!baseType.isAssignableFrom(type))
            throw new RuntimeException(
                    obj + " incorrect type: " + type +
                            ", should be type or subtype of " + baseType);
        countClass(type);
    }
    //countClass()首先对这个确切的类型进行计数。然后,如果其基类可以赋值给baseType,则对基类进行递归调用countClass()。
    private void countClass(Class<?> type) {
        Integer quantity = get(type);
        put(type, quantity == null ? 1 : quantity + 1);
        Class<?> superClass = type.getSuperclass();
        if(superClass != null &&
                baseType.isAssignableFrom(superClass)) {
            countClass(superClass);
        }
    }
    @Override
    public String toString() {
        String result = entrySet().stream()
                .map(pair -> String.format("%s=%s",
                        pair.getKey().getSimpleName(),
                        pair.getValue()))
                .collect(Collectors.joining(", "));
        return "{" + result + "}";
    }
}

count()方法获取其参数的Class,并使用isAssignableFrom()在运行时验证传递的对象实际上在不在我们希望的层次结构里。countClass()首先对这个确切的类型进行计数。然后,如果其基类可以赋值给baseType,则对基类进行递归调用countClass()

package reflection;

import onjava.TypeCounter;
import reflection.pets.Pet;
import reflection.pets.PetCreator;

/**
 * @author: Caldarius
 * @date: 2023/2/13
 * @description:
 */
public class PetCounter4 {
    public static void main(String[] args) {
        TypeCounter counter = new TypeCounter(Pet.class);
        new PetCreator().stream()
                .limit(20)
                .peek(counter::count)
                .forEach(p -> System.out.print(
                        p.getClass().getSimpleName() + " "));
        System.out.println("\n" + counter);
    }
}
/*
Output:
Rat Hamster EgyptianMau Rat Rat Mutt Mutt EgyptianMau Rat Rat Cymric EgyptianMau Rat Mutt Rat Mutt Hamster Rat Mouse Cymric 
{Mutt=4, Rat=8, Manx=2, Dog=4, Mouse=1, EgyptianMau=3, Hamster=2, Cat=5, Rodent=11, Pet=20, Cymric=2}
*/

19.4 注册工厂

通过Pet层次结构来生成对象存在一个问题,即每次向层次结构中添加新类型的Pet时,都必须记住将其添加到PetCreator.java的列表里。在一个要经常添加类的系统中,这可能会成为问题。

你可能会考虑为每个子类添加一个静态初始化器,这样初始化程序就可以将它的类添加到某个列表中。遗憾的是,静态初始化器只在类第一次加载时调用,所以你就碰上了一个“先有鸡还是先有蛋”的问题:生成器在它的列表中没有这个类,它永远不能创建这个类的对象,所以类不会被加载并放置在列表中。

基本上,你必须自己手动创建这个列表(除非你编写一个工具来搜索并分析源代码,然后创建和编译这个列表)。所以最佳的做法就是把这个列表放在一个靠近中心的、位置明显的地方。我们感兴趣的这个层次结构的基类可能就是最好的地方。

我们要做的另一处变更是使用工厂方法(Factory Method)设计模式来推迟对象的创建,将其交给类自己去完成。工厂方法可以被多态地调用,来创建恰当类型的对象。实际上,java.util.function.Supplier通过它的T get()方法提供了一个工厂方法的原型。get()方法可以通过协变返回类型为Supplier的不同子类返回对应的类型。

在此示例中,基类Part包含了一个工厂对象(Supplier<Part>)的静态List。对于本应该由get()方法生成的类型,它们的工厂类都被添加到了列表prototypes里,从而“注册”到了基类中。比较特别的一点是,这些工厂是对象本身的实例。这个列表中的每个对象都是用于创建其他对象的原型

package reflection;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Supplier;
import java.util.stream.Stream;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
class Part implements Supplier<Part> {
    @Override
    public String toString() {
        return getClass().getSimpleName();
    }
    static List<Supplier<? extends Part>> prototypes =
            Arrays.asList(
                    new FuelFilter(),
                    new AirFilter(),
                    new CabinAirFilter(),
                    new OilFilter(),
                    new FanBelt(),
                    new PowerSteeringBelt(),
                    new GeneratorBelt()
            );
    private static Random rand = new Random(47);
    @Override public Part get() {
        int n = rand.nextInt(prototypes.size());
        return prototypes.get(n).get();
    }
}
class Filter extends Part {}

class FuelFilter extends Filter {
    @Override
    public FuelFilter get() { return new FuelFilter(); }
}

class AirFilter extends Filter {
    @Override
    public AirFilter get() { return new AirFilter(); }
}

class CabinAirFilter extends Filter {
    @Override
    public CabinAirFilter get() {
        return new CabinAirFilter();
    }
}

class OilFilter extends Filter {
    @Override
    public OilFilter get() { return new OilFilter(); }
}

class Belt extends Part {}

class FanBelt extends Belt {
    @Override
    public FanBelt get() { return new FanBelt(); }
}

class GeneratorBelt extends Belt {
    @Override public GeneratorBelt get() {
        return new GeneratorBelt();
    }
}

class PowerSteeringBelt extends Belt {
    @Override public PowerSteeringBelt get() {
        return new PowerSteeringBelt();
    }
}
public class RegisteredFactories{
    public static void main(String[] args) {
        Stream.generate(new Part())
                .limit(10)
                .forEach(System.out::println);
    }
}
/*
Output:
GeneratorBelt
CabinAirFilter
GeneratorBelt
AirFilter
PowerSteeringBelt
CabinAirFilter
FuelFilter
PowerSteeringBelt
PowerSteeringBelt
FuelFilter
*/

并不是层次结构中的所有类都应该被实例化。以上示例中的FilterBelt只是分类器,所以你不应该创建它们的实例,而只需要创建它们子类的实例(如果你尝试创建,只会得到基类Part的行为)。

Part实现了Supplier<Part>,所以它可以通过自己的get()提供其他的Part对象。如果调用了基类Partget()方法(或者通过generate()调用get()),它会随机创建特定的Part子类型,每个子类型最终都继承自Part,并重写了get()方法来生成自身的对象。

19.5 InstanceofClass的等价性

当查询类型信息时,instanceofisInstance()的效果是一样的,而它们与Class对象的直接比较有着重要的区别。下面这个示例演示了它们的不同之处:

package reflection;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
class Base {}
class Derived extends Base {}

public class FamilyVsExactType {
    static void test(Object x) {
        System.out.println(
                "Testing x of type " + x.getClass());
        System.out.println(
                "x instanceof Base " + (x instanceof Base));
        System.out.println(
                "x instanceof Derived " + (x instanceof Derived));
        System.out.println(
                "Base.isInstance(x) " + Base.class.isInstance(x));
        System.out.println(
                "Derived.isInstance(x) " +
                        Derived.class.isInstance(x));
        System.out.println(
                "x.getClass() == Base.class " +
                        (x.getClass() == Base.class));
        System.out.println(
                "x.getClass() == Derived.class " +
                        (x.getClass() == Derived.class));
        System.out.println(
                "x.getClass().equals(Base.class)) "+
                        (x.getClass().equals(Base.class)));
        System.out.println(
                "x.getClass().equals(Derived.class)) " +
                        (x.getClass().equals(Derived.class)));
    }
    public static void main(String[] args) {
        test(new Base());
        test(new Derived());
    }
}
/*
Output:
Testing x of type class reflection.Base
x instanceof Base true
x instanceof Derived false
Base.isInstance(x) true
Derived.isInstance(x) false
x.getClass() == Base.class true
x.getClass() == Derived.class false
x.getClass().equals(Base.class)) true
x.getClass().equals(Derived.class)) false
Testing x of type class reflection.Derived
x instanceof Base true
x instanceof Derived true
Base.isInstance(x) true
Derived.isInstance(x) true
x.getClass() == Base.class false
x.getClass() == Derived.class true
x.getClass().equals(Base.class)) false
x.getClass().equals(Derived.class)) true
*/

test()方法使用两种形式的instanceof来对其参数进行类型检查。然后获取Class引用,并使用==equals()来测试Class对象的相等性。令人欣慰的是,instanceofisInstance()产生了完全相同的结果,而equals()==也一样。但从两组测试本身,我们可以得出不同的结论。instanceof与类型的概念保持了一致,它相当于表示“你是这个类,还是这个类的子类”。另一方面,如果你使用==比较实际的Class对象,则不需要考虑继承——它要么是确切的类型,要么不是。

19.6 运行时的类信息

如果不知道某个对象的确切类型,instanceof可以告诉你。但是,这里有一个限制:只有在编译时就知道的类型才能使用instanceof来检测,然后用获得的信息做一些有用的事情。换句话说,编译器必须知道你使用的所有类。

乍一看,这似乎并不是一个多大的限制,但假设你获取了一个不在你的程序空间的对象引用——事实上,在编译时你的程序甚至无法获知这个对象所属的类。也许你只是从磁盘文件或网络连接中获得了一堆字节,然后被告知这些字节代表一个类。这个类在编译器为你的程序生成代码之后很久才出现,那你怎么才能使用这样的类呢?

在传统的编程环境中不太可能会出现这种情况。但当我们进入一个更大的编程世界时,在一些重要场景下就会发生这种事情。首先就是基于组件的编程,在这种编程方式中,我们在构建应用程序的集成开发环境(IDE)中,通过快速应用程序开发(RAD) 模式来构建项目。这是一种可视化编程方法,它通过将代表不同组件的图标拖拽到表单中来创建程序,然后在程序里通过设置组件的属性值来配置它们。这种设计时的配置,要求组件都是可实例化的,并且要公开其部分信息,以允许程序员读取和修改组件的属性。此外,处理图形用户界面(GUI)事件的组件还必须公开相关方法的信息,以便IDE能够帮助程序员重写这些处理事件的方法。反射提供了一种检测可用方法并生成方法名称的机制。

在运行时获取类信息的另一个吸引人的动机就是,希望提供通过网络在远程平台上创建和运行对象的能力。这称为远程方法调用(RMI),它允许Java程序将对象分布到多台机器上。需要这种分布能力的原因有许多,例如,你可能有一个计算密集型的任务,为了提高运算速度,可以将其分解为多个部分,分布到空闲的机器上。或者你可能希望将处理特定类型任务(例如客户-服务器体系结构中的“业务规则”)的代码置于特定的机器上,这样一来,这台机器就成了描述这些操作的公共场所,可以通过对它进行简单的修改来影响系统中的所有人。分布式计算还支持擅长特定任务的专用硬件——例如矩阵求逆——而这对通用程序来说就显得不太合适或者过于昂贵。

Class类和java.lang.reflect库一起支持了反射,这个库里包含FieldMethod以及Constructor类(每个都实现了Member接口)。这些类型的对象是由JVM在运行时创建的,用来表示未知类中对应的成员。这样你就可以使用Constructor来创建新的对象,使用get()set()方法来读取和修改与Field对象关联的字段,使用invoke()方法调用与Method对象关联的方法。另外,你还可以很便捷地调用getFields()getMethods()getConstructors()等方法,以返回表示字段、方法和构造器的对象数组(你可以在JDK文档中查找Class类来了解更多信息)。这样,匿名对象的类信息可以在运行时才完全确定下来,而在编译时就不需要知道任何信息。

重要的是,要意识到反射机制并没有什么神奇之处。当使用反射与未知类型的对象打交道时,JVM会查看这个对象,确定它属于哪个特定的类。在用它做任何事情之前,必须先加载对应的Class对象。因此对于JVM来说,该特定类型的.class文件必须是可用的:要么在本地机器上,要么可以通过网络获得。通过反射,在编译时不可用的.class文件就可以在运行时被打开和检查了。

类方法提取器

通常来说,你不会直接用到反射工具,但它有助于创建更动态的代码。反射在Java中可以用来支持其他特性,比如对象序列化(请参阅进阶卷附录E)。而且有时候动态提取有关类的信息也是很有用的。

请考虑一个类方法提取器。如果我们查看一个类定义的源代码或其JDK文档,只能找到在这个类中被定义或被重写的方法。但对我们来说,可能还有更多继承自基类的可用方法。要找出这些方法既乏味又费时2。幸运的是,反射提供了一种方式,让我们能够编写简单的工具来自动展示完整的接口:

2在过去尤其是这样。不过现在Java的HTML文档有了重大改进,使得查看基类方法变得更加容易。

package reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.regex.Pattern;

/**
 * 使用反射来显示一个类的所有方法,
 * 即使这个方法是在基类中定义的
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 *
 */
//CLI argument: reflection.ShowMethods
public class ShowMethods {
    private static String usage =
            "usage:\n" +
                    "ShowMethods qualified.class.name\n" +
                    "To show all methods in class or:\n" +
                    "ShowMethods qualified.class.name word\n" +
                    "To search for methods involving 'word'";
    private static Pattern p = Pattern.compile("\\w+\\.");
    public static void main(String[] args) {
        if(args.length < 1) {
            System.out.println(usage);
            System.exit(0);
        }
        int lines = 0;
        try {
            Class<?> c = Class.forName(args[0]);
            Method[] methods = c.getMethods();
            Constructor[] ctors = c.getConstructors();
            if(args.length == 1) {
                for(Method method : methods)
                    System.out.println(
                            p.matcher(
                                    method.toString())
                                    .replaceAll(""));
                for(Constructor ctor : ctors)
                    System.out.println(
                            p.matcher(ctor.toString()).replaceAll(""));
                lines = methods.length + ctors.length;
            } else {
                for(Method method : methods)
                    if(method.toString().contains(args[1])) {
                        System.out.println(p.matcher(
                                method.toString()).replaceAll(""));
                        lines++;
                    }
                for(Constructor ctor : ctors)
                    if(ctor.toString().contains(args[1])) {
                        System.out.println(p.matcher(
                                ctor.toString()).replaceAll(""));
                        lines++;
                    }
            }
        } catch(ClassNotFoundException e) {
            System.out.println("No such class: " + e);
        }
    }
}
/*
Output:
public static void main(String[])
public final void wait(long,int) throws InterruptedException
public final void wait() throws InterruptedException
public final native void wait(long) throws InterruptedException
public boolean equals(Object)
public String toString()
public native int hashCode()
public final native Class getClass()
public final native void notify()
public final native void notifyAll()
public ShowMethods()

*/

Class类里的方法getMethods()getConstructors()分别返回了Method对象的数组和Constructor对象的数组。这两个类都提供了对应的方法,来进一步解析它们所代表的方法,并获取其名称、参数和返回值的相关信息。但你也可以像上面的示例那样,只使用toString()方法来生成一个含有完整的方法签名的字符串。其他部分的代码提取了命令行信息,判断某个特定的方法签名是否与我们的目标字符串相匹配(使用contains()),并使用正则表达式去掉了名称限定符(在第18章中介绍过)。

Class.forName()生成的结果在编译时是未知的,因此所有的方法签名信息都是在运行时提取的。如果研究一下JDK文档中关于反射的部分,你就会发现,反射提供了足够的支持,来创建一个在编译时完全未知的对象,并调用此对象的方法(本书后面有这样的例子)。虽然一开始你可能认为自己永远不会用到这些功能,但是反射的价值可能会令你惊讶。上面的输出是从下面的命令行产生的:

java ShowMethods ShowMethods

输出里包含了一个public的无参构造器,即使代码中没有定义任何构造器。你看到的这个构造器是由编译器自动合成的。如果将ShowMethods设为非public类(即包访问权限),那么这个自动合成的无参构造器就不会在输出中显示了。合成的无参构造器会自动获得与类相同的访问权限。

你可以尝试运行带有charintString等额外参数的java ShowMethods java.lang.String

在编写程序时,如果你不记得一个类是否有某个特定的方法,并且也不想在JDK文档中查找索引或类层次结构,或者你不知道这个类是否可以对某个对象(比如Color对象)做些什么,那么这个工具可以替你节省很多时间。

19.7 动态代理

代理(proxy)是基本的设计模式之一。它是为了代替“实际”对象而插入的一个对象,从而提供额外的或不同的操作。这些操作通常涉及与“实际”对象的通信,因此代理通常充当中间人的角色。下面是一个用来展示代理结构的简单示例:

package reflection;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
//代理对象和真实对象均实现的接口
interface Interface {
    void doSomething();
    void somethingElse(String arg);
}
//真实对象
class RealObject implements Interface {
    @Override
    public void doSomething() {
        System.out.println("doSomething");
    }
    @Override
    public void somethingElse(String arg) {
        System.out.println("somethingElse " + arg);
    }
}
//代理对象
class SimpleProxy implements Interface {
    //传入的真实对象
    private Interface proxied;
    //完成真实对象的初始化
    SimpleProxy(Interface proxied) {
        this.proxied = proxied;
    }
    @Override
    public void doSomething() {
        //代理对象完成的额外操作
        System.out.println("SimpleProxy doSomething");
        //真实对象完成的工作
        proxied.doSomething();
    }
    @Override
    public void somethingElse(String arg) {
        System.out.println(
                "SimpleProxy somethingElse " + arg);
        proxied.somethingElse(arg);
    }
}

class SimpleProxyDemo {
    public static void consumer(Interface iface) {
        iface.doSomething();
        iface.somethingElse("bonobo");
    }
    public static void main(String[] args) {
        consumer(new RealObject());
        System.out.println("-------这里划分真实和代理-------");
        consumer(new SimpleProxy(new RealObject()));
    }
}
/*
Output:
doSomething
somethingElse bonobo
SimpleProxy doSomething
doSomething
SimpleProxy somethingElse bonobo
somethingElse bonobo
*/

consumer()方法接受一个Interface参数,所以它不知道自己得到的是一个RealObject还是一个SimpleProxy,两者都实现了Interface接口。SimpleProxy被插入到客户端和RealObject之间来执行操作,然后调用RealObject的相同方法。

在任何时候,如果你想要将额外的操作从“实际”对象中分离出来,特别是当你没有使用这些额外操作,但希望很轻松地就能改成使用,或反过来,这时代理就很有用了(设计模式的关注点就是封装修改——因此你需要做对应的修改来适应模式)。例如,如果你希望跟踪对RealObject中方法的调用,或者测量此类调用的开销,该怎么办?你肯定不希望在应用程序中包含这些代码,而代理可以让你很容易地添加或删除它们。

Java的动态代理(dynamic proxy)比代理更进一步,它可以动态地创建代理,并动态地处理对所代理方法的调用。在动态代理上进行的所有调用都会被重定向到一个调用处理器(invocation handler)上,这个调用处理器的工作就是发现这是什么调用,然后决定如何处理它。下面是用动态代理重写的SimpIeProxyDemo.java:

package reflection;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;


/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */

//动态调用处理器,实现了Java自身的调用处理器接口
class DynamicProxyHandler implements InvocationHandler {
    private Object proxied;
    DynamicProxyHandler(Object proxied) {
        this.proxied = proxied;
    }
    @Override
    //每个被代理的方法都会到这里来,决定自己应该干什么。后两个参数可以构成方法签名。
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        System.out.println(
                "**** proxy: " + proxy.getClass() +
                        ", method: " + method + ", args: " + args);
        if(args != null)
            for(Object arg : args)
                System.out.println("  " + arg);
        return method.invoke(proxied, args);
    }
}

class SimpleDynamicProxy {
    public static void consumer(Interface iface) {
        iface.doSomething();
        iface.somethingElse("bonobo");
    }
    public static void main(String[] args) {
        RealObject real = new RealObject();
        consumer(real);
        // 插入一个代理
        //我们通过调用静态方法Proxy.newProxyInstance()来创建动态代理,它需要三个参数:一个类加载器(通常可以从一个已经加载的对象里获取其类加载器,然后传递给它就可以了),一个希望代理实现的接口列表(不是类或抽象类),以及InvocationHandler接口的一个实现。动态代理会将所有调用重定向到调用处理器,因此调用处理器的构造器通常会获得“实际”对象的引用,以便它在执行完自己的中间任务后可以转发请求。
        Interface proxy = (Interface) Proxy.newProxyInstance(
                Interface.class.getClassLoader(),
                new Class[]{ Interface.class },
                new DynamicProxyHandler(real));
        //再次调用
        consumer(proxy);
    }
}
/*
Output:
doSomething
somethingElse bonobo
**** proxy: class reflection.$Proxy0, method: public abstract void reflection.Interface.doSomething(), args: null
doSomething
**** proxy: class reflection.$Proxy0, method: public abstract void reflection.Interface.somethingElse(java.lang.String), args: [Ljava.lang.Object;@1f17ae12
  bonobo
somethingElse bonobo
*/

我们通过调用静态方法Proxy.newProxyInstance()来创建动态代理,它需要三个参数:一个类加载器(通常可以从一个已经加载的对象里获取其类加载器,然后传递给它就可以了),一个希望代理实现的接口列表(不是类或抽象类),以及InvocationHandler接口的一个实现。动态代理会将所有调用重定向到调用处理器,因此调用处理器的构造器通常会获得“实际”对象的引用,以便它在执行完自己的中间任务后可以转发请求。

代理对象传递给了invoke()方法来处理,以防你需要区分请求的来源,但是在许多情况下,你并不关心这一点。不过,在invoke()内部调用代理的方法时需要小心,因为对接口的调用是通过代理进行重定向的。

通常,你会执行被代理的操作,然后使用Method.invoke()方法将请求转发给被代理的对象,并传入必要的参数。乍一看这可能有些受限,就好像你只能执行通用的操作一样。但是,你可以过滤某些方法调用,同时又放行其他的方法调用:

package reflection;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
class MethodSelector implements InvocationHandler {
    private Object proxied;
    MethodSelector(Object proxied) {
        this.proxied = proxied;
    }
    @Override public Object
    invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        if(method.getName().equals("interesting"))
            System.out.println(
                    "Proxy detected the interesting method");
        return method.invoke(proxied, args);
    }
}

interface SomeMethods {
    void boring1();
    void boring2();
    void interesting(String arg);
    void boring3();
}

class Implementation implements SomeMethods {
    @Override public void boring1() {
        System.out.println("boring1");
    }
    @Override public void boring2() {
        System.out.println("boring2");
    }
    @Override public void interesting(String arg) {
        System.out.println("interesting " + arg);
    }
    @Override public void boring3() {
        System.out.println("boring3");
    }
}

class SelectingMethods {
    public static void main(String[] args) {
        SomeMethods proxy =
                (SomeMethods) Proxy.newProxyInstance(
                        SomeMethods.class.getClassLoader(),
                        new Class[]{ SomeMethods.class },
                        new MethodSelector(new Implementation()));
        proxy.boring1();
        proxy.boring2();
        proxy.interesting("bonobo");
        proxy.boring3();
    }
}
/*
Output:
boring1
boring2
Proxy detected the interesting method
interesting bonobo
boring3
*/

在这里,我们只是查看了方法名称,但你还可以查看方法签名的其他方面,甚至可以搜索特定的参数值。

动态代理并不是日常使用的工具,但它可以很好地解决某些类型的问题。在Erich Gamma等人撰写的《设计模式:可复用面向对象软件的基础》一书和本书的进阶卷第8章中,你可以了解更多有关代理和其他设计模式的信息。

19.8 使用Optional

当使用内置的null来表示对象不存在时,为了确保安全,你必须在每次使用对象的引用时都测试一下它是否为null。这会变得很乏味,并产生冗长的代码。**问题在于null没有自己的行为,而当你尝试用它做任何事情时,都会产生一个NullPointerException。**我们在第13章中介绍过java.util.Optional,它创建了一个简单的代理来屏蔽潜在的null值。Optional对象会阻止你的代码直接抛出NullPointerException

尽管Optional是在Java 8中引入来支持Stream的,但它是一个通用工具,可以应用于普通类就证明了这一点。这个主题之所以包含在本章中,是因为涉及运行时检查。

在实际应用中,到处使用Optional是没有意义的——有时判断一下是否为null没什么不好,有时你可以合理地假设自己不会遇到null,有时甚至通过NullPointerException来检测异常也是可以接受的。Optional看起来在“更接近数据”的地方最有用,此时对象代表问题空间中的实体。举个简单的例子,许多系统里有Person类,但在代码中,有些情况下你并没有获得这样一个实际的对象(或者你可能有,但还没有关于那个对象的所有信息),所以通常你会使用一个null引用来表示,然后对其进行检查。而现在我们就可以使用Optional来代替了:

package reflection;

import java.util.Optional;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
class Person {
    public final Optional<String> first;
    public final Optional<String> last;
    public final Optional<String> address;
    // 省略其余代码
    public final boolean empty;
    Person(String first, String last, String address) {
        this.first = Optional.ofNullable(first);
        this.last = Optional.ofNullable(last);
        this.address = Optional.ofNullable(address);
        empty = !this.first.isPresent()
                && !this.last.isPresent()
                && !this.address.isPresent();
    }
    Person(String first, String last) {
        this(first, last, null);
    }
    Person(String last) { this(null, last, null); }
    Person() { this(null, null, null); }
    @Override public String toString() {
        if(empty)
            return "<Empty>";
        return (first.orElse("") +
                " " + last.orElse("") +
                " " + address.orElse("")).trim();
    }
    public static void main(String[] args) {
        System.out.println(new Person());
        System.out.println(new Person("Smith"));
        System.out.println(new Person("Bob", "Smith"));
        System.out.println(new Person("Bob", "Smith",
                "11 Degree Lane, Frostbite Falls, MN"));
    }
}
/*
Output:
<Empty>
Smith
Bob Smith
Bob Smith 11 Degree Lane, Frostbite Falls, MN
*/

Person的设计有时被称为“数据传输对象”。注意,所有的字段都是publicfinal的,因此没有getter和setter方法。也就是说,Person不可变的——你只能用构造器设置值,然后读取这些值,但你不能修改它们(字符串本身是不可变的,所以你不能修改字符串的内容,也不能给字段重新赋值)。要更改Person,你只能将其替换为新的Person对象。empty字段在构造期间赋值,以便轻松地检查这个Person是否代表一个空对象。

任何使用Person的人在访问这些字符串字段时都会被强制使用Optional接口,因此不会意外触发NullPointerException

现在假设你已经为自己的惊人创意获得了大量风险投资,并准备好了要招聘人员。但在职位空缺时,你可以用Optional来为PositionPerson字段提供占位符:

package reflection;

import java.util.Optional;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
class EmptyTitleException extends RuntimeException {}

class Position {
    private String title;
    private Person person;
    Position(String jobTitle, Person employee) {
        setTitle(jobTitle);
        setPerson(employee);
    }
    Position(String jobTitle) {
        this(jobTitle, null);
    }
    public String getTitle() { return title; }
    public void setTitle(String newTitle) {
        // 如果newTitle是null,则抛出EmptyTitleException:
        title = Optional.ofNullable(newTitle)
                .orElseThrow(EmptyTitleException::new);
    }
    public Person getPerson() { return person; }
    public void setPerson(Person newPerson) {
        // 如果newPerson是null,则使用一个空的Person:
        person = Optional.ofNullable(newPerson)
                .orElse(new Person());
    }
    @Override
    public String toString() {
        return "Position: " + title +
                ", Employee: " + person;
    }
    public static void main(String[] args) {
        System.out.println(new Position("CEO"));
        System.out.println(new Position("Programmer",
                new Person("Arthur", "Fonzarelli")));
        try {
            new Position(null);
        } catch(Exception e) {
            System.out.println("caught " + e);
        }
    }
}
/*
Output:
Position: CEO, Employee: <Empty>
Position: Programmer, Employee: Arthur Fonzarelli
caught reflection.EmptyTitleException
*/

这个示例以不同的方式来使用Optional。注意,titleperson都是普通字段,不受Optional的保护。但是,修改这些字段唯一的方法是通过setTitle()setPerson(),而这两者都使用了Optional的功能来对字段加以限制。

我们想要保证title永远不会被设置为null。在setTitle()方法中,我们可以自己检查newTitle参数。但是函数式编程的很大一部分就是能够重用经过尝试和验证的功能,即便这些功能通常很小,这样可以减少手动编写代码时犯的各种小错误。所以我们用ofNullable()newTitle转换成Optional,这意味着如果newTitlenull,它将生成一个Optional.empty()。然后立即获取该Optional结果,并调用它的orElseThrow()方法,此时如果newTitlenull,将得到一个异常。我们并没有将该字段存储为Optional,但使用了Optional的功能来对title字段施加想要的约束。

EmptyTitleException是一个RuntimeException,因为它代表了一个程序员错误。在这个方案里你仍然得到了一个异常,但你是在错误发生的时候得到它的——也就是当null被传递给setTitle()时——而不是在程序中的其他地方,如果在其他地方的话你就不得不对程序进行调试才能发现问题所在。此外,EmptyTitleException的使用有助于进一步定位错误。

person字段具有不同的约束:如果尝试将其设置为null,它会自动设置为一个空的Person对象。我们使用与之前相同的方法将其转换为Option,但在这个例子中,当提取结果时,我们使用了orElse(new Person())null替换成空的Person来插入。

对于Position,我们不需要创建“空”的标记或方法,因为如果person字段的值是一个空的Person对象,这就意味着这个Position还是处于空缺状态。稍后,你可能会发现必须在此处添加一些明确的内容,但是根据YAGNI3(You Aren't Going to Need It,你并不需要它)原则,在初稿中只“尝试最简单且可行的事情”,直到程序的某些方面要求你添加额外的功能,而不是一开始就假设它是必要的。

3****极限编程(Extreme Programming, XP)的一项宗旨就是“尝试最简单且可行的事情”。

注意Staff类轻松地忽略了Optional的存在,尽管你知道它们在那里,保护你免受NullPointerException的影响:

package reflection;

import java.util.ArrayList;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
public class Staff extends ArrayList<Position> {
    public void add(String title, Person person) {
        add(new Position(title, person));
    }
    public void add(String... titles) {
        for(String title : titles)
            add(new Position(title));
    }
    public Staff(String... titles) { add(titles); }
    public boolean positionAvailable(String title) {
        for(Position position : this)
            if(position.getTitle().equals(title) &&
                    position.getPerson().empty)
                return true;
        return false;
    }
    public void fillPosition(String title, Person hire) {
        for(Position position : this)
            if(position.getTitle().equals(title) &&
                    position.getPerson().empty) {
                position.setPerson(hire);
                return;
            }
        throw new RuntimeException(
                "Position " + title + " not available");
    }
    public static void main(String[] args) {
        Staff staff = new Staff("President", "CTO",
                "Marketing Manager", "Product Manager",
                "Project Lead", "Software Engineer",
                "Software Engineer", "Software Engineer",
                "Software Engineer", "Test Engineer",
                "Technical Writer");
        staff.fillPosition("President",
                new Person("Me", "Last", "The Top, Lonely At"));
        staff.fillPosition("Project Lead",
                new Person("Janet", "Planner", "The Burbs"));
        if(staff.positionAvailable("Software Engineer"))
            staff.fillPosition("Software Engineer",
                    new Person(
                            "Bob", "Coder", "Bright Light City"));
        System.out.println(staff);
    }
}
/*
Output:
[Position: President, Employee: Me Last The Top, Lonely At, Position: CTO, Employee: <Empty>, Position: Marketing Manager, Employee: <Empty>, Position: Product Manager, Employee: <Empty>, Position: Project Lead, Employee: Janet Planner The Burbs, Position: Software Engineer, Employee: Bob Coder Bright Light City, Position: Software Engineer, Employee: <Empty>, Position: Software Engineer, Employee: <Empty>, Position: Software Engineer, Employee: <Empty>, Position: Test Engineer, Employee: <Empty>, Position: Technical Writer, Employee: <Empty>]
*/

在某些地方可能仍然需要检查Optional,这与检查null没有什么不同,但在其他地方(例如本例中的toString()转换)不需要进行额外的检查,可以直接假设所有的对象引用都是有效的。

19.8.1 标签接口

有时使用标签接口(tagging interface)来表示可空性更方便。标签接口没有元素,我们只是将它的名称当作标签来使用:

package onjava;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
public interface Null { }

如果你使用的是接口而不是具体类,那么就可以使用DynamicProxy来自动生成Null。假设有一个Robot接口,它定义了名称、模型以及一个描述了自身功能的List<Operation>

package reflection;

import onjava.Null;

import java.util.*;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
public interface Robot {
    String name();
    String model();
    List<Operation> operations();
    static void test(Robot r) {
        if (r instanceof Null)
            System.out.println("[Null Robot]");
        System.out.println("Robot name: " + r.name());
        System.out.println("Robot model: " + r.model());
        for (Operation operation : r.operations()) {
            System.out.println(operation.description.get());
            operation.command.run();
        }
    }
}

可以通过调用operations()来访问Robot的服务。Robot还包含了一个静态方法来执行测试。

Operation包含一个描述和一个命令[这是一种命令模式(Command pattern)]。它们被定义为对函数式接口的引用,这样你就可以将lambda表达式或方法引用传递给Operation的构造器:

package reflection;

import java.util.function.Supplier;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
public class Operation {
    public final Supplier<String> description;
    public final Runnable command;
    public
    Operation(Supplier<String> descr, Runnable cmd) {
        description = descr;
        command = cmd;
    }
}

现在可以创建一个扫雪的Robot

package reflection;

import java.util.Arrays;
import java.util.List;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
public class SnowRobot implements Robot {
    private String name;
    public SnowRobot(String name) {
        this.name = name;
    }
    @Override
    public String name() { return name; }
    @Override
    public String model() {
        return "SnowBot Series 11";
    }
    private List<Operation> ops = Arrays.asList(
            new Operation(
                    () -> name + " can shovel snow",
                    () -> System.out.println(
                            name + " shoveling snow")),
            new Operation(
                    () -> name + " can chip ice",
                    () -> System.out.println(name + " chipping ice")),
            new Operation(
                    () -> name + " can clear the roof",
                    () -> System.out.println(
                            name + " clearing roof")));
    @Override
    public List<Operation> operations() { return ops; }
    public static void main(String[] args) {
        Robot.test(new SnowRobot("Slusher"));
    }
}
/*
Output:
Robot name: Slusher
Robot model: SnowBot Series 11
Slusher can shovel snow
Slusher shoveling snow
Slusher can chip ice
Slusher chipping ice
Slusher can clear the roof
Slusher clearing roof

*/

可能会有许多不同类型的Robot,而且对于每种Robot类型,如果为Null,则做一些特殊操作——本例中会提供Robot的确切类型信息。此信息由动态代理捕获:

package reflection;

import onjava.Null;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
class NullRobotProxyHandler implements InvocationHandler {
    private String nullName;
    //在这里对代理对象进行了初始化
    private Robot proxied = new NRobot();
    NullRobotProxyHandler(Class<? extends Robot> type) {
        nullName = type.getSimpleName() + " NullRobot";
    }
    private class NRobot implements Null, Robot {
        @Override
        public String name() { return nullName; }
        @Override
        public String model() { return nullName; }
        @Override public List<Operation> operations() {
            return Collections.emptyList();
        }
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        //在创建类的时候需要完成的操作已经完成了,所以代理这里也不需要做什么了,直接返回就可以
        return method.invoke(proxied, args);
    }
}

public class NullRobot {
    public static Robot
    newNullRobot(Class<? extends Robot> type) {
        return (Robot) Proxy.newProxyInstance(
                NullRobot.class.getClassLoader(),
                //希望实现的接口是new方法,希望返回一个满足Null以及Robot的类
                new Class[]{ Null.class, Robot.class },
                new NullRobotProxyHandler(type));
    }
    public static void main(String[] args) {
        Stream.of(
                new SnowRobot("SnowBee"),
                newNullRobot(SnowRobot.class)
        ).forEach(Robot::test);
    }
}
/*
Output:
Robot name: SnowBee
Robot model: SnowBot Series 11
SnowBee can shovel snow
SnowBee shoveling snow
SnowBee can chip ice
SnowBee chipping ice
SnowBee can clear the roof
SnowBee clearing roof
[Null Robot]
Robot name: SnowRobot NullRobot
Robot model: SnowRobot NullRobot

*/

每当需要一个空的Robot对象时,调用newNullRobot()即可,传递给它想要的Robot类型,它会返回一个代理。代理会同时满足RobotNull接口的要求,并提供它所代理的类型的特定名称。

19.8.2 模拟对象和桩

模拟对象(Mock Object)和(Stub)是Optional的逻辑变体。这两个都是在最终的程序中使用的“实际”对象的代理。模拟对象和桩都假装是提供真实信息的实际对象,而不会像Optional那样隐藏对象,甚至包括null对象。

模拟对象和桩之间的区别在于程度的不同。模拟对象往往是轻量级和自测试的,通常我们创建很多模拟对象是为了处理各种不同的测试情况。桩只返回桩数据,它通常是重量级的,并且经常在测试之间重用。桩可以根据它们的调用方式,通过配置进行更改。所以桩是一个复杂的对象,它只做一件事情。如果你需要做很多事情,通常会创建很多小而简单的模拟对象。

19.9 接口和类型信息

interface关键字的一个重要目标是允许程序员隔离组件,从而减少耦合。如果只和接口通信,那么就可以实现这一目标,但是通过类型信息可能会绕过它——接口并不一定保证解耦。假设我们从一个接口开始:

package reflection.interfacea;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
public interface A {
    void f();
}

下面的示例显示了如何偷偷访问实际的实现类型:

package reflection;

import reflection.interfacea.A;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
class B implements A {
    @Override
    public void f() {}

    public void g() {}
}

public class InterfaceViolation {
    public static void main(String[] args) {
        A a = new B();
        a.f();
        // a.g(); // 编译错误
        System.out.println(a.getClass().getName());
        if(a instanceof B) {
            B b = (B)a;
            b.g();
        }
    }
}
//Output:reflection.B

通过反射,可以发现a实际上是被当作B实现的。通过强制转型为B,我们可以调用不在A中的方法。

这是完全合法并且可接受的,但你可能不希望客户程序员这样做,因为这给了他们一个机会,让他们的代码与你的代码耦合程度超出你的期望。也就是说,你可能认为interface关键字正在保护着你,但事实并非如此,而且本例中使用B来实现A这一事实,实际上是公开可见的4

4最著名的案例是Windows操作系统,它有一个已发布API供你调用,还有一组未发布但可见的函数,可以让你发现并调用。为了解决问题,程序员使用了隐藏的API函数,这迫使微软公司将它们作为公共API的一部分进行维护。这成了使微软公司投入巨额成本和大量精力的无底洞。

一种解决方案是直接声明,如果程序员决定使用实际的类而不是接口,他们就得自己承担后果。在许多情况下这可能是合理的,但如果事实并非如此,你就需要实施更严格的控制。

最简单的方法是使用包访问权限来实现,这样包外的客户就看不到它了:

package reflection.packageaccess;

import reflection.interfacea.A;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
class C implements A {
    @Override public void f() {
        System.out.println("public C.f()");
    }
    public void g() {
        System.out.println("public C.g()");
    }
    void u() {
        System.out.println("package C.u()");
    }
    protected void v() {
        System.out.println("protected C.v()");
    }
    private void w() {
        System.out.println("private C.w()");
    }
}

public class HiddenC {
    public static A makeA() { return new C(); }
}

HiddenC是这个包唯一的public部分,调用它时会生成一个A接口。即使makeA()返回了一个C类型,在包外仍然不能使用除A外的任何事物,因为你不能在包外命名C

现在,如果尝试向下转型为C,你会发现无法做到,因为包外没有可用的C类型:

package reflection;

import reflection.interfacea.A;
import reflection.packageaccess.HiddenC;

import java.lang.reflect.Method;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
public class HiddenImplementation {
    public static void
    main(String[] args) throws Exception {
        A a = HiddenC.makeA();
        a.f();
        System.out.println(a.getClass().getName());
        // 编译错误:无法找到符号'C':
    /* if(a instanceof C) {
      C c = (C)a;
      c.g();
    } */
        // 呀!反射仍然允许我们调用g():
        callHiddenMethod(a, "g");
        // 甚至访问权限更小的方法:
        callHiddenMethod(a, "u");
        callHiddenMethod(a, "v");
        callHiddenMethod(a, "w");
    }
    static void
    callHiddenMethod(Object a, String methodName)
            throws Exception {
        Method g =
                a.getClass().getDeclaredMethod(methodName);
        g.setAccessible(true);
        g.invoke(a);
    }
}
/*
Output:public C.f()
reflection.packageaccess.C
public C.g()
package C.u()
protected C.v()
private C.w()
*/

你仍然可以使用反射来访问并调用所有的方法,甚至包括private的方法!如果你知道方法的名称,就可以通过调用Method对象的setAccessible(true)来设置,从而让这个方法可以被调用,就像callHiddenMethod()中所示的那样。

你可能认为通过仅发布已编译的代码可以防止这种情况,但这不是解决方案。只需要运行JDK自带的反编译器javap就能绕过它。

因此,任何人都可以获取你最私有的方法的名称和签名,并调用它们。

如果将接口实现为私有内部类会怎样?下面的示例展示了这种情况:

package reflection;

import reflection.interfacea.A;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
class InnerA {
    private static class C implements A {
        @Override public void f() {
            System.out.println("public C.f()");
        }
        public void g() {
            System.out.println("public C.g()");
        }
        void u() {
            System.out.println("package C.u()");
        }
        protected void v() {
            System.out.println("protected C.v()");
        }
        private void w() {
            System.out.println("private C.w()");
        }
    }
    public static A makeA() { return new C(); }
}

public class InnerImplementation {
    public static void
    main(String[] args) throws Exception {
        A a = InnerA.makeA();
        a.f();
        System.out.println(a.getClass().getName());
        // 反射仍然能访问私有类内部:
        HiddenImplementation.callHiddenMethod(a, "g");
        HiddenImplementation.callHiddenMethod(a, "u");
        HiddenImplementation.callHiddenMethod(a, "v");
        HiddenImplementation.callHiddenMethod(a, "w");
    }
}
/*
Output:
public C.f()
reflection.InnerA$C
public C.g()
package C.u()
protected C.v()
private C.w()*/

这里对反射仍然没有隐藏任何东西。那么匿名类呢?

package reflection;

import reflection.interfacea.A;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
class AnonymousA {
    public static A makeA() {
        return new A() {
            @Override public void f() {
                System.out.println("public C.f()");
            }
            public void g() {
                System.out.println("public C.g()");
            }
            void u() {
                System.out.println("package C.u()");
            }
            protected void v() {
                System.out.println("protected C.v()");
            }
            private void w() {
                System.out.println("private C.w()");
            }
        };
    }
}

public class AnonymousImplementation {
    public static void
    main(String[] args) throws Exception {
        A a = AnonymousA.makeA();
        a.f();
        System.out.println(a.getClass().getName());
        // 反射仍然能访问匿名类内部:
        HiddenImplementation.callHiddenMethod(a, "g");
        HiddenImplementation.callHiddenMethod(a, "u");
        HiddenImplementation.callHiddenMethod(a, "v");
        HiddenImplementation.callHiddenMethod(a, "w");
    }
}
/*
Output:
public C.f()
reflection.AnonymousA$1
public C.g()
package C.u()
protected C.v()
private C.w()
*/

看来没有任何方法可以阻止反射进入并调用非公共访问权限的方法。对于字段,甚至是private的字段,也是如此:

package reflection;

import java.lang.reflect.Field;

/**
 * @author: Caldarius
 * @date: 2023/2/14
 * @description:
 */
class WithPrivateFinalField {
    private int i = 1;
    private final String s = "I'm totally safe";
    private String s2 = "Am I safe?";
    @Override public String toString() {
        return "i = " + i + ", " + s + ", " + s2;
    }
}

public class ModifyingPrivateFields {
    public static void
    main(String[] args) throws Exception {
        WithPrivateFinalField pf =
                new WithPrivateFinalField();
        System.out.println(pf);
        Field f = pf.getClass().getDeclaredField("i");
        f.setAccessible(true);
        System.out.println(
                "f.getInt(pf): " + f.getInt(pf));
        f.setInt(pf, 47);
        System.out.println(pf);
        f = pf.getClass().getDeclaredField("s");
        f.setAccessible(true);
        System.out.println("f.get(pf): " + f.get(pf));
        f.set(pf, "No, you're not!");
        System.out.println(pf);
        f = pf.getClass().getDeclaredField("s2");
        f.setAccessible(true);
        System.out.println("f.get(pf): " + f.get(pf));
        f.set(pf, "No, you're not!");
        System.out.println(pf);
    }
}
/*
Output:
i = 1, I'm totally safe, Am I safe?
f.getInt(pf): 1
i = 47, I'm totally safe, Am I safe?
f.get(pf): I'm totally safe
i = 47, I'm totally safe, Am I safe?
f.get(pf): Am I safe?
i = 47, I'm totally safe, No, you're not!
*/

不过,final字段实际上是安全的,不会发生变化。运行时系统在接受任何更改尝试时并不会报错,但实际上什么也不会发生。

一般来说,这些访问违规并不是世界上最糟糕的事情。如果有人使用这种技术来调用你标记为private或包访问权限的方法(即这些方法不应该被调用),那么当你更改这些方法的某些方面时,他们就不应该抱怨。此外,Java语言提供了一个后门来访问类,这一事实可以让你能够解决某些特定类型的问题。如果没有这个后门的话,这些问题会难以解决,甚至不可能解决。反射带来的好处通常很难否认。

程序员经常对语言提供的访问控制过于自信,以至于相信在安全性方面,Java比其他提供了(显然)不太严格的访问控制的语言更优越5。正如你所看到的,事实并非如此。

5例如,在Python中,你在要隐藏的元素前面放置一个双下划线__,如果尝试在类或包之外访问它,运行时系统就会报错。

19.10 总结

反射从匿名的基类引用中发现类型信息。初学者极易误用它,因为在学会使用多态方法调用之前,使用反射可能感觉很合理。对有过程化编程背景的人来说,很难不把程序组织成一系列的switch语句。你可以用反射实现这一点,但是这样的话,就在代码开发和维护过程中失去了多态的重要价值。面向对象编程语言的目的就是,在任何可能的地方都使用多态,而只在必要的时候使用反射。

但是,如果想按预期使用多态方法调用,就需要控制基类的定义,因为在扩展程序的时候,你可能会发现基类并未包含自己想要的方法。如果基类来自别人的库,一种解决方案就是反射:你可以继承一个新类,然后添加额外的方法。在代码的其他地方,你可以检查自己特定的类型,并调用这个特殊方法。这样做不会破坏程序的多态性以及可扩展性,因为只添加一个新类型的话,并不会让你在程序中到处查找switch语句来修改。但如果添加需要新功能的代码,就必须使用反射来检查你的特定类型。

将某个功能放在基类中可能意味着,为了某个特定类的利益,接口变得不那么合理。例如,考虑一个代表乐器的类层次结构。假设我们想清洁管弦乐队中某些乐器的排气阀。一个办法是在基类Instrument中放置一个clearSpitValve()方法,但这会造成混淆,因为它暗示PercussionStringedElectronic这些乐器也有排气阀。反射可以提供一个合理的解决方案,你可以将方法放在合适的特定类中(在本例中为Wind)。同时,你可能会发现一个更合理的解决方案,例如在基类中提供一个prepareInstrument()方法。但是,当你第一次解决问题时,可能看不到这样的解决方案,而错误地认为必须使用反射。

最后,反射有时能解决效率问题。假设你的代码使用了多态,但是其中某个对象运行这种通用代码的效率极低。你可以使用反射来选择该类型,然后为其编写特定场景的代码来提高效率。但是,请注意不要过早为提高效率而编程。这是一个诱人的陷阱。最好让程序运行起来,再考虑它是否运行得足够快,如果想要解决效率问题,则应该使用分析器(profiler)。

我们还看到,由于反射允许更加动态的编程风格,因此它开创了一个包含各种可能性的编程新世界。对某些人来说,反射的这种动态特性令人不安。你可以执行一些操作,这些操作只能在运行时检查,并且用异常来报告检查结果,而对于已经习惯了静态类型检查安全性的人来说,这看起来好像是一个错误的方向。有些人甚至声称引入运行时异常本身就是一个明确的表示,这说明应该避免这种代码。我发现这种安全感是一种幻觉,因为总有一些事情可能在运行时发生并抛出异常,即使在一个不包含try块或异常说明的程序中也是如此。与之前那种意见相反,我认为一致的错误报告模型的存在,使我们能够通过反射编写动态代码。当然,尽力编写能够进行静态检查的代码是值得的,如果可以的话就应该这么做。但是我相信动态代码是将Java与C++等语言区分开来的重要工具之一。