一、類的加載時機
當程序要使用某個類時,如果該類還未被加載到內存中,系統會通過加載,連接,初始化三步來實現對這個類進行初始化:
(1)加載:
就是指將class文件讀入內存,併爲之創建一個Class對象。任何類被使用時系統都會建立一個Class對象。
(2)連接:
驗證是否有正確的內部結構,並和其他類協調一致,準備 負責爲類的靜態成員分配內存,並設置默認初始化值
(3)初始化:
初始化成員變量等等
類的加載時機分爲以下幾種:
(1)創建類的實例
(2)訪問類的靜態變量,或者爲靜態變量賦值
(3)調用類的靜態方法
(4)初始化某個類的子類
(5)使用反射方式來強制創建某個類或接口對應的java.lang.Class對象
二、什麼是反射
衆所周知,創建一個對象分爲三個階段:
1.源文件階段 .java的文件
2.字節碼階段 .class
3.創建對象階段 new 對象名稱
java的反射機制就是指:
在運行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;
對於任意一個對象,都能夠調用它的任意一個方法和屬性;
這種動態獲取的信息以及動態調用對象的方法的功能稱爲java語言的反射機制。想要使用反射,就必須得要獲取字節碼文件
用途
在日常的第三方應用開發過程中,經常會遇到某個類的某個成員變量、方法或是屬性是私有的或是隻對系統應用開放,這時候就可以利用Java的反射機制通過反射來獲取所需的私有成員或是方法。
接下來通過代碼來進行演示,首先我們創建一個Person類:
package com.zhangyi.demo;
/**
* @author zhangyi
*/
public class Person {
public String name;
public Integer age;
private String food;
//無參的構造方法
public Person() {
}
//有參的構造方法
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public void show() {
System.out.println("我是" + this.name + ",今年" + this.age + "歲,我今天吃了"+this.food);
}
private void eat(String food){
System.out.println("我今天吃了"+ food);
}
}
之前提到,想要使用反射,就必須得要獲取字節碼文件,獲取類的字節碼文件有以下三種方式,以Person類爲例,通過Junit4單元測試來進行演示:
public class PersonTest {
@Test
public void test1() throws ClassNotFoundException {
/*獲取類的字節碼的三種方式*/
/* 第一種 Class類中靜態方法forName() 一般用於讀取配置文件*/
Class<?> clazz1 = Class.forName("com.zhangyi.demo.Person");
/* 第二種靜態屬性class 一般用於當作靜態方法的鎖對象*/
Class<?> clazz2 = Person.class;
/*第三種 Object類的getClass()方法 一般用於判斷兩個對象是否是同一個字節碼文件*/
Person person = new Person();
Class<?> clazz3 = person.getClass();
/*判斷三者是否相等 */
System.out.println(clazz1 == clazz2);
System.out.println(clazz2 == clazz3);
System.out.println(clazz1 == clazz3);
}
}
輸出結果:
true
true
true
因此,我們可以看到,通過這三種方式創建Person類的字節碼文件都是一樣的。
三、反射的部分用法
(一)通過字節碼創建對象
1.通過無參的構造器創建對象:
@Test
public void test2() throws Exception {
/* 1.獲取字節碼*/
Class<?> clazz = Class.forName("com.zhangyi.demo.Person");
/*2.調用字節碼的newInstance()方法*/
Person person = (Person) clazz.newInstance();
person.name = "張儀";
person.age = 21;
//food爲私有的變量,沒提供get、set方法,不可直接調用,後續會通過反射機制進行暴力賦值
person.show();
}
輸出結果:
我是張儀,今年21歲,今天吃了null
** 2.通過有參的構造器創建對象**
@Test
public void test3() throws Exception {
/* 1.獲取字節碼的構造器 clazz.getConstructor(type.class) 因爲在反射階段操作的都是字節碼,不知道具體的類型,只有在創建對象的時候纔去給實際參數*/
Class<?> clazz = Class.forName("com.zhangyi.demo.Person");
Constructor constructor = clazz.getConstructor(String.class, Integer.class);
/* 2.通過構造器創建對象 調用構造器的newInstance方法並傳入參數*/
Person person = (Person) constructor.newInstance("張儀",21);
person.show();
}
輸出結果:
我是張儀,今年21歲,今天吃了null
(二)獲取字段
1.獲取公共的字段:
@Test
public void test4() throws Exception{
/*1.根據反射創建對象*/
Class<?> clazz = Class.forName("com.zhangyi.demo.Person");
Person person = (Person) clazz.newInstance();
/*2.獲取公共的字段*/
Field name = clazz.getField("name");
Field age = clazz.getField("age");
/*3.設置person對象的屬性*/
name.set(person,"張儀");
age.set(person,21);
person.show();
}
輸出結果:
我是張儀,今年21歲,今天吃了null
2.獲取私有的字段
@Test
public void test5() throws Exception{
/*1.根據反射創建對象*/
Class<?> clazz = Class.forName("com.zhangyi.demo.Person");
Person person = (Person) clazz.newInstance();
/*2.獲取公共的字段 並且設置person對象的相應屬性*/
Field name = clazz.getField("name");
Field age = clazz.getField("age");
name.set(person,"張儀");
age.set(person,21);
/*3.通過暴力反射獲取私有的字段 並且通過setAccessible方法去除私有權限 */
Field food = clazz.getDeclaredField("food");
food.setAccessible(true);
//賦值
food.set(person,"麪條");
person.show();
}
輸出結果:
我是張儀,今年21歲,今天吃了麪條
(三)獲取方法
1.獲取公共的方法:
@Test
public void test6() throws Exception{
/*1.根據反射創建對象*/
Class<?> clazz = Class.forName("com.zhangyi.demo.Person");
Person person = (Person) clazz.newInstance();
/*2.獲取公共的方法*/
Method show = clazz.getMethod("show");
/*3.調用invoke方法 若無參數則可以不用傳值*/
show.invoke(person);
}
輸出結果:
我是null,今年null歲,今天吃了null
2.獲取私有的方法:
@Test
public void test7() throws Exception{
/*1.根據反射創建對象*/
Class<?> clazz = Class.forName("com.zhangyi.demo.Person");
Person person = (Person) clazz.newInstance();
/*2.通過暴力反射獲取私有的方法 並且通過setAccessible方法去除私有權限 若有參數記得傳參*/
Method eat = clazz.getDeclaredMethod("eat",String.class);
eat.setAccessible(true);
/*3.調用invoke方法 若無參數則可以不用傳值,有則記得傳值*/
eat.invoke(person,"麪條");
}
輸出結果:
我今天吃了麪條
(四)經典小案例
需求:如何繞過數組泛型檢測
科普一個小知識點:java的泛型又被稱之爲假泛型,java的泛型僅在編譯期有效,在運行期則會被擦除,即編譯的.class文件中並沒有泛型,也就是說所有的泛型參數類型在編譯後都會被清除掉。這就是所謂的類型擦除。感興趣可以自行編譯一下查看class文件。
需求實現代碼如下:
@Test
public void test8() throws Exception {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
// list.add("添加字符串會報錯");
/*獲取ArrayList的字節碼*/
Class<?> clazz = Class.forName("java.util.ArrayList");
/*獲取add方法*/
Method add = clazz.getMethod("add", Object.class);
/*此時調用方法add 給list添加字符串*/
add.invoke(list,"此時添加字符串不會報錯");
System.out.println(list);
}
輸出結果:
[1, 此時添加字符串不會報錯]
以上就是我的部分見解。