java進階(三)-- 類型信息與反射機制

          前段時間在看《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的反射機制,請移步到我的另外一篇博文

《java基礎知識(二)-- 反射機制》


發佈了29 篇原創文章 · 獲贊 3 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章