前段時間在看《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(運行時類型信息 )。
本文重點介紹RTTI(運行時類型信息),如果想了解更多關於java的反射機制,請移步到我的另外一篇博文