运行时类型信息使得你可以在程序运行时发现和使用类型信息
Java是如何让我们在运行时识别对象和类的信息的。主要有两种方式:
- 一种使用传统的“RTTI”,它假定我们在编译时就已经知道了所有的类型信息。另
- 一种是“反射”机制,它允许我们在运行时发现和使用类型信息。
1.为什么需要使用RTTI
以使用了多态的类层次结构的例子举例:
如上图,泛型是基类Shape,而派生出来的具体类有Circle,Square和Triangle。
这是一个类层次结构图,基类位于顶部,而派生类向下拓展。
代码值操纵对基类的引用,如果添加一个新类来拓展程序也不会影响原来的代码(可拓展性),如图中Shape接口中动态绑定的draw()方法,目的就是使用泛化的Shape引用来调用draw()。draw()方法在所有派生类中都会被覆盖,由于是draw()方法是被动态绑定的,所以通过泛化的Shape引用来调用,也能产生正确的行为,即多态。
通常,我们会创建一个具体对象(本例中,Circle,Square或Triangle),然后把它向上转型为Shape(忽略对象的具体类型),并在后面的程序中使用匿名(即不知道具体类型)的Shape引用。
Shape shape = new Circle();
//Shape对象放入数组时会向上转型,转型为Shape且丢失了Shape对象的具体类型
//对数组而言,它们只是Shape类的对象
List<Shape> shapeList = Arrays.asList(new Circle(),new Square(),new Triangle());
//从数组中取出元素时,这种容器(实际上将所有事物都当作Object持有)会自动将结果转型为Shape。
for(Shape shape:shapeList){
shape.draw();
}
Java中,所有的类型转换都是在运行时进行正确性检查的,也就是RTTI(Runtime Type Information)名字的含义:在运行时,识别一个对象的类型。
通常,我们希望大部分代码尽可能地少了解对象的具体类型,而是只与对象家族中的一个通用表示打交道。这样,代码更容易写,更容易读,更便于维护,设计也更容易实现,理解和改变。所以,多态是面向对象编程的基本目标。
使用RTTI,可以查询某个Shape引用所指向的对象的确切类型,然后选择或者剔除特例。
2. Class对象
Class对象:包含了与类有关的信息,Class对象是用来创建类的所有的“常规”对象的。Java使用Class对象来执行其RTTI。
编写并编译一个新类——>产生一个Class对象(被保存在一个同名的.class文件中)——>为了生成这个类的对象,运行这个程序的JVM将使用被称为"类加载器"的子系统。
Java程序在它开始运行之前并非被完全加载,其各个部分是在必需时才加载的
Class对象和其他对象一样,我们可以获取并操作它的引用。
//这个方法是Class类(所有Class对象都属于这个类)的一个静态成员。
//是取得Class对象的引用的一种方法。用一个包含目标类的文本名的字符串作为输入参数,返回一个Class对象的引用。
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
Class中包含很多有用的方法,如下:
//全限定类名
getName();
//判断是否是接口
isInterface();
//获取不含包名的类名
getSimpleName();
//获取全限定的类名
getCanonicalName();
//获取直接基类
getSuperClass();
//获取包含的接口
getInterfaces();
//获取实例
newInstance();
2.1 类字面常量
Java除了forName()获取Class对象引用外,还可通过使用类字面常量
FancyToy.class;
//这样使用简单,安全,编译时会受到检查(不需要trycatch语句的使用)
类字面常量不仅可以应用于普通的类,更可以应用于接口,数组以及基本数据结构。(对于基本数据类型的包装器类,它有一个标准字段TYPE。TYPE字段是一个引用,指向对应的基本数据类型的Class对象)
//等价于,一般直接使用".class"的形式,以保持与普通类的一致性。
boolean.class ======Boolean.TYPE
void.class=========Void.TYPE
char.class=========Character.TYPE
使用".class"来创建对Class对象的引用时,不会自动初始化Class对象。
为了使用类而做的准备工作有:
- 加载(由类加载器执行,查找字节码并从中创建一个Class对象)
- 链接 (验证类中的字节码,为静态域分配存储空间)
- 初始化(若该类具有超类,则对其初始化,执行静态初始化器和静态初始化块)
2.2 泛化的Class引用
Class引用指向某个Class对象,可制造类的实例,并包含可作用于这些实例的所有方法代码,还包含该类的静态成员。
Java SE5允许对Class引用所指向的Class对象的类型进行限定(通过泛型语法)
Class intClass = int.class;
//通过泛型语法,可让编译器强制执行额外的类型检查
Class<Integer> genericIntClass = int.class;
//使用了通配符?(表示任何事物)
Class<?> intClass = int.class;
向Class引用添加泛型语法的原因仅仅是为了提供编译期类型检查。
2.3 新的转换语法
Java SE5 中还添加了用于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;
House h = houseType.cast(b);
h = (House)b; // ... or just do this.
}
}
cast()方法接受参数对象,并将其转型为Class引用的类型。
3. 类型转换前先做检查
3.1 RTTI的几种表现形式
- 传统的类型转换,如"(Shape)"。由RTTI确保类型转换的正确性,若执行了一个错误的类型转换,抛出ClassCastException异常。(Java中,此操作要执行类型检查,有向上转型和向下转型两种)
- 代表对象的类型的Class对象。通过查询Class对象可以获取运行时所需的信息。
- 关键字instanceof,它返回一个布尔值,告诉我们对象是不是某个特定类型的实例。
if(x instanceof Dog)
((Dog)x).bark();
3.2 动态的instanceof
Class.isInstance()方法提供了一种动态测试对象的途径。
4. 注册工厂
使用注册工厂的目的是将对象的创建工作交给类去完成,即创建一个工厂方法,然后进行多态的调用,从而为你创建恰当类型的对象。在如下的简单的版本中,工厂方法就是Factory接口中的Create方法。
public interface Factory<T>{T create();}
所谓工厂方法,就是意味着我只提供一个创建实例的工厂方法,而无需每创建一个继承类就编写一个新的方法。关于工厂方法更多细节后边在设计模式模块专门再进行学习。
5. instanceof与Class的等价性
Instanceof保持了类型的概念,指的是“你是这个类吗,或者你是这个类的派生类吗”
"=="则是比较的实际的Class对象,没有考虑继承(要么是这个确切的类型,要么不是)
6. 反射:运行时的类信息
反射概念是程序在编译的时候并不知道具体的类型信息,直到程序运行时通过反射才获取到了准确的类信息。这里提供了几个方法支持获取准确的类信息,以便创建动态的代码。使用Class类的getMethods()方法可以获取这个类所包含的所有方法,使用getConstructors()方法可以获取这个类的所有构造函数。前文也提到过,使用Class.forName()可以用来动态的加载类。它的生成结果在编译的时候是不可知的,因此所有的方法特征信息和签名都是在运行时被提取出来的。
6.1 类方法提取器
Class的getMethods和getConstructors方法分别返回Method对象的数组和Constructor对象的数组。这两个类都提供了深层的方法,用于解析其对象所代表的方法,并获取其名字,输入参数和返回值。
Clsss.forName()生成的结果在编译期间是不可知的,因此所有的方法特征签名信息都是在执行时被提取出来的。
7. 动态代理
代理是常用的设计模式之一,它的作用是在基本的对象操作之外,增加一些其它额外的操作。比如我想使用一个对象,同事想了解这个对象的运行过程,那么这个运行过程的监控显然就不能放在基本的对象代码中。有点类似前面文章中提到的组合类的思想,新建一个代理类,代理类引用要使用的对象,然后增加一些新的功能。刚好前几天跟公司一个同事面了一个新人,同事问道代理模式之后自己做了一些阐述,可以把代理模式类比成是中介,中介的目的是卖东西的基础上赚钱,卖东西就是基本操作,赚钱就是中介也就是代理做的额外的操作。下面一个示例展示简单的代理模式:
- 先定义一个接口:
public interface Interface {
void doSomeThing();
void somethingElse(String arg);
}
- 然后是这个接口的实现类,也就是前边所说的真正要操作的运行的对象:
public class RealObject implements Interface {
@Override
public void doSomeThing() {
// TODO Auto-generated method stub
System.out.println("RealObject DoSomeThing");
}
@Override
public void somethingElse(String arg) {
// TODO Auto-generated method stub
System.out.println("RealObject somethingElse");
}
}
- 然后定义一个代理类,代理类实现了Interface接口,同时通过传参的形式传入了前边的实现类,在完成实现类功能的基础上,做了自己的操作:
public class SimpleProxy implements Interface{
private Interface proxied;
public SimpleProxy(Interface proxied) {
// TODO Auto-generated constructor stub
this.proxied = proxied;
}
@Override
public void doSomeThing() {
// TODO Auto-generated method stub
System.out.println("Proxy DoSomething");
proxied.doSomeThing();
}
@Override
public void somethingElse(String arg) {
// TODO Auto-generated method stub
System.out.println("Proxy somethingElse");
proxied.somethingElse(arg);
}
}
- 最后是Main方法,因为consumer方法传参是Interface接口,所以任何实现了它接口的实体类都可以当做参数。这里演示了使用基本的实体类和使用代理的区别,代理在完成普通实体类的功能基础上打印了自己的操作内容:
public class SimpleProxyDemo {
public static void consumer(Interface iface){
iface.doSomeThing();
iface.somethingElse("bobo");
}
public static void main(String[] args) {
consumer(new RealObject());
System.out.println("==============");
consumer(new SimpleProxy(new RealObject()));
}
}
运行结果:
doSomething
somethingElse bonobo
SimpleProxy doSomething
doSomething
SimpleProxy somethingElse bonobo
somethingElse bonobo
8. Java动态代理
Java的动态代理比代理的思想更向前迈进了一步,因为它可以动态的创建代理,并且动态的处理对所代理方法的调用。动态代理所做的所有操作都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型,并确定相应的对策。下面用动态代理重写上边的示例:
Java中要实现动态代理类,必须要继承InvocationHandle这个类,这个类内部嵌入的对象是要被实现的真正的对象,同样使用构造方法传入,这个类唯一的一个方法invoke,它有三个参数,第一个参数是生成的动态代理类。这里我个人理解,既然动态代理是动态的创建代理,那么这个参数固然是所创建的动态代理,第二个参数是传入的对象执行的方法,第三个参数是传入的参数。最后将请求通过Method.invoke()方法,传入必要的参数,执行代理对象的方法。
动态代理Handler类:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class DynamicProxyHandler implements InvocationHandler{
private Object proxied;
public DynamicProxyHandler(Object proxied) {
// TODO Auto-generated constructor stub
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);
}
}
// TODO Auto-generated method stub
return method.invoke(proxied, args);
}
}
Main函数:
import java.lang.reflect.Proxy;
public class SimpleDynamicProxyDemo {
public static void consumer(Interface iface){
iface.doSomeThing();
iface.somethingElse("DIDI");
}
public static void main(String[] args) {
RealObject real = new RealObject();
consumer(real);
System.out.println("=========");
Interface proxy = (Interface)Proxy.newProxyInstance(Interface.class.getClassLoader(),new Class[]{Interface.class},new DynamicProxyHandler(real));
consumer(proxy);
}
}
运行结果:
doSomething
somethingElse bonobo
**** proxy: class com.basic.java.$Proxy0, method: public abstract void com.basic.java.Interface.doSomething(), args: null
doSomething
**** proxy: class com.basic.java.$Proxy0, method: public abstract void com.basic.java.Interface.somethingElse(java.lang.String), args: [Ljava.lang.Object;@4b67cf4d
bonobo
somethingElse bonobo
最近项目中用到了AOP做日志记录的功能,Spring的AOP核心思想就是动态代理,现在更加理解了一些,简单表述一下就是在切入点处执行一写其它操作,如我这里是记录日志,然后执行正常的业务方法。记录日志就是脱离在实际业务之外的一些操作。
8. 空对象
这里感觉不是很常用,大概理解了一下,就是当一个对象为null的时候,任何对这个对象的操作都会引发异常。为了避免这种情况,当一个对象为null的时候,我们定义一个空对象赋值给它。何谓空对象呢?就是一个不存在实际意义但是不会引发异常的对象。文中具体代码就不写了。