導讀
在之後的幾篇文章,我會講解我自己的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);
}
測試結果:
這是通過構造器創建的對象。但是注意的是,形參類型和和參數值的位數一定要相等,否則,就會報出錯誤的。
總結
爲什麼寫這篇文章,前面也說了,很多框架都用到了反射和RTTI。但是,我們的平常的工作,一般以業務爲主。往往都是使用別人封裝好的框架,比如spring、hibernate、mybatis、beanutils等框架。所以,我們不大會關注反射,但是,你如果想要往更高的方向去攀登,還是要把基礎給打撈。否則,基礎不穩,爬得越高,摔得越重。
我會以後的篇章中,通過介紹我寫的spring、hibernate框架,來講解更好地講解反射。