类型信息
运行时类型信息使得你可以在程序运行时发现和使用类型信息
本章节将讨论Java是如何让我们在运行时识别对象和类的信息的. 主要有两种方式,一种是"传统的" RTTI(Run-Time Type Identification),它假定我们在编译时已经知道了所有的类型;另一种是"反射机制",它允许我们在运行时发现和使用类的信息.
Class对象
-
要理解RTTI在Java中的工作原理,首先必须知道类型信息在运行时是如何表示的.这项工作是由称为Class对象的特殊对象完成的.它包含了与类有关的信息. 事实上,Class对象就是用来创建类的所有"常规" 对象的.
-
类是程序的一部分,每个类都有一个Class对象.换言之,每当编写并且编译了一个新类,就会产生一个Class对象(更恰当的说,是被保存在一个同名的.class文件中.)
-
所有的类都是对其第一次使用时,动态加载到JVM中的. 当程序创建第一个对类的静态成员引用时,就会加载这个类.这个也证明构造器也是类的静态方法,即使在构造器之前并没有使用static关键字. 因此,使用
new
操作符创建类的新对象也会被当作对类的静态成员的引用.
这也证明了,Java程序在它开始运行之前并非完全被加载,其各个部分是在必需时才加载的. 类加载器首先检查这个类的Class对象是否已经加载.如果尚未加载,默认的类加载器就会根据类名查找.class文件(例如,某个附加类加载器可能会在数据库中查找字节码).在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良Java代码(这是Java中用于安全防范目的的措施之一). 一旦某个类的Class对象被载入内存,它就会用来创建这个类的所有对象.
具体可以点击这里查看对应的Demo
接下来介绍三种方法来获取Class对象的引用:
- Class.forName()
- Calss.getSuperClass()
- Class.class -- 类字面常量
注意:有一点很有趣,当使用".class"来创建对Class对象的引用时,不会自动地初始化该Class对象. 为了使用类而做的准备工作实际包含三个步骤:
- 加载, 这是类加载器执行的.该步骤将查找字节码(通常在classpath所指定的路径中查找,但这并非时必需的),并且从字节码中创建一个Class对象.
- 链接. 在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用.
- 初始化. 如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块.
初始化被延迟到了对静态方法(构造器隐式地是静态的) 或者非常数静态域进行首次引用时才执行
具体可以点击这里查看对应的Demo
- 初始化有效地实现了尽可能的"惰性".从所写的Demo中可以得出仅使用
.class
语法来获得对类的引用不会触发初始化.- 如果一个static final 值是"编译期常量",那么这个值不需要对类进行初始化就可以被读取.
- 如果一个static域不是final的,那么对它在访问时,总是要求在它被读取之前,要先进行链接(为这个域分配存储空间)和初始化(初始化存储空间);
泛化的Class引用
Class引用总是指向某个Class对象,它可以制造类的实例,并包含可作用于这些实例的所有方法代码.它还包含该类的静态成员,因此,Class引用表示的就是它所指向对象的确切类型,而该对象便是Class类的一个对象
在Java SE5中,Class<?> 优于平凡的Class,即便它们时等价的,并且平凡的Class如你所见,不会产生编译器警告信息.Class<?>的好处就是它表示你并非时碰巧或者由于疏忽,而使用了一个非具体的类引用,你就是选择了非具体的版本.
类型转换前先做检查
迄今为止,我们已知的RTTI形式包括:
- 传统的类型转换,如"Shape",由RTTI确保类型转换的正确性,如果执行了一个错误的类型转换,就会抛出一个ClassCastException异常.
- 代表对象的类型的Class对象.通过查询Class对象可以获取运行时所需的信息.
- RTTI在Java中还有第三种形式,就是关键字instanceof.他返回一个布尔值,告诉我们对象是不是某个特定类型的实例.可以用提问的方式使用它,就像这样:
if(x instanceof Dog)
((Dog) x).bark();
反射:运行时的类信息
Class类与java.lang.reflect
类库一起对反射的概念进行了支持,该类库包含了Field、Method以及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))
方法会做如下几件事:
-
根据传入的第二个参数
interfaces
动态生成一个类,实现interfaces
中的接口,该例中即Interface
接口的somethingElse
和doSomething
方法。并且继承了Proxy
类,重写了hashcode,toString,equals
等三个方法。具体实现可参看ProxyGenerator.generateProxyClass(...);
该例中生成了$Proxy0
类。 -
通过传入的第一个参数
classloder
将刚生成的类加载到jvm中。即将$Proxy0
类load。 -
利用第三个参数,调用
$Proxy0
的$Proxy0(InvocationHandler)
构造函数 创建$Proxy0
的对象,并且用interfaces
参数遍历其所有接口的方法,并生成Method
对象初始化对象的几个Method成员变量 -
将
$Proxy0
的实例返回给客户端。
invoke()方法中传递进来了代理对象,以防你需要区分请求的来源,但是在许多情况下,你并不关心这一点.然而,在invoke()内部,在代理上调用方法时需要格外当心,因为对接口的调用将被重定向为对代理的调用.
通常,你会执行被代理的操作,然后使用Method.invoke()将请求转发给被代理对象,并传入必须的参数.这初看起来可能有些受限,就像你只能执行泛化操作一样.但是,你可以通过传递其他的参数,来过滤某些方法调用:
点击这里查看具体示例
从示例代码中我们只查看了方法名,但是你还可以查看方法签名的其他方面,甚至可以搜索特定的参数值.
至此,我们已经看到了,由于反射允许更加动态的编程风格,因此它开创了编程的新世界.
本文所有示例代码均可在https://github.com/ccgogoing/java-study上面查看,之后我在学习过程中的一些记录也均会在上面发布.
网友评论