天天看點

Java如何在運作時識别類型資訊?

在Java中,并不是所有的類型資訊都能在編譯階段明确,有一些類型資訊需要在運作時才能确定,這種機制被稱為RTTI,英文全稱為Run-Time Type Identification,即運作時類型識别,有沒有一點“知行合一”的味道?運作時類型識别主要由Class類實作。

在 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如何在運作時識别類型資訊”的過程中,我的思路逐漸地清晰了起來——這真是一個自我提升的好辦法!

上一篇:Java異常處理:給程式罩一層保險

下一篇:Java枚舉:小小enum,優雅而幹淨

微信掃描左側二維碼,關注作者的微信公衆号:「

沉默王二

背景回複“

666

”即可擷取一份 500G 的高清教學視訊,并且已經分門别類,可以按需下載下傳,速去!

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。

如果覺得還有幫助的話,可以點一下右下角的【推薦】。