关闭

Java编程思想 - 类型信息与反射机制

标签: java类型信息反射
411人阅读 评论(0) 收藏 举报
分类:

首先介绍一个本文后面会频繁提到的概念:RTTI(Runtime Type Information,或者,Run-Time Type Identification),运行时类型信息。简单来说,就是指程序能够在运行时发现和使用类型信息。



       RTTI能做什么??它解放了程序在编期间执行的面向类型的操作,不管是程序的安全性还是可扩展性和可维护性,都得到了大大的加强。



      我们一般使用二种方式来实现运行时识别对象和类的信息:“传统的”RTTI和“反射”机制。


、一个大家都熟悉的例子

传统的RTTI假定我们在编译时已经知道了所有的信息。


下面我们来看一看一个很熟悉的例子:


Java代码  收藏代码
  1. <span style="font-size: small;">package cn.OOP.Typeinfo;  
  2.   
  3. import java.util.Arrays;  
  4. import java.util.List;  
  5.   
  6. abstract class Student{  
  7.     void study(){ System.out.println(this+".study()");}  
  8.     abstract public String toString();  
  9. }  
  10.   
  11. class PrimaryStudent extends Student{  
  12.     public String toString(){ return "PrimaryStudent";}  
  13. }  
  14.   
  15. class HighSchoolStudent extends Student{  
  16.     public String toString(){ return "HighSchoolStudent";}  
  17. }  
  18.   
  19. class UniversityStudent extends Student{  
  20.     public String toString(){ return "UniversityStudent";}  
  21. }  
  22.   
  23.   
  24. public class Students {  
  25.   
  26.     public static void main(String args[]){  
  27.         List<Student>  studentList = Arrays.asList(  
  28.                 new PrimaryStudent(),new HighSchoolStudent(),new UniversityStudent()  
  29.             );  
  30.         for(Student s : studentList){  
  31.             s.study();  
  32.         }  
  33.     }  
  34. }  
  35. /* output: 
  36. PrimaryStudent.study() 
  37. HighSchoolStudent.study() 
  38. UniversityStudent.study() 
  39. *///  
  40. </span>  

  基类Studeng中包含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通过容器和泛型来确保这一点,而在运行时就由转型来实现。



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



二、一个特殊的编程问题

在现实当中,当我们能够知道某个泛化引用的确切类型的时候,我们可以方便快捷的解决它,怎么办??


比如:我们扫描某个区域的生物(animal),并将之装入一个数组。当用户要突出的显出其中的某一类,如人类,的时候,系统是并不容易判断的。因为,对于程序而言,数组中的对象都是aniaml。使用RTTI,可以查询animal引用的确切类型,然后选择或者剔除特例。




三、Class对象

Class对象是RTTI在Java中工作机制的核心。


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

 

无论何时,只要我们想要在运行时使用类型信息,就必须获得恰当的class对象的引用。

 

 

常规来讲,class对象有三种获得方式,而且,它包含很多有用的方法。看下面的程序:

 

 

Java代码  收藏代码
  1. package cn.OOP.Typeinfo;  
  2.   
  3. interface Drinkable{}  
  4. interface Sellable{}  
  5.   
  6. class Coke{  
  7.     Coke(){}           //运行这个程序后,注释掉这个默认的无参构造器再试一试  
  8.     Coke(int i ){}  
  9. }  
  10.   
  11. class CocaCola extends Coke   
  12.     implements Drinkable,Sellable{  
  13.     public CocaCola() { super(1);}  
  14. }  
  15.   
  16.   
  17. public class TestClass {  
  18.     static void printinfo(Class c){  
  19.         System.out.println("Class Name:"+c.getName()+" is interface? ["+  
  20.                 c.isInterface()+"]");  
  21.         System.out.println("Simple Name:"+c.getSimpleName());  
  22.         System.out.println("Canonical Name:"+c.getCanonicalName());  
  23.     }  
  24.       
  25.       
  26.     public static void main(String args[]){  
  27.         Class c= null;  
  28.           
  29.         try{  
  30.             c = Class.forName("cn.OOP.Typeinfo.CocaCola");  
  31.               
  32.             //or we can init c in this way  
  33. //          CocaCola cc = new CocaCola();  
  34. //          c = cc.getClass();  
  35.               
  36.             //we can also init c in this way  
  37. //          c = CocaCola.class;  
  38.               
  39.         }catch(ClassNotFoundException e){  
  40.             System.out.println("Can't find CocaCola!!");  
  41.             System.exit(1);  
  42.         }  
  43.           
  44.         printinfo(c);  
  45.           
  46.         for(Class face : c.getInterfaces()){  
  47.             printinfo(face);  
  48.         }  
  49.     }  
  50. }/* output: 
  51. Class Name:cn.OOP.Typeinfo.CocaCola is interface? [false] 
  52. Simple Name:CocaCola 
  53. Canonical Name:cn.OOP.Typeinfo.CocaCola 
  54. Class Name:cn.OOP.Typeinfo.Drinkable is interface? [true] 
  55. Simple Name:Drinkable 
  56. Canonical Name:cn.OOP.Typeinfo.Drinkable 
  57. Class Name:cn.OOP.Typeinfo.Sellable is interface? [true] 
  58. Simple Name:Sellable 
  59. Canonical Name:cn.OOP.Typeinfo.Sellable 
  60. *///  

 

CocaCola类继承自Cola类并实现了Drinkable、Sellable接口。在mian方法中,我们用forName()方法创建了一个

Class对象的引用。需要注意的是,forName()方法传入的参数必须是全限定名(就是包含包名)

 

在printinfo()方法中,分别使用getSimpleName()和getCanonicalName()来打印出不含包名的类名和全限定的类名。isInterface()很明显,是得到这个class对象是否表示一个接口。虽然,我们这里只是演示了class对象的3种方法,但实际上,通过class对象,我们发现我们能够了解到类型的几乎所有的信息(之所以是“几乎”是因为我们有时候并不需要客户了解我们提供的类的某些信息,而选择性的屏蔽。大家可以试一试用class对象查询某些类的private属性!)

 

例子有出现了三种不同的获得class对象的方法:

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

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

Obj.class :  类字面常量,这种方式很安全,因为它在编译时就会得到检查(因此不需要放到try语句块中),而且高效。

我们可以根据我们的程序的条件和需要,选择上面三种方式中的任何一种来实现RTTI。

 

 

四、泛化的Class引用

通过上面的例子我们可以很容易的知道,class引用表示的是它所指向的对象的确切类型,并且,通过class对象我们

能获取特定类的几乎所有信息。这很容易理解。

 

但是,Java的设计者并不止步于此。通过泛型,我们可以让class引用所指向的类型更加具体。

 

package cn.OOP.Typeinfo;

Java代码  收藏代码
  1. public class GenericClassReference {  
  2.   
  3.     public static void main(String args[]){  
  4.         Class intClass = int.class;  
  5.         Class<Integer> genericIntClass = int.class;  
  6.           
  7.         genericIntClass = Integer.class;  //same thing  
  8.         intClass = double.class;  
  9. //      genericIntClass = double.class;   //Illegal,<span style="font-size: xx-small;"><span class="hps" style="color: #333333; font-family: arial, sans-serif; white-space: normal; background-color: #f5f5f5;">Can not</span><span style="color: #333333; font-family: arial, sans-serif; white-space: normal; background-color: #f5f5f5;"> </span><span class="hps" style="color: #333333; font-family: arial, sans-serif; white-space: normal; background-color: #f5f5f5;">compiled</span></span>  
  10.     }  
  11. }  

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

 

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

Class<Number> c = int.class;

乍一看,没什么不对,Integer继承自Number类,不就是父类引用指向子类对象么。但实际上,这句代码会在编译时就出错。因为Integer的class对象引用不是Number的Class引用的子类。这看起来很诡异,但却是事实。

 

事实上,正确的做法是使用通配符?。看下面:

package cn.OOP.Typeinfo;

Java代码  收藏代码
  1. public class WildClassReference {  
  2.   
  3.     public static void main(String args[]){  
  4.         Class<?> intClass = int.class;  //? means  everything  
  5.         intClass = double.class;  
  6.           
  7.         Class<? extends Number> longClass = long.class;  
  8.         longClass = float.class;    //Compile Success  
  9.     }  
  10. }  
 

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

 

五、反射:RTTI实现和动态编程

就上面的例子我们看到,RTTI可以告诉你所有的你想知道的类型信息,前提是这个类型是在编译时候已知的。

 

这好像没有什么不对吧??

 

将眼界放开点:假设程序获取了一个我们程序空间以外的对象的引用。即编译时并不存在的类。例如:从本地硬盘,从网络。要知道,Java的一大优势就是适于现在的WEB环境!!!

 

看下面:

package cn.OOP.Typeinfo;

Java代码  收藏代码
  1. import java.util.Scanner;  
  2.   
  3. public class Reflection {  
  4.   
  5.       
  6.     public static void main(String args[]){  
  7.         Class c = null;  
  8.         Scanner sc = new Scanner(System.in);  
  9.         System.out.println("Please put the name of the Class you want load:");  
  10.         String ClassName = sc.next();  
  11.           
  12.         try {  
  13.             c = Class.forName(ClassName);  
  14.             System.out.println("successed load the Class:"+ClassName);  
  15.         } catch (ClassNotFoundException e) {  
  16.             System.out.println("Can not find the Class ACommonClass");  
  17.             System.exit(1);  
  18.         }  
  19.           
  20.           
  21.     }  
  22. }  

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

 

这时的输出是:

Please put the name of the Class you want load:

 

 

然后,由我们给定一个类名。假定,我们需要输入一个我们自定义的类:ACommonClass。注意,这是关键。这个时候,我们并没有开始写这个类,更没有编译这个类,也就没有对应的.class文件。

 

这个时候,写第二个程序(类)

package cn.OOP.Typeinfo;

Java代码  收藏代码
  1. public class ACommonClass {   
  2.     //I am just a generic Class  
  3. }  

  编译这个类,得到此类的.class 文件。然后,在程序一(注意,这个程序一直没有停止,一直在运行)输入类名:

cn.OOP.Typeinfo.ACommonClass,阻塞停止,打印输出。

 

最终结果:

Please put the name of the Class you want load:

Java代码  收藏代码
  1. cn.OOP.Typeinfo.ACommonClass  
  2. successed load the Class:cn.OOP.Typeinfo.ACommonClass  
 

在这个例子中,我们看到了一个和以前传统的编程截然不同的东西:在程序运行时,我们还能云淡风轻的写着程序必须的类,而且,这个类还能用于这个已经开始的程序!!!!神奇吧。

 

当然,这只是一个最简单的RTTI反射应用,通常的Java动态编程会更复杂,也更神奇!!

 

Class类与java.lang.reflect类库一起对反射机制提供了支持。当通过反射与一个未知类型的对象打交道时,JVM只是简单的检查这个对象,看他属于哪个特定的类(就像传统的RTTI一样)。当我们用反射机制做某些事情时,我们还是必须知道特定的类(也就是必须得到.class文件),要么在本地,要么从网络获取。所不同的是,由于设计体系的特殊,我们逃避了在编译期间的检查,知道运行时猜打开和检查.class文件。

 

未完待续!!!!

等会更新

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:101418次
    • 积分:1480
    • 等级:
    • 排名:千里之外
    • 原创:31篇
    • 转载:95篇
    • 译文:0篇
    • 评论:11条
    最新评论