Java类型信息详解

作者: 程序员七哥 | 来源:发表于2018-05-06 18:12 被阅读3次

    类型信息

    运行时类型信息使得你可以在程序运行时发现和使用类型信息

    本章节将讨论Java是如何让我们在运行时识别对象和类的信息的. 主要有两种方式,一种是"传统的" RTTI(Run-Time Type Identification),它假定我们在编译时已经知道了所有的类型;另一种是"反射机制",它允许我们在运行时发现和使用类的信息.

    Class对象

    1. 要理解RTTI在Java中的工作原理,首先必须知道类型信息在运行时是如何表示的.这项工作是由称为Class对象的特殊对象完成的.它包含了与类有关的信息. 事实上,Class对象就是用来创建类的所有"常规" 对象的.

    2. 类是程序的一部分,每个类都有一个Class对象.换言之,每当编写并且编译了一个新类,就会产生一个Class对象(更恰当的说,是被保存在一个同名的.class文件中.)

    3. 所有的类都是对其第一次使用时,动态加载到JVM中的. 当程序创建第一个对类的静态成员引用时,就会加载这个类.这个也证明构造器也是类的静态方法,即使在构造器之前并没有使用static关键字. 因此,使用new 操作符创建类的新对象也会被当作对类的静态成员的引用.

    这也证明了,Java程序在它开始运行之前并非完全被加载,其各个部分是在必需时才加载的. 类加载器首先检查这个类的Class对象是否已经加载.如果尚未加载,默认的类加载器就会根据类名查找.class文件(例如,某个附加类加载器可能会在数据库中查找字节码).在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良Java代码(这是Java中用于安全防范目的的措施之一). 一旦某个类的Class对象被载入内存,它就会用来创建这个类的所有对象.

    具体可以点击这里查看对应的Demo

    接下来介绍三种方法来获取Class对象的引用:

    1. Class.forName()
    2. Calss.getSuperClass()
    3. Class.class -- 类字面常量

    注意:有一点很有趣,当使用".class"来创建对Class对象的引用时,不会自动地初始化该Class对象. 为了使用类而做的准备工作实际包含三个步骤:

    • 加载, 这是类加载器执行的.该步骤将查找字节码(通常在classpath所指定的路径中查找,但这并非时必需的),并且从字节码中创建一个Class对象.
    • 链接. 在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用.
    • 初始化. 如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块.
      初始化被延迟到了对静态方法(构造器隐式地是静态的) 或者非常数静态域进行首次引用时才执行

    具体可以点击这里查看对应的Demo

    1. 初始化有效地实现了尽可能的"惰性".从所写的Demo中可以得出仅使用.class语法来获得对类的引用不会触发初始化.
    2. 如果一个static final 值是"编译期常量",那么这个值不需要对类进行初始化就可以被读取.
    3. 如果一个static域不是final的,那么对它在访问时,总是要求在它被读取之前,要先进行链接(为这个域分配存储空间)初始化(初始化存储空间);

    泛化的Class引用

    Class引用总是指向某个Class对象,它可以制造类的实例,并包含可作用于这些实例的所有方法代码.它还包含该类的静态成员,因此,Class引用表示的就是它所指向对象的确切类型,而该对象便是Class类的一个对象

    在Java SE5中,Class<?> 优于平凡的Class,即便它们时等价的,并且平凡的Class如你所见,不会产生编译器警告信息.Class<?>的好处就是它表示你并非时碰巧或者由于疏忽,而使用了一个非具体的类引用,你就是选择了非具体的版本.

    类型转换前先做检查

    迄今为止,我们已知的RTTI形式包括:

    1. 传统的类型转换,如"Shape",由RTTI确保类型转换的正确性,如果执行了一个错误的类型转换,就会抛出一个ClassCastException异常.
    2. 代表对象的类型的Class对象.通过查询Class对象可以获取运行时所需的信息.
    3. RTTI在Java中还有第三种形式,就是关键字instanceof.他返回一个布尔值,告诉我们对象是不是某个特定类型的实例.可以用提问的方式使用它,就像这样:
    if(x instanceof Dog)
      ((Dog) x).bark();
    

    反射:运行时的类信息

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

    重要的是,要认识到反射机制并没有什么神奇之处。当通过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类(就像RTTI那样)。在用它做其他事情之前必须先加载那个类的Class对象。因此,那个类的.class文件对于JVM来说必须是可获取的:要么在本地机器上,要么可以通过网络取得。所以RTTI和反射之间真正的区别只在于,对RTTI来说,编译器在编译时打开和检查.class文件。(换句话说,我们可以用“普通”方式调用对象的所有方法)。而对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开和检查.class文件。

    类方法提取器

    通常你不需要直接使用反射工具,但是它们在你需要创建更加动态的代码时会很有用.反射在Java中是用来支持其他特性的,例如对象序列化和JavaBean. 但是,如果能动态地提取某个类的信息有的时候还是很有用的.

    可以点击这里查看代码
    Class.forName()生成的结果在编译时时不可知的,因此所有的方法特征签名信息都是在执行时被提取出来的.如果研究一下JDK文档中关于反射的部分,就会看到,反射机制提供了足够的支持,使得能够创建一个在编译时完全未知的对象,并调用此对象的方法.

    动态代理

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

    package org.ccgogoing.java.typeinfo;
    
    /**
     * 描述:
     * 动态代理
     *
     * @outhor chong
     * @create 2018-05-02 22:54
     */
    public class SimpleProxyDemo {
    
        public static void consumer(Interface iface) {
            iface.doSomething();
            iface.somethingElse("bonobo");
        }
    
        public static void main(String[] args) {
            consumer( new RealObject());
            consumer(new SimpleProxy(new RealObject()));
        }
    }
    
    
    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;
        public 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);
        }
    }
    /* Output:
    doSomething
    somethingElse bonobo
    SimpleProxy doSomething
    doSomething
    SimpleProxy somethingElse bonobo
    somethingElse bonobo
    *///
    
    

    因为consumer()接受的Interface,所以它无法知道真正获得的到底时RealObject还是SimpleProxy,因为这二者都实现了Interface.但是SimpleProxy已经插入到了客户端和RealObject之间,因此它会执行操作,然后调用RealObject上相同的方法.

    Java的动态代理比代理的思想更向前迈进了一步,因为它可以动态地创建代理并动态地处理对所代理方法的调用. 在动态代理上所做的所有调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定相应的对策.

    查看用动态代理重写的SimpleProxyDemo.java; 请点击这里 查看

    从上述代码示例,可以看出通过调用Proxy.newProxyInstance()可以创建动态代理,这个方法需要得到一个类加载器(你通常可以从已加载的对象中获取其类加载器,然后传递给它),一个你希望该代理实现的接口列表(不是类或者抽象类),以及InvocationHandler接口的一个实现.动态代理可以将所有调用重定向到调用处理器,因此通常会向调用处理器的构造器传递给一个"实际"对象的引用,从而使得调用处理器在执行其中任务时,可以将请求转发.

    动态代理

    探究代理对象proxy

    最终的代理对象proxy到底长什么样子呢,是一个什么样的对象去实现对被代理对象方法的调用,下面我们写一个测试类,打印出这个proxy类,如下代理所示:

    package org.ccgogoing.java.typeinfo;
    
    import java.lang.reflect.*;
    
    public class ClassDefinitionPrintUtil {
    
    
        public static void main(String[] args) {
            RealObject real = new RealObject();
            Interface proxy = (Interface) Proxy.newProxyInstance(
                    Interface.class.getClassLoader(),
                    new Class[] { Interface.class }, new DynamicProxyHandler(real));
            System.out.println(proxy.getClass().getName());
            //打印出proxy类的结构
            printClassDefinition(proxy.getClass());
        }
    
    
        public static void printClassDefinition(Class clz) {
    
            StringBuilder clzModifier = new StringBuilder();
            int mod = clz.getModifiers() & Modifier.methodModifiers();
            if (mod != 0) {
                clzModifier.append(Modifier.toString(mod)).append(' ');
            }
            String superClz = clz.getSuperclass().getName();
            if (superClz != null && !superClz.equals("")) {
                superClz = "extends " + superClz;
            }
    
            Class[] interfaces = clz.getInterfaces();
    
            String inters = "";
            for (int i = 0; i < interfaces.length; i++) {
                if (i == 0) {
                    inters += "implements ";
                }
                inters += interfaces[i].getName();
            }
    
            System.out.println(clzModifier + clz.getName() + " " + superClz + " "
                    + inters);
            System.out.println("{");
    
            Field[] fields = clz.getDeclaredFields();
            for (int i = 0; i < fields.length; i++) {
                System.out.println("\t" + fields[i].toString() + ';');
            }
    
            System.out.println();
            Constructor[] constructors = clz.getDeclaredConstructors();
            for (int i = 0; i < constructors.length; i++) {
                System.out.println("\t" + constructors[i].toString() + ';');
            }
            System.out.println();
            Method[] methods = clz.getDeclaredMethods();
            for (int i = 0; i < methods.length; i++) {
                System.out.println("\t" + methods[i].toString() + ';');
            }
            System.out.println("}");
        }
    
    }
    

    输出如下:

    org.ccgogoing.java.typeinfo.$Proxy0
    final org.ccgogoing.java.typeinfo.$Proxy0 extends java.lang.reflect.Proxy implements org.ccgogoing.java.typeinfo.Interface
    {
        private static java.lang.reflect.Method org.ccgogoing.java.typeinfo.$Proxy0.m1;
        private static java.lang.reflect.Method org.ccgogoing.java.typeinfo.$Proxy0.m3;
        private static java.lang.reflect.Method org.ccgogoing.java.typeinfo.$Proxy0.m2;
        private static java.lang.reflect.Method org.ccgogoing.java.typeinfo.$Proxy0.m4;
        private static java.lang.reflect.Method org.ccgogoing.java.typeinfo.$Proxy0.m0;
    
        public org.ccgogoing.java.typeinfo.$Proxy0(java.lang.reflect.InvocationHandler);
    
        public final boolean org.ccgogoing.java.typeinfo.$Proxy0.equals(java.lang.Object);
        public final java.lang.String org.ccgogoing.java.typeinfo.$Proxy0.toString();
        public final int org.ccgogoing.java.typeinfo.$Proxy0.hashCode();
        public final void org.ccgogoing.java.typeinfo.$Proxy0.doSomething();
        public final void org.ccgogoing.java.typeinfo.$Proxy0.somethingElse(java.lang.String);
    }
    

    此时,我们就可以看到Proxy对象的类结构,那么很明显,Proxy.newProxyInstance(Interface.class.getClassLoader(),new Class[] { Interface.class }, new DynamicProxyHandler(real))方法会做如下几件事:

    1. 根据传入的第二个参数interfaces动态生成一个类,实现interfaces中的接口,该例中即Interface接口的 somethingElsedoSomething 方法。并且继承了Proxy类,重写了 hashcode,toString,equals等三个方法。具体实现可参看 ProxyGenerator.generateProxyClass(...); 该例中生成了$Proxy0类。

    2. 通过传入的第一个参数classloder将刚生成的类加载到jvm中。即将$Proxy0类load。

    3. 利用第三个参数,调用$Proxy0$Proxy0(InvocationHandler)构造函数 创建$Proxy0的对象,并且用interfaces参数遍历其所有接口的方法,并生成Method对象初始化对象的几个Method成员变量

    4. $Proxy0的实例返回给客户端。

    invoke()方法中传递进来了代理对象,以防你需要区分请求的来源,但是在许多情况下,你并不关心这一点.然而,在invoke()内部,在代理上调用方法时需要格外当心,因为对接口的调用将被重定向为对代理的调用.

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

    点击这里查看具体示例
    从示例代码中我们只查看了方法名,但是你还可以查看方法签名的其他方面,甚至可以搜索特定的参数值.

    至此,我们已经看到了,由于反射允许更加动态的编程风格,因此它开创了编程的新世界.

    本文所有示例代码均可在https://github.com/ccgogoing/java-study上面查看,之后我在学习过程中的一些记录也均会在上面发布.

    相关文章

      网友评论

        本文标题:Java类型信息详解

        本文链接:https://www.haomeiwen.com/subject/pxiilftx.html