Java编程思想 第十四章:类型信息

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

Java是如何让我们在运行时识别对象和类的信息的。主要有两种方式:

  1. 一种使用传统的“RTTI”,它假定我们在编译时就已经知道了所有的类型信息。另
  2. 一种是“反射”机制,它允许我们在运行时发现和使用类型信息。

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对象。

为了使用类而做的准备工作有:

  1. 加载(由类加载器执行,查找字节码并从中创建一个Class对象)
  2. 链接 (验证类中的字节码,为静态域分配存储空间)
  3. 初始化(若该类具有超类,则对其初始化,执行静态初始化器和静态初始化块)

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的几种表现形式

  1. 传统的类型转换,如"(Shape)"。由RTTI确保类型转换的正确性,若执行了一个错误的类型转换,抛出ClassCastException异常。(Java中,此操作要执行类型检查,有向上转型和向下转型两种)
  2. 代表对象的类型的Class对象。通过查询Class对象可以获取运行时所需的信息。
  3. 关键字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. 动态代理

代理是常用的设计模式之一,它的作用是在基本的对象操作之外,增加一些其它额外的操作。比如我想使用一个对象,同事想了解这个对象的运行过程,那么这个运行过程的监控显然就不能放在基本的对象代码中。有点类似前面文章中提到的组合类的思想,新建一个代理类,代理类引用要使用的对象,然后增加一些新的功能。刚好前几天跟公司一个同事面了一个新人,同事问道代理模式之后自己做了一些阐述,可以把代理模式类比成是中介,中介的目的是卖东西的基础上赚钱,卖东西就是基本操作,赚钱就是中介也就是代理做的额外的操作。下面一个示例展示简单的代理模式:

  1. 先定义一个接口:
public interface Interface {
	void doSomeThing();
	void somethingElse(String arg);
}
  1. 然后是这个接口的实现类,也就是前边所说的真正要操作的运行的对象:

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");
	}
	
}
  1. 然后定义一个代理类,代理类实现了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);
	}
	
}
  1. 最后是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的时候,我们定义一个空对象赋值给它。何谓空对象呢?就是一个不存在实际意义但是不会引发异常的对象。文中具体代码就不写了。

猜你喜欢

转载自blog.csdn.net/qq_21125183/article/details/85038537
今日推荐