標籤:
From Thinking in Java 4th Edition
RTTI(Run-Time Type Information),運行時類型資訊,使得你可以在程式運行時發現和使用類型資訊。對RTTI的需要,揭示了物件導向設計中許多有趣(並且複雜)的問題,同時也提出了如何組織程式的問題。
Java是如何讓我們在運行時識別對象和類的資訊的。主要有兩種方式:
1. “傳統的”RTTI,它假定我們在編譯時間已經知道了所有的類型
2. “反射”機制,它允許我們在運行時發現和使用類型資訊
通常會建立一個具體對象(Circle, Square, or Triangle),把它向上轉型成Shape(忽略對象的具體類型),並在後面的程式中使用匿名的Shape引用:
import java.util.*;abstract class Shape {void draw() { System.out.println(this + ".draw()"); }abstract public String toString();}class Circle extends Shape {public String toString() { return "Circle"; }}class Square extends Shape {public String toString() { return "Square"; }}class Triangle extends Shape {public String toString() { return "Triangle"; }}public class Shapes {public static void main(String[] args){List<Shape> shapeList = Arrays.asList(new Circle(), new Square(), new Triangle());for(Shape shape : shapeList)shape.draw();}} /* Output:Circle.draw()Square.draw()Triangle.draw()*/
當從數組中取出元素時,這種容器——實際上它將所有的事物都當作Object持有——會自動將結果轉換為Shap。這是RTTI最基本的使用形式,所有的類型轉換都是在運行時進行正確檢查的。這也是RTTI名字的含義:在運行時,識別一個對象的類型。
這個例子中,RTTI的轉換並不徹底: Object被轉型為Shape,而不是轉型為Circle, Square or Triangle。這是因為目前我們從List<Shape>只知道儲存的都是Shape,在編譯時間,將由容器和Java泛型來強制確保這一點;而在運行時,由類型轉換操作來確保這一點。
接下來就是多態機制了,Shape對象實際執行什麼樣的代碼,是由引用所指向的具體對象Circle, Square o Triangle而決定的。
使用RTTI,可以查詢某個Shape引用所指向的對象的確切類型,然後選擇或者剔除特列。
Class對象
要理解RTTI在Java中的工作原理,首先必須知道類型資訊在運行時是如何表示的。這項工作是由稱為Class對象的特殊對象完成的,它包含了與類有關的資訊。
類是程式的一部分,每個類都有一個Class對象。每當編寫並且編譯了一個新類,就會產生一個Class對象(更恰當地說,是被儲存在一個同名的.class檔案中)。為了產生這個類的對象,運行在這個程式的Java虛擬機器將使用被稱為“類載入器”的子系統。
類載入器子系統實際上可以包含一條類載入器鏈,但是只有一個原生類載入器,它是JVM實現的一部分。
所有的類都是在對其第一次使用時,動態載入到JVM中的。當程式建立第一個對類的靜態成員的引用時,就會載入這個類。
一旦某個類的Class對象被載入記憶體,它就被用來建立這個類的所有對象:
// Examination of the way the class loader works.import static net.mindview.util.Print.*;class Candy {static { print("Loading Candy"); }}class Gum {static { print("Loading Gum"); }}class Cookie {static { print("Loading Cookie"); }}public class SweetShop {public static void main(String[] args){print("inside main");new Candy();print("After creating Candy");try {Class.forName("Gum");} catch(ClassNotFoundException e){print("Couldn‘t find Gum");}print("After Class.forName(\"Gum\")");new Cookie();print("After creating Cookie");}} /* Output:inside mainLoading CandyAfter creating CandyLoading GumAfter Class.forName("Gum")Loading CookieAfter creating Cookie*/
forName()是取得Class對象的引用的一種方法。它是由一個包含目標類的文本名的String作輸入參數,返回的是一個Class對象的引用。對forName()的調用是為了它產生的“副作用”:如果類Gum還沒有被載入,那麼就載入它。在載入過程中,Gum的static子句就被執行。
無論何時,只要你想在運行時使用類型資訊,就必須首先獲得對恰當的Class對象的引用。Class.forName()就是實現此功能的便捷途徑,因為你不需要為了獲得Class的引用而持有該類型的對象。
但是,如果你已經有了一個感興趣的類型的對象,那就可以通過調用getClass()方法來擷取Class引用(這個方法屬於根類Object的一部分,它將返回表示該對象的實際類型的Class引用):
// Testing Class Class.package Leetcode.DrJava;import static net.mindview.util.Print.*;interface HasBatteries {}interface Waterproof {}interface Shoots {}class Toy {// Comment out the following default constructor// to see NoSuchMethodError from (*1*)Toy() {}Toy(int i) {}}class FancyToy extends Toy implements HasBatteries, Waterproof, Shoots {FancyToy() { super(1); }}public class ToyTest {static void printInfo(Class cc){print("Class name: " + cc.getName() + " is interface? [" + cc.isInterface() + "]");print("Simple name: " + cc.getSimpleName());print("Canonical name: " + cc.getCanonicalName());}public static void main(String[] args){Class c = null;// get the reference of a Class try {c = Class.forName("Leetcode.DrJava.FancyToy");} catch(ClassNotFoundException e){print("Can‘t find FancyToy");System.exit(1);}printInfo(c);// return the interfaces in "c" Class.for(Class face : c.getInterfaces())printInfo(face);// return the superclass of the "c" classClass up = c.getSuperclass();Object obj = null;try {// Requires default constructorobj = up.newInstance();} catch(InstantiationException e){print("Cannot instantiate");System.exit(1);} catch(IllegalAccessException e){print("Can‘t access");System.exit(1);}printInfo(obj.getClass());}} /* Output:Class name: Leetcode.DrJava.FancyToy is interface? [false]Simple name: FancyToyCanonical name: Leetcode.DrJava.FancyToyClass name: Leetcode.DrJava.HasBatteries is interface? [true]Simple name: HasBatteriesCanonical name: Leetcode.DrJava.HasBatteriesClass name: Leetcode.DrJava.Waterproof is interface? [true]Simple name: WaterproofCanonical name: Leetcode.DrJava.WaterproofClass name: Leetcode.DrJava.Shoots is interface? [true]Simple name: ShootsCanonical name: Leetcode.DrJava.ShootsClass name: Leetcode.DrJava.Toy is interface? [false]Simple name: ToyCanonical name: Leetcode.DrJava.Toy*/
1. 在main()中,用forName()方法在適當的try語句塊中,建立了一個Class引用,並將其初始化為指向FancyToy的Class。(forName中的參數用的是全限定名,即包含包名)。
2. printInfo()中使用getName()來產生許可權定的類名;運用getSimpleName()來產生不包含包名的類名;運用getCanonicalName()來產生全限定的類名。
(getName()返回的是虛擬機器裡面的class的表示, 而getCanonicalName()返回的是更容易理解的表示)
3. Class.getInterfaces()方法返回的是Class對象,它們表示感興趣的Class對象中所包含的介面
4. 如果有一個Class的對象,還可以使用getSuperclass()方法查詢其直接基類。
5. Class的newInstance()方法是實現“虛擬構造器”的一種途徑:“我不知道你的具體類型,但無論如何要正確地建立你自己”。
使用newInstance()方法來建立的類,必須帶有預設的構造器。之後我們會看到如何利用Java的反射API,用任意的構造器來動態地建立類的對象。
類型字面常量
Java提供了另外一種方法來產生對Class對象的引用,即使用類型字面常量。對以上程式可以這樣寫:
FancyToy.class
這樣做更簡單、安全,並且在編譯時間就會受到檢查。
當使用“.class”來建立對Class對象的引用時,不會自動地初始化該Class對象。為了使用類而做的準備工作實際包含三個步驟:
1. 載入,這是由類載入器執行的,檢查位元組碼,並建立Class對象
2. 連結。驗證類中的位元組碼,為靜態域分配空間
3. 初始化。執行靜態初始化器和靜態初始化塊。
初始化被延遲到了對靜態方法或者非常數靜態域進行首次引用時執行:
import java.util.*;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 initable = Initable.class;System.out.println("After creating Initable ref");// Does not trigger initialization:System.out.println(Initable.staticFinal);// Does not trigger initialization:System.out.println(Initable.staticFinal2);// Does not trigger initialization:System.out.println(Initable2.staticNonFinal);Class initable3 = Class.forName("Initable3");System.out.println("After creating Initable3 ref");System.out.println(Initable3.staticNonFinal);}} /* Output:After creating Initable ref47Initializing Initable258Initializing Initable2147Initializing Initable3After creating Initable3 ref74*/
1. 從對initable引用的建立中可以看到,僅使用.class文法來獲得對類的引用不會引發初始化。
2. 但是從initable3引用的建立可以看出,Class.forName()立即就進行了初始化。
3. 如果一個static final值是“編譯期常量”,就像Initable.staticFinal那樣,那麼這個值不需要對Initable類進行初始化就可以讀取
4. 但如果一個域僅僅是static和final的,還不足以確保3的行為,例如對Initable.staticFinal2的訪問將強制進行類的初始化,因為它不是編譯期常量
5. 如果一個static域不是final的,那麼對它訪問時,就要求在訪問前進行連結(為這個域分配儲存空間)和初始化(初始化該儲存空間),就像對Initable2.statiNonFinal的訪問。
Thinking in Java Chapter 14