Java中的型別資訊
學習執行時型別資訊,要與多型機制進行區分
面向物件的程式語言的目的是讓我們在凡是可以使用多型的地方都使用多型機制。
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) { //當把Shape物件放入List<Shape>的陣列時會向上轉型。 //但在向上轉型為Shape的時候也丟失了Shape物件的具體型別。 List<Shape> shapeList = Arrays.asList(new Circle(), new Square(), new Triangle()); //從陣列中取出元素時,List容器(實際上它將所有的事物都當作Object持有) //回自動將結果轉型回Shape 這是RTTI最基本的形式 for (Shape shape : shapeList) //這個地方是多型機制來處理 //shape物件實際執行什麼樣的程式碼,是由引用所指向的具體物件來決定的 shape.draw(); } }
如上面的程式碼,當把Shape物件放入List<Shape>的陣列時會向上轉型。但在向上轉型為Shape的時候也丟失了Shape物件的具體型別。對於陣列而言,它們只是Shape類的物件。
當從陣列中取出元素時,這種容器—實際上它將所有的事物都當作Object持有—會自動將結果轉型回Shape。Object被轉型為Shape,而不是轉型為Circle,Square。
接下來多型機制發揮作用。Shape物件具體執行什麼樣的程式碼,是由引用所指向的具體物件Circle,Square來決定的。通常,你希望大部分程式碼儘可能少的瞭解物件的具體型別,而是隻與物件家族中的一個通用表示打交道。“多型”是面向物件程式設計的基本目標。
什麼時候需要執行時型別資訊呢?
當你碰到這樣的應用場景時,想知道某個泛化引用的確切型別,從而進行某種操作。比如,我們希望選出圖形中所有的圓形,就要識別到Circle這個確切的型別。
執行時型別資訊 使得你可以在程式執行時發現和使用型別資訊。
這樣可以把你從只能在編譯期執行面向型別的操作的禁錮中解脫出來。
在Java中是如何讓我們在執行時識別物件和類的資訊的呢?
主要有兩種方式:
1. 在執行時進行強制型別轉換
2. 使用反射機制,在執行時載入類,並建立物件
型別資訊在執行時是如何表示的呢?
Class物件,它包含了與類有關的資訊。Java使用Class物件來確定執行時的型別資訊。
每個類都有一個Class物件(每當編寫並編譯了一個新類,就會產生一個Class物件,被儲存在一個同名的.class檔案中)。為了使用這個類的Class物件,執行這個程式的java虛擬機器將使用被稱為“類載入器”的子系統。
所有的類都是在對其第一次使用時,動態載入到JVM中的。當程式建立第一個對類的靜態成員的引用時,就會載入這個類。這個證明構造器也是類的靜態方法。
類載入的過程:
當我們寫完一個Java原始檔編譯成一個.class檔案的時候,如果載入進JVM,就要經歷一個上圖所示的生命週期。
當虛擬機器遇到一條new指令的時候,首先去常量池定位這個類的符號引用,如果這個類已經被載入,解析和初始化了,則直接使用(比如單例物件)。否則,就執行上述過程。當一個類物件之前已經例項化過了,再次生成一個新物件時只需要初始化了。
1. 類的載入
類的載入是通過類載入器(ClassLoader)來完成。類載入器通過一個類的全限定名來獲取這個類的二進位制流,也就是那個.class檔案流,將一些類的靜態資訊(欄位,方法名,常量等)放到方法區,生成這個類的Class物件。事實上,虛擬機器並不一定要求這個class檔案必須在本地,也沒有要求必須是java語言編寫的(Groovy等也可以)。這個類可以來源於網路,也可以來源於資料庫,甚至我們還可以手動寫一個符合規範的class檔案(CGlib)。當把這個class檔案載入進虛擬機器就完成了類載入的過程。
2. 驗證階段
因為class檔案可以通過各種途徑獲取,還可以不使用java語言來編寫,在類載入的過程中怎麼保證載入的class檔案是符合規範並且不會在執行過程中引起JVM宕機呢?這就需要一個驗證階段。驗證階段完成了class檔案格式的驗證(比如魔數,版本號等),元資料的驗證(類的繼承合法性),位元組碼驗證,符號引用驗證等。當一個class檔案完全通過JVM檢驗合格之後就可以進入準備階段了。
3. 準備階段
準備階段做的事情很簡單。就是為這個類的一些類變數(static)在方法區分配記憶體但沒有初始化(初始化都是在初始化階段完成)。但是常量欄位除外(static final)。這些欄位在這個階段已經賦值並且放入方法區的常量池了。
4. 解析階段
類的解析是虛擬機器做的最複雜的一項工作。它會將一些符號引用(虛地址)替換為直接引用(實際地址),完成類的繼承關係的解析,完成欄位和方法的解析。還會進行各種許可權的驗證。當一個類的各種符號引用被替換為了直接引用。它的各種父類,介面,欄位,方法完成了解析。這個類就可以進入初始化階段了。
5. 初始化階段
類的初始化階段簡單的說就是執行一個<init>方法。這個方法是Java虛擬機器幫我們生成的。是在建構函式執行(建構函式是程式設計師主動呼叫的)前由java虛擬機器呼叫執行。目的在於完成類例項各個變數的初始化賦值,靜態程式碼塊的執行等。當然,如果一個類沒有靜態程式碼塊,也沒有變數賦值,那麼這個< init>方法什麼也不用幹了。
將class檔案載入到JVM中的方法:
1. new一個類物件
2. 呼叫Class.forName()方法
只要你想在執行時使用型別資訊,就必須首先獲得對恰當的Class物件的引用。
獲得Class物件引用的方法:
1. 具體物件.getClass(): 當你持有此型別的物件時,可以使用此方法
2. Class.forName():這是實現此功能的便捷途徑,因為你不需要為了獲得Class引用而持有該型別的物件。要放到try-catch子句中,因為如果找不到這個類,會丟擲ClassNotFoundException。獲得對類的引用的同時進行初始化。
3. 類字面常量。在編譯期就會受到檢查,無需try-catch。使用類字面常量來獲得對類的引用不會引發初始化,初始化實現了“惰性”,即延遲到了對靜態方法(構造器隱式地是靜態的)或者非常數靜態域進行首次引用時才執行。
強制型別轉換的幾種情況
1. 我們在編譯時已經知道了所有的型別,所以直接進行強制型別轉換
2. 進行向下轉型前,如果沒有其他資訊可以告訴你這個物件是什麼型別,那麼可以使用關鍵字instanceof
3. 進行向下轉型前,如果沒有其他資訊可以告訴你這個物件是什麼型別,那麼可以Class類的方法isInstance(obj)