Java如何在運行時識別類型信息?

  • 2019 年 10 月 4 日
  • 筆記

在日常的學習工作當中,有一些知識是我們在讀書的時候就能夠習得;但有一些知識不是的,需要在實踐的時候才能得到真知——這或許就是王陽明提倡的「知行合一」。

在Java中,並不是所有的類型信息都能在編譯階段明確,有一些類型信息需要在運行時才能確定,這種機制被稱為RTTI,英文全稱為Run-Time Type Identification,即運行時類型識別,有沒有一點「知行合一」的味道?運行時類型識別主要由Class類實現。

01 Class類

在Java中,我們常用「class」(首字母為小寫的c)關鍵字來定義一個類,說這個類是對某一類對象的抽象。你比如說王二是一個網絡知名作者,我們可以這樣簡單地定義作者類:

package com.cmower.java_demo.fifteen;    class Author {      private String pen_name;      private String real_name;  }

現在,我們想知道Writer這個類本身的一些信息(比如說類名),該怎麼辦呢?這時候就需要用到「Class」(首字母為大寫的C)類,該類包含了與類有關的信息。請看以下代碼:

public class Test {      public static void main (String [] args) {          Author wanger = new Author();          Class c1 = wanger.getClass();          System.out.println(c1.getName());          //輸出 com.cmower.java_demo.fifteen.Author      }  }

當我們創建了作者對象wanger後,就可以通過wanger.getClass()獲取wanger的Class對象,通過c1.getName()可獲得wanger對象的類名。

想像一下,經過五年的刻意練習,王二從一名寫作愛好者晉陞為一名作家了。我們用代碼來假裝一下:

package com.cmower.java_demo.fifteen;    class Author {      private String pen_name;      private String real_name;  }    class Writer extends Author {      private String honour;  }    public class Test {      public static void main (String [] args) {          Author wanger = new Writer();          Class c1 = wanger.getClass();          System.out.println(c1.getName());          //輸出 com.cmower.java_demo.fifteen.Writer      }  }

在上例中,即使我們將Writer的對象引用wanger向上轉型為Author,wanger的Class對象類型依然是Writer(通過輸出結果可以判定)。這也就是說,Java能夠在運行時自動識別類型的信息,它不會因為wanger的引用類型是Author而丟失wanger真正的類型信息(Writer)。Java是怎麼做到這一點呢?

當Java創建某個類的對象,比如Writer類對象時,Java會檢查內存中是否有相應的Class對象。如果內存中沒有相應的Class對象,那麼Java會在.class文件中尋找Writer類的定義,並加載Writer類的Class對象。

一旦Class對象加載成功,就可以用它來創建這種類型的所有對象。這也就是說,每個對象在運行時都會有對應的Class對象,這個Class對象包含了這個對象的類型信息。因此,我們能夠通過Class對象知道某個對象「真正」的類型,並不會因為向上轉型而丟失。

02 獲取Class對象的其他方式

在使用getClass()方法獲取一個類的Class對象時,我們必須要先獲取這個類的對象,比如上面提到的wanger。如果我們之前沒有獲取這個類的對象,就需要用另外兩種方式來獲取類的Class對象:

Class c2 = Writer.class;  System.out.println(c2.getName());    try {      Class c3 = Class.forName("com.cmower.java_demo.fifteen.Writer");      System.out.println(c3.getName());  } catch (ClassNotFoundException e) {      e.printStackTrace();  }

1)當使用.class來獲取Class對象時,不會自動地初始化該Class對象,初始化被延遲到了對靜態方法或者非final靜態域進行首次引用時才執行。這樣做不僅更簡單,而且更安全,因為它在編譯時就會受到檢查(因此不需要置於try語句塊中)。

2)Class.forName會自動地初始化該Class對象,但需要指定類名,並且需要置於try語句塊中。

03 Class類提供的常用方法

Class類為我們提供了一些非常有用的方法,比如說getName()用來返回類名,getPackage()返回類所在的包名。

我們還可以利用Class類提供的newInstance()方法來創建相應類的對象,比如:

Class c2 = Writer.class;  System.out.println(c2.getName());    try {      Writer wangsan = (Writer) c2.newInstance();      System.out.println(wangsan);      // 輸出:com.cmower.java_demo.fifteen.Writer@7852e922  } catch (InstantiationException | IllegalAccessException e1) {      e1.printStackTrace();  }

由於我們在創建Class對象c2時沒有使用泛型,所以newInstance()返回的對象類型需要強轉為Writer。我們可以在此基礎上進行改進,示例如下:

Class<Writer> c4 = Writer.class;  System.out.println(c4.getName());    try {      Writer wangsan = c4.newInstance();      System.out.println(wangsan);      // 輸出:com.cmower.java_demo.fifteen.Writer@7852e922  } catch (InstantiationException | IllegalAccessException e1) {      e1.printStackTrace();  }

04 反射

我們還可以通過getFields()獲取所有public修飾的字段,通過getMethods()返回所有public修飾的方法。

甚至,我們還可以通過getDeclaredFields()獲取更多字段,包括公共、受保護、默認(包)訪問和私有字段,但不包括繼承字段。對應的,getDeclaredMethods()用來獲取更多方法。示例如下:

package com.cmower.java_demo.fifteen;    import java.lang.reflect.Field;  import java.lang.reflect.Method;    class Author {      private String pen_name;      private String real_name;  }    class Writer extends Author {      private String honour;        private void makeMoney() {          System.out.println("很多很多錢");      }  }    public class Test {      public static void main(String[] args) {            Class<Writer> c4 = Writer.class;          System.out.println(c4.getName());            try {              Writer wangsan = c4.newInstance();              System.out.println(wangsan);                Field[] fields = c4.getDeclaredFields();              for (Field field : fields) {                  System.out.println(field.getName());              }                Method[] methods = c4.getDeclaredMethods();              for (Method method : methods) {                  System.out.println(method.getName());              }          } catch (InstantiationException | IllegalAccessException e1) {              e1.printStackTrace();          }        }  }

上面的例子其實涉及到了反射,Field、Method(還有例子中未提到的Constructor)都來自java.lang.reflect類庫。Class類與java.lang.reflect類庫一起對反射的概念進行了支持。

有時候,我們需要從磁盤文件或網絡文件中讀取一串位元組碼,並把它轉換成一個類,這時候就需要用到反射。最常見的典型例子就是將一串JSON字符串(在網絡傳輸中最初的形態可能是位元組數組)反射為對應類型的對象。

阿里巴巴提供的FastJSON提供了 toJSONString()parseObject() 方法來將 Java 對象與 JSON 相互轉換。調用toJSONString方法即可將對象轉換成 JSON 字符串,parseObject 方法則反過來將 JSON 字符串轉換成對象。FastJSON的內部其實用的就是反射機制。

package com.cmower.common.util;    import java.io.UnsupportedEncodingException;    import org.apache.commons.logging.Log;  import org.apache.commons.logging.LogFactory;    import com.alibaba.fastjson.JSON;    @SuppressWarnings("all")  public class JsonUtil {      private static Log logger = LogFactory.getLog("json");        public static byte[] objectToByte(Object obj) throws UnsupportedEncodingException {          String jsonStr = JSON.toJSONString(obj);          logger.debug("序列化後數據:" + jsonStr);          return jsonStr.getBytes("UTF-8");      }        public static <T> T byteToObject(byte[] data, Class<T> obj) throws UnsupportedEncodingException {          String objectString = new String(data, "UTF-8");          logger.debug("反序列化後數據 : " + objectString);          return JSON.parseObject(objectString, obj);      }        public static <T> Object stringToObject(String data, Class<T> obj) throws UnsupportedEncodingException {          logger.debug("反序列化後數據 : " + data);          return JSON.parseObject(data, obj);      }  }

05 總結

為了完成這篇文章,我特意和青苗谷的一名技術專家聊了聊,問他了幾個很傻逼的問題:「『運行時』是什麼意思?是站在Java虛擬機的角度,還是程序員的角度?」

他給了我很好的解釋和啟發,我不由覺得非常的慚愧,作為一名年紀頗長的Java學習者,竟然對理論知識薄弱到令人髮指的地步——不知道你是否也有這樣的困惑?

但寫作的好處就在於此,在向讀者解釋「Java如何在運行時識別類型信息」的過程中,我的思路逐漸地清晰了起來——這真是一個自我提升的好辦法!