运行时类型信息使得你可以在程序运行时发现和使用类型信息。
本章将讨论Java 是如何让我们在运行时识别对象和类的信息的(如何在运行阶段,识别对象和类的信息)。主要有两种方式:一种是“传统的”RTTI,它假定我们在编译时已经知道了所有的类型,另一种是“反射”机制,它允许我们在运行时发现和使用类的信息。
RTTI:在Java中,所有的类型转换都是在运行时进行正确性检查的。这也是RTTI的含义:在运行时,识别一个对象的类型。
本章的主题围绕着”Java如何让我们在程序在运行的时候,识别出对象和类型信息“问题进行展开的。
14.1 为什么需要RTTI-- 多态中表现的类型转换是RTTI最基本的使用形式,但这种转换并不彻底。因为会涉及到具体类型(子类)信息丢失的问题。
1.1 代码证明具体类型丢失package net.mrliuli.rtti;
import java.util.Arrays;
import java.util.List;
abstract class Shape{
void draw(){
System.out.println(this + ".draw()");
}
abstract public String toString(); //要求子类需要实现 toString()
}
class Circle extends Shape{
@Override
public String toString() {
return "Circle";
}
public void drawCircle(){}
}
class Square extends Shape{
@Override
public String toString() {
return "Square";
}
}
class Triangle extends Shape{
@Override
public String toString() {
return "Triangle";
}
}
public class Shapes {
public static void main(String[] args){
List shapeList = Arrays.asList(
new Circle(), new Square(), new Triangle() // 向上转型为 Shape,此处会丢失原来的具体类型信息!!对于数组而言,它们只是Shape类对象!
);
for(Shape shape : shapeList){
shape.draw(); // 数组实际上将所有事物都当作Object持有,在取用时会自动将结果转型回声明类型即Shape。
}
//shapeList.get(0).drawCircle(); //这里会编译错误:在Shape类中找不到符号drawCircle(),证实了具体类型信息的丢失!!
}
}
在这个例子中,当把Shape对象放入List
在这个例子中,RTTI类型转换并不彻底:Object被转型为Shape,而不是转型为Cirele、Square或者Triangle。这是因为目前我们只知道这个Liste
接下来就是多态机制的事情了,Shape对象实际执行什么样的代码,是由引用所指向的具体对象Circle、Square或者Triangle而决定的。通常,也正是这样要求的,你希望大部分代码尽可能少地了解对象的具体类型,而是只与对象家族中的一个通用表示打交道(在这个例子中Shape)。这样代码会更容易写,更容易读,且更便于维护,设计也更容易实现、理解和改变。所以“多态”是面向对象编程的基本目标。
1.2 RTTI的三种形式(重要)- 传统的类型转换(多态),由RTTI确保类型转换的正确性,如果执行了错误的类型转换,会抛出一个ClassCastException的异常。
- 代表对象类型的Class对象,通过查询Class对象获取所需要的信息
- 类型转换前用instanceof检查
详细讲解请参考:
Think in java(五)RTTI的的三种形式、类型信息、class.forname与.class的区别___2018__的博客-CSDN博客
14.2 Class对象要理解RTTI在Java中的工作原理,首先必须知道类型信息在运行时是如何表示的。
1.1 RTTI在Java中的工作原理要能够在运行时识别具体类型,说明必然有东西在运行时保存了具体类型信息,这个东西就是Class对象,一种特殊对象。即Class对象表示了运行时的类型信息,它包含了与类有关的信息。只要你想在运行时使用类型信息,就必须首先获得对恰当的Class对象的引用。
- 事实上Class对象就是用来创建类的所有的“常规”对象的。
- 每个类(接口、抽象类)都有一个Class对象。换言之,每当编写并且编译了一个新类,就会产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。
- 也就是说,Class对象在.java文件编译成.class文件时就生成了,且就保存在这个.class文件中。
类是程序的一部分,每个类都有一个Class对象(包括接口、抽象类)。换言之,每当编写并且编译了一个新类,就会产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。为了生成这个类的对象,运行这个程序的Java虚拟机(JVM)将使用被称为“类加载器”的子系统(原生类加载器加载的是所调的可信类,包括lava API类,它们通常是从本地盘加载的)。
所有的类都是在对其第一次使用时,动态加载到JVM中的。Java程序在它开始运行之前并非被完全加载,其各个部分是在必需时才加载的。当程序创建第一个对类的静态成员的引用时,就会加载这个类。这个证明构造器也是类的静态方法,即使在构造器之前并没有使用static关键字。
1.2.1 类加载器工作流程类加载流程:加载、验证、准备、解析、初始化(静态方法、静态代码块)。
- 类加载器首先检查这个类的Class对象是否已经加载。
- 如果尚未加载,默认的类加载器就会根据类名查找.class文件(例如,某个附加类加载器可能会在数据库中查找字节码)。
- 在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良Java代码(这是Java中用于安全防范目的的措施之一)。
- 通过Class.forName(),就是一个便捷途径,这种方式不需要为了获得Class对象引用而持有该类型的对象。(即没有创建过或没有这个类型的对象的时候就可以获得Class对象引用。)
- 如果已经有一个类型的对象,那就可以通过调用这个对象的getClass()方法来获取它的Class对象引用了。这个方法属于Object,返回表示该对象的实际类型的Class对象引用。
- 使用类字面常量.class是获取Class对象引用的另一种方法。如 FancyToy.class。建议使用这种方法。
以下程序展示Class包含的很多有用的方法:
- getName() 获取类的全限定名称
- getSimpleName() 获取不含包名的类名
- getCanonicalName() 获取全限定的类名
- isInterface() 判断某个Class对象是否是接口
- getInterfaces() 返回Class对象实现的接口数组
- getSuperClass() 返回Class对象的直接基类
- newInstance() 创建一个这个Class对象所代表的类的一个实例对象。
Class引用在编译期不具备任何更进一步的类型信息,所以它返回的只是一个Object引用,但是这个Object引用指向的是这个Class引用所代表的具体类型。即需要转型到具体类型才能给它送Object以外的消息。
newInstance()这个方法依赖于Class对象所代表的类必须具有可访问的默认的构造函数(Nullary constructor,即无参的构造器),否则会抛出InstantiationException 或IllegalAccessException 异常
package net.mrliuli.rtti;
interface HasBatteries{}
interface Waterproof{}
interface Shoots{}
class Toy{
Toy(){}
Toy(int i){}
}
class FancyToy extends Toy implements HasBatteries, Waterproof, Shoots{
FancyToy(){ super(1); }
}
public class ToyTest {
static void printInfo(Class cc){
System.out.println("Class name: " + cc.getName() + " is interface? [" + cc.isInterface() + "]");
System.out.println("Simple name: " + cc.getSimpleName());
System.out.println("Canonical name: " + cc.getCanonicalName());
}
public static void main(String[] args){
Class c = null;
try{
c = Class.forName("net.mrliuli.rtti.FancyToy");
}catch (ClassNotFoundException e){
System.out.println("Can't find FancyToy");
System.exit(1);
}
printInfo(c);
System.out.println("=============================");
for(Class face : c.getInterfaces()){
printInfo(face);
}
System.out.println("=============================");
Class up = c.getSuperclass();
Object obj = null;
try{
// Requires default constructor:
obj = up.newInstance();
}catch (InstantiationException e){
System.out.println("Cannot instantiate");
System.exit(1);
}catch (IllegalAccessException e){
System.out.println("Cannot access");
System.exit(1);
}
printInfo(obj.getClass());
}
}
1.2.5 初始化惰性
初始化被延迟到了对静态方法(构造器隐式地是静态的)或者非常数静态域进行首次引用时才执行,即初始化有效地实现了尽可能 的“惰性”。
以下程序证实了上述观点。注意,将一个域设置为static 和 final的,不足以成为“编译期量”或“常数静态域”,如 static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);就不是编译期常量,对它的引用将强制进行类的初始化。
package net.mrliuli.rtti;
import java.util.Random;
class Initable{
static final int staticFinal = 47; // 常数静态域
static final int staticFinal2 = ClassInitialization.rand.nextInt(1000); // 非常数静态域(不是编译期常量)
static{
System.out.println("Initializing Initable");
}
}
class Initable2{
static int staticNonFinal = 147; // 非常数静态域
static {
System.out.println("Initializing Initable2");
}
}
class Initable3{
static int staticNonFinal = 74; // 非常数静态域
static {
System.out.println("Initializing Initable3");
}
}
public class ClassInitialization {
public static Random rand = new Random(47);
public static void main(String[] args) throws Exception {
Class initalbe = Initable.class; // 使用类字面常量.class获取Class对象引用,不会初始化
System.out.println("After creating Initable ref");
System.out.println(Initable.staticFinal); // 常数静态域首次引用,不会初始化
System.out.println(Initable.staticFinal2); // 非常数静态域首次引用,会初始化
System.out.println(Initable2.staticNonFinal); // 非常数静态域首次引用,会初始化
Class initable3 = Class.forName("net.mrliuli.rtti.Initable3"); // 使用Class.forName()获取Class对象引用,会初始化
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal); // 已初始化过
}
}
1.3 泛化的Class引用
1.3.1 Class对象类型限制
Class引用总是指向某个Class对象,此时,这个Class对象可以是各种类型的,当使用泛型语法对Class引用所指向的Class对象的类型进行限定时,这就使得Class对象的类型变得具体,这样编译器编译时也会做一些额外的类型检查工作。如
package net.mrliuli.rtti;
public class GenericClassReferences {
public static void main(String[] args){
Class intClass = int.class;
Class genericIntClass = int.class;
genericIntClass = Integer.class; // Same thing
intClass = double.class;
// genericIntClass = double.class; // Illegal, genericIntClass 限制为Integer 的Class对象
}
}
1.3.2 使用通配符?放松对Class对象类型的限制
通配符?是Java泛型的一部分,?表示“任何事物”。以下程序中Class> intClass = int.class; 与 Class intClass = int.class; 是等价的,但使用Class>优于使用Class,因为它说明了你是明确要使用一个非具体的类引用,才选择了一个非具体的版本,而不是由于你的疏忽。
package net.mrliuli.rtti;
public class WildcardClassReferences {
public static void main(String[] args){
Class> intClass = int.class;
intClass = double.class;
}
}
1.3.3 类型范围
将通配符与extends关键字相结合如Class extends Number>,就创建了一个范围,使得这个Class引用被限定为Number类型或其子类型。
package net.mrliuli.rtti;
public class BoundedClassReferences {
public static void main(String[] args){
Class extends Number> bounded = int.class;
bounded = double.class;
bounded = Number.class;
// Or anything derived from Number
}
}
泛型类语法示例:
package net.mrliuli.rtti;
import java.util.ArrayList;
import java.util.List;
class CountedInteger{
private static long counter;
private final long id = counter++;
public String toString(){
return Long.toString(id);
}
}
public class FilledList {
private Class type;
public FilledList(Class type){
this.type = type;
}
public List create(int nElements){
List result = new ArrayList();
try{
for(int i = 0; i < nElements; i++){
result.add(type.newInstance());
}
}catch(Exception e){
throw new RuntimeException(e);
}
return result;
}
public static void main(String[] args){
FilledList fl = new FilledList(CountedInteger.class); // 存储一个类引用
System.out.println(fl.create(15)); // 产生一个list
}
}
总结,使用泛型类后
- 使得编译期进行类型检查
- .newInstance()将返回确切类型的对象,而不是Object对象
RTTI有三种形式。
- 传统的类型转换,由RTTI确保类型转换的正确性,如果执行了错误的类型转换,会抛出一个ClassCastException的异常。
- 代表对象类型的Class对象,通过查询Class对象获取所需要的信息
- 类型转换前用instanceof检查
前面我们讲了前两种,现在我们开始讲解第三种instanceof的形式:
它返回一个布尔值,告诉我们对象是不是某个特定类型或其子类。如if(x instanceof Dog)语句会检查对象x是否从属于Dog类。
还一种形式是动态的instanceof:Class.isInstance()方法提供了一种动态地测试对象的途径。Class.isInstance()方法使我们不再需要instanceof表达式。
在查询类型信息时,以instanceof的形式(即以instanceof的形式或isInstance0的形式,它们产生相同的结果)与直接比较Class对象有一个很重要的差别。下面的例子展示了这种差别:
//: typeinfo/FamilyVsExactType.java
// The difference between instanceof and class
package typeinfo;
import static net.mindview.util.Print.*;
class Base {}
class Derived extends Base {}
public class FamilyVsExactType {
static void test(Object x) {
print("Testing x of type " + x.getClass());
print("x instanceof Base " + (x instanceof Base));
print("x instanceof Derived "+ (x instanceof Derived));
print("Base.isInstance(x) "+ Base.class.isInstance(x));
print("Derived.isInstance(x) " +
Derived.class.isInstance(x));
print("x.getClass() == Base.class " +
(x.getClass() == Base.class));
print("x.getClass() == Derived.class " +
(x.getClass() == Derived.class));
print("x.getClass().equals(Base.class)) "+
(x.getClass().equals(Base.class)));
print("x.getClass().equals(Derived.class)) " +
(x.getClass().equals(Derived.class)));
}
public static void main(String[] args) {
test(new Base());
test(new Derived());
}
}
//:~
- instanceof 和 isInstance() 保持了类型的概念,它指的是“你是这个类吗,或者你是这个类的派生类吗?”
- == 和 equals() 没有考虑继承——它要么是这个确切的类型,要么不是。
发射的概念:在程序运行时,动态的获取类的属性和方法。
Class类与jaya.lang.reflect类库一起对反射的概念进行了支持,该类库包含了Field、Method以及Constructor类(每个类都实现了Member接口)。这些类型的对象是由JVM在运行时创建的,用以表示未知类里对应的成员。这样你就可以使用Constructor创建新的对象,用get0和setO方法读取和修改与Field对象关联的字段,用invoke0)方法调用与Method对象关联的方法。另外,还可以调用getFields0、getMethods0和getConstructors0等很便利的方法,以返回表示字段、方法以及构造器的对象的数组(在JDK文档中,通过查找Class类可了解更多相关资料)。这样,匿名对象的类信息就能在运行时被完全确定下来,而在编译时不需要知道任何事情。
而对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开和检查.class文件。
1.1 类方法提取器反射机制提供了一种方法,使我们能够编写可以自动展示完整接口的简单工具。面就是其工作方式:
//: typeinfo/ShowMethods.java
// Using reflection to show all the methods of a class,
// even if the methods are defined in the base class.
// {Args: ShowMethods}
import java.lang.reflect.*;
import java.util.regex.*;
import static net.mindview.util.Print.*;
public class ShowMethods {
private static String usage =
"usage:n" +
"ShowMethods qualified.class.namen" +
"To show all methods in class or:n" +
"ShowMethods qualified.class.name wordn" +
"To search for methods involving 'word'";
private static Pattern p = Pattern.compile("\w+\.");
public static void main(String[] args) {
if(args.length < 1) {
print(usage);
System.exit(0);
}
int lines = 0;
try {
Class> c = Class.forName(args[0]);
Method[] methods = c.getMethods();
Constructor[] ctors = c.getConstructors();
if(args.length == 1) {
for(Method method : methods)
print(
p.matcher(method.toString()).replaceAll(""));
for(Constructor ctor : ctors)
print(p.matcher(ctor.toString()).replaceAll(""));
lines = methods.length + ctors.length;
} else {
for(Method method : methods)
if(method.toString().indexOf(args[1]) != -1) {
print(
p.matcher(method.toString()).replaceAll(""));
lines++;
}
for(Constructor ctor : ctors)
if(ctor.toString().indexOf(args[1]) != -1) {
print(p.matcher(
ctor.toString()).replaceAll(""));
lines++;
}
}
} catch(ClassNotFoundException e) {
print("No such class: " + e);
}
}
} //:~
14.7 动态代理
代理是基本的设计模式之一,它是你为了提供额外的或不同的操作,而插人的用来代替“实际”对象的对象。这些操作通常涉及与“实际”对象的通信,因此代理通常充当着中间人的角色。下面是一个用来展示代理结构的简单示例:
//: typeinfo/SimpleProxyDemo.java
import static net.mindview.util.Print.*;
interface Interface {
void doSomething();
void somethingElse(String arg);
}
class RealObject implements Interface {
public void doSomething() { print("doSomething"); }
public void somethingElse(String arg) {
print("somethingElse " + arg);
}
}
class SimpleProxy implements Interface {
private Interface proxied;
public SimpleProxy(Interface proxied) {
this.proxied = proxied;
}
public void doSomething() {
print("SimpleProxy doSomething");
proxied.doSomething();
}
public void somethingElse(String arg) {
print("SimpleProxy somethingElse " + arg);
proxied.somethingElse(arg);
}
}
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()));
}
} //:~
因为consumer(接受的Interface,所以它无法知道正在获得的到底是RealObject还是SimpleProxy,因为这二者都实现了Interface。但是SimpleProxy已经被插入到了客户端和RealObject之间,因此它会执行操作,然后调用RealObject上相同的方法。
在任何时刻,只要你想要将额外的操作从“实际〞对象中分离到不同的地方,特别是当你希望能够很容易地做出修改,从没有使用额外操作转为使用这些操作,或者反过来时,代理就显得很有用(设计模式的关键就是封装修改——因此你需要修改事务以证明这种模式的正确性)。例如,如果你希望跟踪对RealObject中的方法的调用,或者希望度最这些调用的开销,那么你应该怎样做呢?这些代码肯定是你不希望将其合并到应用中的代码,因此代理使得你可以很容易地添加或移除它们。
Java的动态代理比代理的思想更向前迈进了一步,因为它可以动态地创建代理并动态地处理对所代理方法的调用。在动态代理上所做的所有调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定相应的对策。下面是用动态代理重写的SimpleProxyDemojava:
//: typeinfo/SimpleDynamicProxy.java
import java.lang.reflect.*;
class DynamicProxyHandler implements InvocationHandler {
private Object proxied;
public DynamicProxyHandler(Object proxied) {
this.proxied = proxied;
}
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);
return method.invoke(proxied, args);
}
}
class SimpleDynamicProxy {
public static void consumer(Interface iface) {
iface.doSomething();
iface.somethingElse("bonobo");
}
public static void main(String[] args) {
RealObject real = new RealObject();
consumer(real);
// Insert a proxy and call again:
Interface proxy = (Interface)Proxy.newProxyInstance(
Interface.class.getClassLoader(),
new Class[]{ Interface.class },
new DynamicProxyHandler(real));
consumer(proxy);
}
} //:~
通过调用静态方法Proxy.newProxyInstanceO可以创建动态代理,这个方法需要得到一个类加载器(你通常可以从已经被加载的对象中获取其类加载器,然后传递给它),一个你希望该代理实现的接口列表(不是类或抽象类),以及InvocationHlandler接口的一个实现。动态代理可以将所有调用重定向到调用处理器,因此通常会向调用处理器的构造器传递给一个“实际”对象的引用,从而使得调用处理器在执行其中介任务时,可以将请求转发。
invoke0方法中传递进来了代理对象,以防你需要区分请求的来源,但是在许多情况下,你并不关心这一点。然而,在invokeO内部,在代理上调用方法时需要格外当心,因为对接口的调用将被重定向为对代理的调用。
通常,你会执行被代理的操作,然后使用Method.invokeO将请求转发给被代理对象,并传人必需的参数。这初看起来可能有些受限,就像你只能执行泛化操作一样。但是,你可以通过传递其他的参数,来过滤某些方法调用:
//: typeinfo/SelectingMethods.java
// Looking for particular methods in a dynamic proxy.
import java.lang.reflect.*;
import static net.mindview.util.Print.*;
class MethodSelector implements InvocationHandler {
private Object proxied;
public MethodSelector(Object proxied) {
this.proxied = proxied;
}
public Object
invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if(method.getName().equals("interesting"))
print("Proxy detected the interesting method");
return method.invoke(proxied, args);
}
}
interface SomeMethods {
void boring1();
void boring2();
void interesting(String arg);
void boring3();
}
class Implementation implements SomeMethods {
public void boring1() { print("boring1"); }
public void boring2() { print("boring2"); }
public void interesting(String arg) {
print("interesting " + arg);
}
public void boring3() { print("boring3"); }
}
class SelectingMethods {
public static void main(String[] args) {
SomeMethods proxy= (SomeMethods)Proxy.newProxyInstance(
SomeMethods.class.getClassLoader(),
new Class[]{ SomeMethods.class },
new MethodSelector(new Implementation()));
proxy.boring1();
proxy.boring2();
proxy.interesting("bonobo");
proxy.boring3();
}
} //:~
这里,我们只查看了方法名,但是你还可以查看方法签名的其他方面,甚至可以搜索特定的参数值。
动态代理并非是你日常使用的工具,但是它可以非常好地解决某些类型的问题。你在《Thinking in Patterns》(查看www.MindView.net)和Erich Gamma等人撰写的《DesignPatterns》°这两本书中了解到有关代理和其他设计模式的更多知识。



