Java编程思想拾遗(12)反射

345 阅读7分钟

运行时类型信息使得你可以在程序运行时发现和使用类型信息,它使你从只能在编译期执行面向类型的操作的禁锢中解脱了出来。

Java让我们在运行时识别对象和类的信息的两种方式:一种是传统的RTTI,它假定我们在那编译时已经知道了所有的类型;另一种是反射机制,它允许我们在运行时发现和使用类的信息。

通常你希望大部分代码尽可能少地了解对象的具体类型,而是只与对象家族中的一个通用表示打交道,这样代码更容易写,更容易读,且更便于维护,所以多态是面向对象编程的基本目标。但是假如你碰到一个特殊的编程问题--如果能够知道某个泛化引用的确切类型,就可以使用更简单的方式去解决它。(RTTI涵盖多态,也支持对多态进行补充以提供灵活的应用,这个概念是作者从C++延续过来的,读者可以不用太纠结词法,理解其设计初衷、实现原理、使用场景即可)

RTTI形式包括:

  • 传统的类型转换,如Shape,由RTTI确保类型转换的正确性,如果执行了一个错误的类型转换,就会抛出一个ClassCastException异常。
  • 代表对象的类型的Class对象,通过查询Class对象可以获取运行时所需的信息。
  • instanceof关键字,判断对象是不是某个特定类型的实例

Class对象

Class对象包含了与类有关的信息,是用来创建类的所有的”常规“对象的,每当编写并编译了一个新类,就会产生一个Class对象,由类加载器对其进行支持(JVM相关知识后面有机会我会跟朋友们分享一下)。

类加载

所有的类都是在对其第一次使用时,动态加载到JVM中的,当程序创建第一个对类的静态成员的引用时,就会加载这个类,这个证明构造器也是类的静态结构,即使在构造器之前并没有使用static关键字。类加载器首先检查这个类的Class对象是否已经加载,如果尚未加载,会根据类名查找.class文件,然后加载字节码并创建一个CLass对象,一旦某个类的Class对象被载入内存,他就被用来创建这个类的所有对象。

类对象引用

无论何时,只要你想在运行时使用类型信息,就必须首先获得对恰当的Class对象的引用,Class.forName()就是实现此功能的便捷途径,因为你不需要为了获得Class引用而持有该类型的对象,当然有了也可以通过继承Object的getClass()方法获取Class引用。

Class的newInstacne()方法是实现虚拟构造器的一种途径,虚拟构造器允许你声明“我不知道你的确切类型,但是无论如何要正确地创建你自己”,因为使用的只是一个Class引用,编译期也不具备任何更进一步的类型信息,当你创建新实例时,会得到Object引用,在你可以发送Object能够接受之外的任何消息之前,你必须更多地了解它,并执行某种转型。另外newInstance()要求这个类必须带有默认构造器。

Class up = c.getSuperClass();
Object obj = up.newInstance():

Java还提供了类字面常量来生成对Class对象的引用

FancyToy.class

这样做不仅更简单,而且更安全,因为它在编译时就会受到检查,并且它根除了对forName()方法的调用,不会自动地初始化该Class对象,初始化被延迟到了对静态方法(构造器隐式是静态的)或者非常数静态域进行首次引用时才执行,所以也更高效。类字面常量不仅可以应用于普通的类,也可以应用于接口、数组以及基本数据类型。

泛化的Class引用

Java允许你对Class引用所指向的Class对象的类型进行限定,通过实用泛型语法,可以让编译器强制执行额外的类型检查。

public class GenericClassReferences {
    public static void main(String[] args) {
        Class intClass = int.class;
        CLass<Integer> genericIntClass = int.class
        genericIntClass = Integer.class
        intClass = double.class;
        // genericIntClass = double.class // Illegal
    }
}

为了在使用泛化的Class引用时放松限制,可以使用通配符?表示“任何事物”。

public class WildcardClassReferences {
    public static void main(String[] args) {
        Class<?> intClass = int.class;
        intClass = double.class;
    }
}

Class优于平凡的Class,即便它们是等价的,平凡的Class不会产生编译器告警信息,Class的好处是它表示你并非是碰巧或者由于疏忽,而使用了一个非具体的类引用,你就是选择了非具体的版本(是显式使用倾向)。

类型检查

instanceof

在类型向下转型前,可以通过类型检查提供转型的安全度与精确度。

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

isInstance

对instanceof有比较严的限制:只可将其与命名类型进行比较,而不能与Class对象作比较。Class.isInstance方法提供了一种动态地测试对象的途径。

public void count(Pet pet) {
    for (Map.Entry<Class<? extends Pet>, Integer> pair : entrySet()) {
        if (pair.getKey().isInstance(pet)) {
            put(pair.getKey(), pair.getValue() + 1);
        }
    } 
}

isAssignableFrom

Class.isAssignableFrom()允许对有继承关系的类型上进行类型判断。

public void count(Object obj) {
    Clss<?> type = obj.getClass();
    if (!baseType.isAssignableFrom(type)) {
        throw new RuntimeException();
    }
    countClass(type):
}

private void count(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):
    }
}

反射

RTTI有一个限制:在编译时编译器必须知道所有要通过RTTI来处理的类。如果获取了一个指向某个并不在你程序空间中的对象的引用,在编译时你的程序根本没法获知这个对象所属的类,比如磁盘文件和远程网络的字节流。

Class类与java.lang.reflect类库一起对反射的概念进行了支持,该类库包含了Field、Method以及Constructor类(每个类都实现了Member接口)。这些类型的对象是由JVM在运行时创建的,用以表示未知类里对应的成员,这样匿名对象的类信息就能在运行时被完全确定下来,而在编译时不需要知道任何事情。

public clss ShowMethods {
    public static void main(String[] args) {
        // ignore arguments check and exception catch
        Class<?> c = Class.forName(args[0]);
        Method[] methods = c.getMethods();
        Constructor[] ctors = c.getConstructors();
        // extract method and constructor info
    }
}

Class.forName()生成的结果在编译时是不可知的,因此所有的方法特征签名信息都是在执行时被提取出来的,反射机制提供了足够的支持,使得能够创建一个在编译时完全未知的对象,并调用此对象的方法。

通过使用反射,仍旧可以到达并调用所有方法,甚至是private方法,如果知道方法名,你就可以在其Method对象上调用serAccessible(true)。对域来说也是一样不过final除外,final域实际上在遭遇修改时死安全的,运行时系统会在不抛异常的情况下接受任何修改尝试,但是实际上不会发生任何修改。(实际应用场景有动态配置项目,比如apollo)

RTTI和反射之间真正的区别在于,对RTTI来说,编译器在编译时打开和检查.class文件,而对于反射机制来说.class文件在编译时是不可获取的,所以是在运行时打开和检查.class文件。

动态代理

在任何时刻,只要你想将额外的操作从实际对象中分离到不同的地方,特别是当你希望能够很容易地做出修改,从没有使用额外操作转为使用这些操作,或者反过来时,代理就显得很有用。Java的动态代理比代理的思想更向前迈进了一步,因为它可以动态地创建代理并动态地处理对所代理方法的调用,在动态代理商所做的所有调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定相应的对策。

class DynamicProxyHandler implements InvocationHandler {
    private Object proxied;
    public DynamicProxyHandler(Object proxied) {
        this.proxied = proxied;
    }
    
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("proxy: " + proxy.getClass() + ", method: " + method + ", args: " + args);
        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);
        Interface proxy = (Interface)Proxy.newProxyInstance(Interface.class.getClassLoader(), 
            new Class[]{Interface.class},
            new DynamicProxyHandler(real));
        consumer(proxy);
    }
}