框架與RTTI的關係,RTTI與反射之間的關係

導讀

在之後的幾篇文章,我會講解我自己的hibernate、spring、beanutils框架,但講解這些框架之前,我需要講解RTTI和反射。

工作將近一年了,我們公司項目所使用的框架是SSH,或者,其他公司使用的是SSM框架。不管是什麼樣的框架,其都涉及到反射。那麼,什麼是反射?我們在生成對象時,事先並不知道生成哪種類型的對象,只有等到項目運行起來,框架根據我們的傳參,才生成我們想要的對象。

比如,我們從前端調用後端的接口,查詢出這個人的所有項目,我們只要傳遞這個人的id即可。當然,數據來源於數據庫,那麼,問題來了,數據是怎麼從持久態轉化成我們想要的順時態的?這裏面,就涉及到了反射。但是,一提到反射,我們勢必就提到RTTI,即運行時類型信息(runtime Type Infomation)。


RTTI


  • po類
/**
 * Created By zby on 16:53 2019/3/16
 */
@AllArgsConstructor
@NoArgsConstructor
public class Pet {

    private String name;

    private String food;

    public void setName(String name) {
        this.name = name;
    }

    public void setFood(String food) {
        this.food = food;
    }

    public String getName() {
        return name;
    }

    public String getFood() {
        return food;
    }
}

/**
 * Created By zby on 17:03 2019/3/16
 */
public class Cat extends Pet{

    @Override
    public void setFood(String food) {
        super.setFood(food);
    }
}

/**
 * Created By zby on 17:04 2019/3/16
 */
public class Garfield extends Cat{

    @Override
    public void setFood(String food) {
        super.setFood(food);
    }
}


/**
 * Created By zby on 17:01 2019/3/16
 */
public class Dog extends Pet{

    @Override
    public void setFood(String food) {
        super.setFood(food);
    }
}

以上是用來說明的persistent object類,也就是,我們在進行pojo常用的javabean類。其有繼承關係,如下圖:

繼承關係圖

  • 展示信息

如一下代碼所示,方法eatWhatToday有兩個參數,這兩個參數一個是接口類,一個是父類,也就是說,我們並不知道打印出的是什麼信息。只有根據接口的實現類來和父類的子類,來確認打印出的信息。這就是我們輸的運行時類型信息,正因爲有了RTTI,java纔有了動態綁定的概念。

/**
 * Created By zby on 17:05 2019/3/16
 */
public class FeedingPet {

    /**
     * Created By zby on 17:05 2019/3/16
     * 某種動物今天吃的是什麼
     *
     * @param baseEnum 枚舉類型 這裏表示的時間
     * @param pet      寵物
     */
    public static void eatWhatToday(BaseEnum baseEnum, Pet pet) {
        System.out.println( pet.getName() + "今天" + baseEnum.getTitle() + "吃的" + pet.getFood());
    }
    
}

  • 測試類
 @Test
public void testPet(){
    Dog dog=new Dog();
    dog.setName("寵物狗京巴");
    dog.setFood(FoodTypeEnum.FOOD_TYPE_BONE.getTitle());

    FeedingPet.eatWhatToday(DateTypeEnum.DATE_TYPE_MORNING,dog);

    Garfield garfield=new Garfield();
    garfield.setName("寵物貓加菲貓");
    garfield.setFood(FoodTypeEnum.FOOD_TYPE_CURRY.getTitle());
    FeedingPet.eatWhatToday(DateTypeEnum.DATE_TYPE_MIDNIGHT_SNACK,garfield);
}

打印出的信息爲:

打印信息

那麼,這和反射有什麼關係呢?


反射獲取當前類信息

正如上文提到的運行時類型信息,那麼,類型信息在運行時是如何表示的?此時,我們就想到了Class這個特殊對象。見名知其意,即類對象,其包含了類的所有信息,包括屬性、方法、構造器。

我們都知道,類是程序的一部分,每個類都有一個Class對象。每當編寫並且執行了一個新類,就會產生一個Class對象(更恰當地說,是被保存在一個同名的.class文件中)。爲了生成這個類的對象,運行當前程序的jvm將使用到類加載器。jvm首先調用bootstrap類加載器,加載核心文件,jdk的核心文件,比如Object,System等類文件。然後調用plateform加載器,加載一些與文件相關的類,比如壓縮文件的類,圖片的類等等。最後,才用applicationClassLoader,加載用戶自定義的類。

加載當前類信息

反射正式利用了Class來創建、修改對象,獲取和修改屬性的值等等。那麼,反射是怎麼創建當前類的呢?


  • 第一種,可以使用當前上下文的類路徑來創建對象,如我們記載jdbc類驅動的時候,如以下代碼:
/**
 * Created By zby on 18:07 2019/3/16
 * 通過上下文的類路徑來加載信息
 */
public static Class byClassPath(String classPath) {
    if (StringUtils.isBlank(classPath)) {
        throw new RuntimeException("類路徑不能爲空");
    }
    classPath = classPath.replace(" ", "");
    try {
        return Class.forName(classPath);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    return null;
}

  • 第二種,通過類字面常量,這種做法非常簡單,而且更安全。因爲,他在編譯時就會受到檢查,我們不需要將其置於try catch的代碼快中,而且,它根除了對forName的方法調用,所以,更高效。這種是spring、hibernate等主流框架使用的。如代碼所示:
 /**
 * Created By zby on 18:16 2019/3/16
 * 通過類字面常量加載當前類的信息
 */
public static void byClassConstant() {
    System.out.println(Dog.class);
}

  • 第三種,是通過對象來創建當前類,這種會在框架內部使用。
/**
* Created By zby on 18:17 2019/3/16
* 通過類對象加載當前類的信息
*/
public static Class byCurrentObject(Object object) {
    return object.getClass();
}

反射創建當前類對象

我們創建當前對象,一般有兩種方式,一種是通過clazz.newInstance();這種一般是無參構造器,並且創建對對象後,可以獲取其屬性,通過屬性賦值和方法賦值,如如代碼所示:


  • 第一種,通過clazz.newInstance()創建對象
/**
 * Created By zby on 18:26 2019/3/16
 * 普通的方式創建對象
 */
public static <T> T byCommonGeneric(Class clazz, String name, BaseEnum baseEnum) {
    if (null == clazz) {
        return null;
    }
    try {
        T t = (T) clazz.newInstance();
        
        //通過屬性賦值,getField獲取公有屬性,獲取私有屬性
        Field field = clazz.getDeclaredField("name");
        //跳過檢查,否則,我們沒辦法操作私有屬性
        field.setAccessible(true);
        field.set(t, name);
        
        //通過方法賦值
        Method method1 = clazz.getDeclaredMethod("setFood", String.class);
        method1.setAccessible(true);
        method1.invoke(t, baseEnum.getTitle());

        return t;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

測試:
 @Test
public void testCommonGeneric() {
    Dog dog= GenericCurrentObject.byCommonGeneric(Dog.class,
            "寵物狗哈士奇", 
            FoodTypeEnum.FOOD_TYPE_BONE);
    FeedingPet.eatWhatToday(DateTypeEnum.DATE_TYPE_NOON,dog);
}

叔叔出結果爲:

輸出結果爲

你會發現一個神奇的地方,就是名字沒有輸出來,但我們寫了名字呀,爲什麼沒有輸出來?因爲,dog是繼承了父類Pet,當我們在創建子類對象時,首先,會加載父類未加載的構造器、靜態代碼塊、靜態屬性、靜態方法等等。但是,Dog在這裏是以無參構造器加載的,當然,同時也通過無參構造器的實例化了父類。我們在給dog對象的name賦值時,、並沒有給父類對象的name賦值,所以,dog的name是沒有值的。父類引用指向子類對象,就是這個意思。

如果我們把Dog類中的 @Override public void setFood(String food) {super.setFood(food); }super.setFood(food); 方法去掉,屬性food也是沒有值的。如圖所示:

寵物沒有值


  • 通過構造器創建對象
    /**
     * Created By zby on 18:26 2019/3/16
     * 普通的方式創建對象
     */
    public static <T> T byConstruct(Class clazz, String name, BaseEnum baseEnum) {
        if (null == clazz) {
            return null;
        }
//        參數類型,
        Class paramType[] = {String.class, String.class};
        try {
//          一般情況下,構造器不止一個,我們根據構器的參數類型,來使用構造器創建對象
            Constructor constructor = clazz.getConstructor(paramType);
//            給構造器賦值,賦值個數和構造器的形參個數一樣,否則,會報錯
            return (T) constructor.newInstance(name, baseEnum.getTitle());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    
    測試:
    
   @Test
    public void testConstruct() {
        Dog dog= GenericCurrentObject.byConstruct(Dog.class,
                "寵物狗哈士奇",
                FoodTypeEnum.FOOD_TYPE_BONE);
        System.out.println("輸出寵物的名字:"+dog.getName()+"\n");
        System.out.println("寵物吃的什麼:"+dog.getFood()+"\n");
        FeedingPet.eatWhatToday(DateTypeEnum.DATE_TYPE_MIDNIGHT_SNACK,dog);
    }

測試結果:

clipboard.png

這是通過構造器創建的對象。但是注意的是,形參類型和和參數值的位數一定要相等,否則,就會報出錯誤的。

總結

爲什麼寫這篇文章,前面也說了,很多框架都用到了反射和RTTI。但是,我們的平常的工作,一般以業務爲主。往往都是使用別人封裝好的框架,比如spring、hibernate、mybatis、beanutils等框架。所以,我們不大會關注反射,但是,你如果想要往更高的方向去攀登,還是要把基礎給打撈。否則,基礎不穩,爬得越高,摔得越重。

我會以後的篇章中,通過介紹我寫的spring、hibernate框架,來講解更好地講解反射。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章