Java类型信息详解

类型信息

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

本章节将讨论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上面查看,之后我在学习过程中的一些记录也均会在上面发布.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容