前段时间在看《Thinking in java》,由于之前一直都在写业务代码,包括交易、对账、银行利息理财等等,忽略了对底层支撑代码的研究,每次看到反编译出来的依赖工程后总会遇到一些类型信息的代码,也没有深入去研究,看完类型信息与反射机制后,有种茅塞顿开之感,写写个人感受。

       首先介绍下后面会经常用到的概念RTTI(Run-Time type Identification),运行时类型信息。简单理解就是程序能够在运行时发现和使用类型信息。

       RTTI有什么作用?它解放了程序在编译期执行的面向类型的操作,不管是程序的安全性还是可扩展性,都等到了大大的加强。

       我们一般有两种方法来实现运行时识别对象和类的信息:传统的RTTI和反射机制

       先看一个简单的例子:


package cn.OOP.Typeinfo;

import java.util.Arrays;
import java.util.List;

abstract class Student{
	void study(){ System.out.println(this+".study()");}
	abstract public String toString();
}

class PrimaryStudent extends Student{
	public String toString(){ return "PrimaryStudent";}
}

class HighSchoolStudent extends Student{
	public String toString(){ return "HighSchoolStudent";}
}

class UniversityStudent extends Student{
	public String toString(){ return "UniversityStudent";}
}


public class Students {

	public static void main(String args[]){
		List<Student>  studentList = Arrays.asList(
				new PrimaryStudent(),new HighSchoolStudent(),new UniversityStudent()
			);
		for(Student s : studentList){
			s.study();
		}
	}
}
/* output:
PrimaryStudent.study()
HighSchoolStudent.study()
UniversityStudent.study()
*/



基类student中包含study方法,通过传递this参数给System.out.println(),间接使用toString()来打印类标识符。这里,toString()被声明为Abstract方法,强制了子类覆盖该方法,并可以防止无格式的Student格式化。

       输出结果反映,子类通过覆盖toString()方法,study方法在不同的情况下,会有不同的输出(多态)。

       而且,在将Student子类的对象放入List<Student>数组时,对象被自动向上转型为Student类,但同时也丢失了student对象的具体类型信息,对于程序而言,如果我们不对数组内的对象进行向下转型,那么他们“只是”student对象。

       上述例子中,还有一个地方用到了RTTI,容器List将它持有的对象都当成object对象来处理。当我们从数组中取出对象时,对象被转型回student类型。这是最基本的RTTI形式,因为在java中,所有的类型转换都是在运行时才进行正确性检查的。

       还有一点,例子中的RTTI类型转换并不彻底,object对象被转型成student,而不是Ustudent、Pstudent、Hstudent。这是因为程序只知道数组中保存的是student,在编译时java通过容器和泛型来确保这一点,而在运行时就由转型来实现。

       例子很简单,但说明的东西很多。

一个特殊的编程问题        

       在编程过程中,如果我们能够知道某个泛化的引用的确切类型的时候,我们可以方便快捷的解决它,有没有什么方法能够知道这个泛化的引用的确切类型呢?

       比如,我们将所有继承自基类A的类全部放入一个数组或者list中,当我们需要对该基类下的某个子类找出来的时候,系统时并不容易判断的,因为对于程序而言,数组中的对象都时基类A,使用RTTI,久可以查询基类A的确切类型,然后选出或者剔除该子类型。

Class对象

       class对象是RTTI在java中工作机制的核心。

       我们知道,java程序是由一个一个类组成的,而对于每一个类,都有一个class对象与之对应,也就是说,每编译一个新类都会产生一个class对象(事实上,这个class对象是被保存在同名的.class文件当中的)。这个过程涉及到类的加载,这里不展开篇幅去写它。

       无论何时,想要得到运行时类型信息,就必须得到class对象的引用,常规来讲,class对象的引用有三种获取方式,而且它包含很多有用的方法,请看如下程序:


package cn.OOP.Typeinfo;

interface Drinkable{}
interface Sellable{}

class Coke{
	Coke(){}           //运行这个程序后,注释掉这个默认的无参构造器再试一试
	Coke(int i ){}
}

class CocaCola extends Coke 
	implements Drinkable,Sellable{
	public CocaCola() { super(1);}
}


public class TestClass {
	static void printinfo(Class c){
		System.out.println("Class Name:"+c.getName()+" is interface? ["+
				c.isInterface()+"]");
		System.out.println("Simple Name:"+c.getSimpleName());
		System.out.println("Canonical Name:"+c.getCanonicalName());
	}
	
	
	public static void main(String args[]){
		Class c= null;
		
		try{
			c = Class.forName("cn.OOP.Typeinfo.CocaCola");
			
			//or we can init c in this way
//			CocaCola cc = new CocaCola();
//			c = cc.getClass();
			
			//we can also init c in this way
//			c = CocaCola.class;
			
		}catch(ClassNotFoundException e){
			System.out.println("Can't find CocaCola!!");
			System.exit(1);
		}
		
		printinfo(c);
		
		for(Class face : c.getInterfaces()){
			printinfo(face);
		}
	}
}/* output:
Class Name:cn.OOP.Typeinfo.CocaCola is interface? [false]
Simple Name:CocaCola
Canonical Name:cn.OOP.Typeinfo.CocaCola
Class Name:cn.OOP.Typeinfo.Drinkable is interface? [true]
Simple Name:Drinkable
Canonical Name:cn.OOP.Typeinfo.Drinkable
Class Name:cn.OOP.Typeinfo.Sellable is interface? [true]
Simple Name:Sellable
Canonical Name:cn.OOP.Typeinfo.Sellable
*///



       

CocaCola类继承自Cola类并实现了drinkable和sellable接口,在main方法中,我们用了forName()方法创建一个class对象的引用,需要注意的是,forName方法传入的参数必须时全限定名(就是包含包名)

        在printInfo方法中,分别使用getSimpleName和getCanconicaName来打印出不包含包名的类名和全限定的类名。isInterface方法很明显,是得到这儿class对象是否表示一个接口。虽然我们在这里知识看到class对象的3种方法,但实际上,通过class对象我们能够了解到类型的几乎所有信息。上述例子中有三种不同的获取class对象的方法:

       Class.forName():最简单,也是最快捷的方法,因为我们不需要为获取class对象而持有该类对象的实例。

       obj.getClass():当我们已经拥有一个感兴趣的类型的对象时,这个方法很好用。

       obj.class:类字面常亮,这种方式很安全,因为它在编译时就会得到检查,因此不用放到try-catch中,而且非常高效。

泛化的class引用

       通过上面的例子我们可以知道,class引用表示的是它所只想对象的确切类型,并且,通过class对象能够获得特定类的几乎所有信息。但是,java的设计者并不止步于此,通过泛型,我们能够让class引用所指向的类型更加具体:


public class GenericClassReference {

	public static void main(String args[]){
		Class intClass = int.class;
		Class<Integer> genericIntClass = int.class;
		
		genericIntClass = Integer.class;  //same thing
		intClass = double.class;
//		genericIntClass = double.class;  
	}
}



   看这个例子,普通的class引用intClass能被随意赋值指向任意类型。但是使用了泛型以后,编译器会强制对class的引用的重新赋值进行检查。

       但是这种泛型的使用与普通的泛型又是不同的,比如下面这条语句:

       Class<Number> C = int.class;

       初看貌似没有什么问题,Integer继承自Number类,不就是父类引用指向子类对象么,但是实际上,这行代码在编译时就会报错,因为Integer的class对象引用不是Number的class 引用的子类。如何解决这个问题,使用通配符?解决,请看:


public class WildClassReference {

	public static void main(String args[]){
		Class<?> intClass = int.class;  //? means  everything
		intClass = double.class;
		
		Class<? extends Number> longClass = long.class;
		longClass = float.class;    //Compile Success
	}
}



       

通配符?表示“任何类”,所以intClass能够重新指向double.class,同时,? extends Number表示任何Number类的子类。

反射:RTTI实现和动态编程

       上面的例子可以看出,RTTI可以告诉你所有你想知道的类型信息,但是前提是这个类型在编译的时候时已知的。但是假设程序获取一个程序空间以外的对象的引用,即编译时并不存在的类,例如从本地硬盘,从网络,那怎么办呢?


import java.util.Scanner;

public class Reflection {

	
	public static void main(String args[]){
		Class c = null;
		Scanner sc = new Scanner(System.in);
		System.out.println("Please put the name of the Class you want load:");
		String ClassName = sc.next();
		
		try {
			c = Class.forName(ClassName);
			System.out.println("successed load the Class:"+ClassName);
		} catch (ClassNotFoundException e) {
			System.out.println("Can not find the Class ACommonClass");
			System.exit(1);
		}
		
		
	}
}



当我们运行这个程序的时候,程序会阻塞在这一步,String ClassName = sc.next();

这时输出的是:Please input the name of the Class you want load:

然后我们输入一个类名,比如我们输入一个我们自定义的类:ACommonClass,但是我们并没有开始写这个类,

更没有编译这个类,也就没有对应的.Class文件。

这时候,我们才开始写我们的ACommonClass类

public class ACommonClass { 
	//I am just a generic Class
}



编译这个类,得到此类的class 文件,然后在上一个程序中输入类名ACommonClass,阻塞停止,打印输出

 

cn.OOP.Typeinfo.ACommonClass
successed load the Class:ACommonClass



在这个例子中,我们能看到一个和传统编程不同的东西,在程序运行时,我们还能云淡风轻的写着程序必须的类。当然,这知识一个最简单的RTTI反射的应用,Class类与java.lang.reflect类库一起对反射机制提供了支持,当我们用反射机制做某些事情的时候,我们还是必须知道特定的类(也就是必须得到.class文件),要么在本地,要么从网络获取,所不同的是,由于设计体系的特殊,我们逃避了在编译期的检查,直到运行时才打开和检查.class文件,我想这就是为什么这个机制叫做RTTI(运行时类型信息 )。