1. 程式人生 > >RRTI的概念以及Class對象作用

RRTI的概念以及Class對象作用

eat 有趣的 getclass 2種 init null java虛擬機 class對象 小例子

  深入理解Class對象
  
  RRTI的概念以及Class對象作用
  
  認識Class對象之前,先來了解一個概念,RTTI(Run-Time Type Identification)運行時類型識別,對於這個詞一直是 C++ 中的概念,至於Java中出現RRTI的說法則是源於《Thinking in Java》一書,其作用是在運行時識別一個對象的類型和類的信息,這裏分兩種:傳統的”RRTI”,它假定我們在編譯期已知道了所有類型(在沒有反射機制創建和使用類對象時,一般都是編譯期已確定其類型,如new對象時該類必須已定義好),另外一種是反射機制,它允許我們在運行時發現和使用類型的信息。在Java中用來表示運行時類型信息的對應類就是Class類,Class類也是一個實實在在的類,存在於JDK的java.lang包中,其部分源碼如下:
  
  public final class Class<T> implements java.io.Serializable,GenericDeclaration,Type, AnnotatedElement {
  
  private static final int ANNOTATION= 0x00002000;
  
  private static final int ENUM = 0x00004000;
  
  private static final int SYNTHETIC = 0x00001000;
  
  private static native void registerNatives();
  
  static {
  
  registerNatives();
  
  }
  
  /*
  
  * Private constructor. Only the Java Virtual Machine creates Class objects.(私有構造,只能由JVM創建該類)
  
  * This constructor is not used and prevents the default constructor being
  
  * generated.
  
  */
  
  private Class(ClassLoader loader) {
  
  // Initialize final field for classLoader. The initialization value of non-null
  
  // prevents future JIT optimizations from assuming this final field is null.
  
  classLoader = loader;
  
  Class類被創建後的對象就是Class對象,註意,Class對象表示的是自己手動編寫類的類型信息,比如創建一個Shapes類,那麽,JVM就會創建一個Shapes對應Class類的Class對象,該Class對象保存了Shapes類相關的類型信息。實際上在Java中每個類都有一個Class對象,每當我們編寫並且編譯一個新創建的類就會產生一個對應Class對象並且這個Class對象會被保存在同名.class文件裏(編譯後的字節碼文件保存的就是Class對象),那為什麽需要這樣一個Class對象呢?是這樣的,當我們new一個新對象或者引用靜態成員變量時,Java虛擬機(JVM)中的類加載器子系統會將對應Class對象加載到JVM中,然後JVM再根據這個類型信息相關的Class對象創建我們需要實例對象或者提供靜態變量的引用值。需要特別註意的是,手動編寫的每個class類,無論創建多少個實例對象,在JVM中都只有一個Class對象,即在內存中每個類有且只有一個相對應的Class對象,挺拗口,通過下圖理解(內存中的簡易現象圖):
  
  到這我們也就可以得出以下幾點信息:
  
  Class類也是類的一種,與class關鍵字是不一樣的。
  
  手動編寫的類被編譯後會產生一個Class對象,其表示的是創建的類的類型信息,而且這個Class對象保存在同名.class的文件中(字節碼文件),比如創建一個Shapes類,編譯Shapes類後就會創建其包含Shapes類相關類型信息的Class對象,並保存在Shapes.class字節碼文件中。
  
  每個通過關鍵字class標識的類,在內存中有且只有一個與之對應的Class對象來描述其類型信息,無論創建多少個實例對象,其依據的都是用一個Class對象。
  
  Class類只存私有構造函數,因此對應Class對象只能有JVM創建和加載
  
  Class類的對象作用是運行時提供或獲得某個對象的類型信息,這點對於反射技術很重要(關於反射稍後分析)。
  
  Class對象的加載及其獲取方式
  
  Class對象的加載
  
  前面我們已提到過,Class對象是由JVM加載的,那麽其加載時機是?實際上所有的類都是在對其第一次使用時動態加載到JVM中的,當程序創建第一個對類的靜態成員引用時,就會加載這個被使用的類(實際上加載的就是這個類的字節碼文件),註意,使用new操作符創建類的新實例對象也會被當作對類的靜態成員的引用(構造函數也是類的靜態方法),由此看來Java程序在它們開始運行之前並非被完全加載到內存的,其各個部分是按需加載,所以在使用該類時,類加載器首先會檢查這個類的Class對象是否已被加載(類的實例對象創建時依據Class對象中類型信息完成的),如果還沒有加載,默認的類加載器就會先根據類名查找.class文件(編譯後Class對象被保存在同名的.class文件中),在這個類的字節碼文件被加載時,它們必須接受相關驗證,以確保其沒有被破壞並且不包含不良Java代碼(這是java的安全機制檢測),完全沒有問題後就會被動態加載到內存中,此時相當於Class對象也就被載入內存了(畢竟.class字節碼文件保存的就是Class對象),同時也就可以被用來創建這個類的所有實例對象。下面通過一個簡單例子來說明Class對象被加載的時機問題(例子引用自Thinking in Java):
  
  package com.zejian;
  
  class Candy {
  
  static { System.out.println("Loading Candy"); }
  
  }
  
  class Gum {
  
  static { System.out.println("Loading Gum"); }
  
  }
  
  class Cookie {
  
  static { System.out.println("Loading Cookie"); }
  
  }
  
  public class SweetShop {
  
  public static void print(Object obj) {
  
  System.out.println(obj);
  
  }
  
  public static void main(String[] args) {
  
  print("inside main");
  
  new Candy();
  
  print("After creating Candy");
  
  try {
  
  Class.forName("com.zejian.Gum");
  
  } catch(ClassNotFoundException e) {
  
  print("Couldn‘t find Gum");
  
  }
  
  print("After Class.forName(\"com.zejian.Gum\")");
  
  new Cookie();
  
  print("After creating Cookie");
 
  在上述代碼中,每個類Candy、Gum、Cookie都存在一個static語句,這個語句會在類第一次被加載時執行,這個語句的作用就是告訴我們該類在什麽時候被加載,執行結果:
  
  inside main
  
  Loading Candy
  
  After creating Candy
  
  Loading Gum
  
  After Class.forName("com.zejian.Gum")
  
  Loading Cookie
  
  After creating Cookie
  
  Process finished with exit code 0
  
  從結果來看,new一個Candy對象和Cookie對象,構造函數將被調用,屬於靜態方法的引用,Candy類的Class對象和Cookie的Class對象肯定會被加載,畢竟Candy實例對象的創建依據其Class對象。比較有意思的是
  
  Class.forName("com.zejian.Gum");
  
  1
  
  1
  
  其中forName方法是Class類的一個static成員方法,記住所有的Class對象都源於這個Class類,因此Class類中定義的方法將適應所有Class對象。這裏通過forName方法,我們可以獲取到Gum類對應的Class對象引用。從打印結果來看,調用forName方法將會導致Gum類被加載(前提是Gum類從來沒有被加載過)。
  
  Class.forName方法
  
  通過上述的案例,我們也就知道Class.forName()方法的調用將會返回一個對應類的Class對象,因此如果我們想獲取一個類的運行時類型信息並加以使用時,可以調用Class.forName()方法獲取Class對象的引用,這樣做的好處是無需通過持有該類的實例對象引用而去獲取Class對象,如下的第2種方式是通過一個實例對象獲取一個類的Class對象,其中的getClass()是從頂級類Object繼承而來的,它將返回表示該對象的實際類型的Class對象引用。
  
  public static void main(String[] args) {
  
  try{
  
  //通過Class.forName獲取Gum類的Class對象
  
  Class clazz=Class.forName("com.zejian.Gum");
  
  System.out.println("forName=clazz:"+clazz.getName());
  
  }catch (ClassNotFoundException e){
  
  e.printStackTrace();
  
  }
  
  //通過實例對象獲取Gum的Class對象
  
  Gum gum = new Gum();
  
  Class clazz2=gum.getClass();
  
  System.out.println("new=clazz2:"+clazz2.getName());

  這種方式相對前面兩種方法更加簡單,更安全。因為它在編譯器就會受到編譯器的檢查同時由於無需調用forName方法效率也會更高,因為通過字面量的方法獲取Class對象的引用不會自動初始化該類。更加有趣的是字面常量的獲取Class對象引用方式不僅可以應用於普通的類,也可以應用用接口,數組以及基本數據類型,這點在反射技術應用傳遞參數時很有幫助,關於反射技術稍後會分析,由於基本數據類型還有對應的基本包裝類型,其包裝類型有一個標準字段TYPE,而這個TYPE就是一個引用,指向基本數據類型的Class對象,其等價轉換如下,一般情況下更傾向使用.class的形式,這樣可以保持與普通類的形式統一。
  
  boolean.class = Boolean.TYPE;
  
  char.class = Character.TYPE;
  
  byte.class = Byte.TYPE;
  
  short.class = Short.TYPE;
  
  int.class = Integer.TYPE;
  
  long.class = Long.TYPE;
  
  float.class = Float.TYPE;
  
  double.class = Double.TYPE;
  
  void.class = Void.TYPE;
  
  前面提到過,使用字面常量的方式獲取Class對象的引用不會觸發類的初始化,這裏我們可能需要簡單了解一下類加載的過程,如下:
  
  加載:類加載過程的一個階段:通過一個類的完全限定查找此類字節碼文件,並利用字節碼文件創建一個Class對象
  
  鏈接:驗證字節碼的安全性和完整性,準備階段正式為靜態域分配存儲空間,註意此時只是分配靜態成員變量的存儲空間,不包含實例成員變量,如果必要的話,解析這個類創建的對其他類的所有引用。
  
  初始化:類加載最後階段,若該類具有超類,則對其進行初始化,執行靜態初始化器和靜態初始化成員變量。
  
  由此可知,我們獲取字面常量的Class引用時,觸發的應該是加載階段,因為在這個階段Class對象已創建完成,獲取其引用並不困難,而無需觸發類的最後階段初始化。下面通過小例子來驗證這個過程:
  
  import java.util.*;
  
  class Initable {
  
  //編譯期靜態常量
  
  static final int www.dihaoyule798.com/ staticFinal = 47;
  
  //非編期靜態常量
  
  static final int staticFinal2 =
  
  ClassInitialization.rand.nextInt(1000);
  
  static {
  
  System.out.println("Initializing Initable");
  
  }
  
  }
  
  class Initable2www.yuheng119.com/ {
  
  //靜態成員變量
  
  static int staticNonFinal = 147;
  
  static www.xyseo.net{
  
  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對象
  
  Class initable = Initable.class;
  
  System.out.println("www.baqist.cn www.yyizx.cn After creating Initable ref");
  
  //不觸發類初始化
  
  System.out.println(Initable.staticFinal);
  
  //會觸發類初始化
  
  System.out.println(Initable.staticFinal2);
  
  //會觸發類初始化
  
  System.out.println(Initable2.staticNonFinal);
  
  //forName方法獲取Class對象
  
  Class initable3 = Class.forName("Initable3");
  
  System.out.println("After creating Initable3 ref");
  
  System.out.println(Initable3.staticNonFinal);
  
 

RRTI的概念以及Class對象作用