JAVA反射概念及使用詳解 一、什麼是反射? 反射:框架設計的靈魂 框架:半成品軟體。可以在框架的基礎上進行軟體開發,簡化編碼 反射:將類的各個組成部分封裝為其他對象,這就是反射機制 好處: 可以在程式運行過程中,操作這些對象。 可以解耦,提高程式的可擴展性。 圖片來源https://b ...
JAVA反射概念及使用詳解
一、什麼是反射?
反射:框架設計的靈魂
框架:半成品軟體。可以在框架的基礎上進行軟體開發,簡化編碼
反射:將類的各個組成部分封裝為其他對象,這就是反射機制
好處:
可以在程式運行過程中,操作這些對象。
可以解耦,提高程式的可擴展性。
圖片來源https://blog.csdn.net/qsbbl/article/details/85801571
定義:JAVA反射機制是在運行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個對象,都能夠調用它的任意一個方法,這種動態獲取、調用對象方法的功能稱為java語言的反射機制。
反射是通過Class對象(位元組碼文件),來知道某個類的所有屬性和方法。也就是說通過反射我們可以獲取構造器,對象,屬性,方法(原本不知道)
不像現在這個類我們能看見,之後在JAVA框架中,很多類我們是看不見的,不能直接用類名去獲取對象,只能通過反射去獲取。
圖片來源https://blog.csdn.net/sinat_38259539/article/details/71799078
二、獲取Class對象的三種方式:
要想使用反射,必須先得到代表的位元組碼的Class對象,Class類用於表示.class文件(位元組碼)
1.通過該類的對象去獲取到對應的Class對象(基本不用它)
//第一種方式: student--->Class對象 通過getClass()方法
//Student是一個空類
Student student = new Student();
//這裡我們就省去泛型了,Class<?>
Class stuClass = student.getClass(); //獲取到了對應的Class對象
System.out.println(stuClass);
System.out.println(stuClass.getName()); //獲取Class對象的名字
輸出:
class fanshe.Student
fanshe.Student
但是需要註意的是,第一種我們基本不用,這裡顯然和反射機制相悖(你有類對象 student 還去用反射獲取Class類對象幹嘛,多此一舉)
2.通過類名.class靜態屬性獲取(比較簡單)
//第二種方式: 每個類創建後 都會有一個預設的靜態的class屬性 用於返回該類的class對象
//需要註意的是: 任何數據類型(包括基本數據類型)都有“靜態”的class屬性
Class stuClass2 = Student.class;
System.out.println("是否為同一個class對象?"+(stuClass==stuClass2));
結果:true
這裡需要註意的是,這種方式雖然比較簡單,但是需要導包,不然會編譯錯誤(對比第三種,全限定類名方式)
3.通過Class類中的靜態方法 forName()方法獲取(最為常見)
//第三種方式: Class.forName("fanshe.Student"); 註意參數一定為該類的全限定類名
try {
Class stuClass3 = Class.forName("fanshe.Student");
//System.out.println(stuClass3); 輸出仍然是class fanshe.Student
System.out.println("是否為同一個class對象?"+(stuClass3==stuClass2));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
結果:true
結論:同一個位元組碼文件(*.class)在一次程式運行過程中,只會被載入一次,不論通過哪一種方式獲取的Class對象都是同一個。
二、通過Class對象獲取到該類的構造器:
獲取到Class對象後,我們就可以通過Class對象獲取到該類的構造器了。有很多方法,以下逐一介紹:
首先創建一個Class對象
Class stuClass = Student.class;//我們採用了第二種方式
這個是我的Student類,為了演示構造了多個構造器
public class Student {
//通過多個構造器不同的修飾符 不同的形參列表
Student(String name) {
System.out.println("用default修飾的Student的含有一個String參數的構造器:"+name);
}
public Student() {
System.out.println("用public修飾的Student的無參構造器");
}
public Student(String name,int age) {
System.out.println("用public修飾的Student的含有兩個參數的構造器:"+name+age);
}
public Student(boolean sex) {
System.out.println("用public修飾的Student的含有一個參數的構造器:"+sex);
}
protected Student(int age) {
System.out.println("用protected修飾的Student的含有一個參數的構造器:"+age);
}
private Student(String name,int age,boolean sex) {
System.out.println("用private修飾的Student的含有三個參數的構造器:"+name+age+sex);
}
}
1.getDeclaredConstructors()
返回 Constructor 對象的一個數組,這些對象反映此 Class 對象表示的類聲明的所有構造方法
Constructor[] conArray01 = stuClass.getDeclaredConstructors();
for (Constructor constructor : conArray01) {
System.out.println(constructor);
}
//輸出結果:輸出了所有的構造器
private fanshe.Student(java.lang.String,int,boolean)
protected fanshe.Student(int)
public fanshe.Student(boolean)
public fanshe.Student(java.lang.String,int)
public fanshe.Student()
fanshe.Student(java.lang.String)
2.getConstructors()
返回一個包含某些 Constructor 對象的數組,這些對象反映此 Class 對象所表示的類的所有公共(public)構造方法。
Constructor[] consArray02 = stuClass.getConstructors();
for (Constructor constructor : consArray02) {
System.out.println(constructor);
}
//輸出的是所有的public修飾的構造方法:
//public fanshe.Student(boolean)
//public fanshe.Student(java.lang.String,int)
//public fanshe.Student()
3.getConstructor(Class<?>... parameterTypes)
返回一個 Constructor 對象,它反映此 Class 對象所表示的類的指定公共(public)構造方法。
//參數為長度可變的形參:獲取指定的構造器 參數為指定構造器的形參類型對應的class對象
//用public修飾的構造器
Constructor con0 = stuClass.getConstructor(null); //無參的 無參構造器參數就是null或者空,等同於stuClass.getConstructor()
Constructor con = stuClass.getConstructor(boolean.class); //boolean的
System.out.println(con0);
System.out.println(con);
//輸出:
//public fanshe.Student()
//public fanshe.Student(boolean)
註意,輸出的都是public類型的,也只能是public類型的。
4.getDeclaredConstructor(Class<?>... parameterTypes)
返回一個 Constructor 對象,該對象反映此 Class 對象所表示的類或介面的指定構造方法。
Constructor con1 = stuClass.getDeclaredConstructor(int.class);
System.out.println(con1);
Constructor con2 = stuClass.getDeclaredConstructor(String.class,int.class,boolean.class);
System.out.println(con2);
//輸出:
//protected fanshe.Student(int)
//private fanshe.Student(java.lang.String,int,boolean)
總結:
①有參數的3和4,返回的都是一個構造器(好理解,構造器重載要求參數列表不一樣)。沒參數的1和2,返回的都是Constructor類型的數組。你可以這樣記:方法名加了s,例getConstructors(), 的返回都是多個構造器,沒有s的都是一個。
②方法名包含了Declared返回的構造器訪問許可權修飾符沒有限制,而沒有包含Declared例getConstructors(),返回的都是public修飾的構造器。
簡而言之:加s的比沒加s的返回的構造方法多,加Declared比沒加的多,全加的(getDeclaredConstructors())最多
那麼獲取了構造器如何去創建對象呢?
三、通過獲取到的構造器創建對象:
//這裡我們需要捕獲一下異常
//因為我們的構造器是私有的,不能在其他類去創建,所以我們用了setAccessible()這個方法,從而可以創建
/*
private Student(String name,int age,boolean sex){
System.out.println("用private修飾的Student的含有三個參數的構造器:"+name+age+sex);
}*/
try {
con2.setAccessible(true); //忽略構造器的訪問修飾符,解除私有限定
Object object = con2.newInstance("張三",10,true); //這句話就相當於new Student("張三",10,true);
Student student = (Student) object; //指向子類對象的父類引用重新轉為子類引用
} catch (Exception e) {
e.printStackTrace();
}
//輸出:用private修飾的Student的含有三個參數的構造器:張三10true
註意:
xxx.setAccessible(true) 是為瞭解除私有限定,以後會經常出現,之後就不多加解釋了
四、 通過Class對象獲取成員變數
以下方法有很多異常需要拋出,我就省略了
先定義一個Teacher類:
public class Teacher {
public Teacher() {
}
public String name;
protected int age;
boolean sex;
private String address;
@Override
public String toString() {
return "Teacher [name=" + name + ", age=" + age + ", sex=" + sex + ", address=" + address + "]";
}
}
Class teaClass = Class.forName("fanshe.Teacher"); //創建Class對象(第三種方式)
//獲取Teacher類中的所有的public欄位
Field[] f = teaClass.getFields();
for (Field field : f) {
System.out.println(field);
}
//獲取Teacher類中的所有的欄位(包含各種訪問修飾符)
System.out.println("===========所有的欄位=======================>");
Field[] df = teaClass.getDeclaredFields();
for (Field field : df) {
System.out.println(field);
}
System.out.println("===========公共的特定的欄位=======================>");
// 獲取公共的特定的欄位
Field field = teaClass.getField("name");
System.out.println(field);
System.out.println("===========特定的欄位=======================>");
// 獲取特定的欄位
Field field2 = teaClass.getDeclaredField("age");
System.out.println(field2);
/**
* 為欄位設置具體的值
* 參數一:該類的對象
* 參數二:為特定的屬性賦值
*/
Object object = teaClass.getConstructor().newInstance();//相當於Object object = new Teacher();
field.set(object, "張三豐");
System.out.println("名稱為:"+((Teacher)object).name);
輸出結果:
public java.lang.String fanshe.Teacher.name //所有public欄位
===========所有的欄位=======================>
public java.lang.String fanshe.Teacher.name
protected int fanshe.Teacher.age
boolean fanshe.Teacher.sex
private java.lang.String fanshe.Teacher.address
===========公共的特定的欄位=======================>
public java.lang.String fanshe.Teacher.name
===========特定的欄位=======================>
protected int fanshe.Teacher.age
名稱為:張三豐
記憶方法同上,這裡就不贅述了。
五、通過Class對象獲取到該類的方法
首先創建了一個Employee類:
public class Employee {
public Employee() {}
public String name;
protected double money;
String address;
private long number;
protected void getDayMoney(String name) {
System.out.println("我是Employee受保護的獲取日薪的方法,有一個參數為:"+name);
}
public void getweekMoney(String name,double money) {
System.out.println("我是Employee公有的獲取周薪的方法,沒有參數..");
}
void getMonthMoney() {
System.out.println("我是Employee預設的獲取月薪的方法,沒有參數..");
}
private void getYearMoney(int age) {
System.out.println("我是Employee私有的的獲年薪月薪的方法,有一個參數為:"+age);
}
public static void main(String[] args) {
System.out.println("Employee中的main()方法執行了...");
System.out.println(Arrays.toString(args));
}
@Override
public String toString() {
return "Employee [name=" + name + ", money=" + money + ", address=" + address + ", number=" + number + "]";
}
}
通過Class對象去獲取該類的方法:
Class<?> emplClass = Class.forName("fanshe.Employee");
//1.獲取所有的公共的方法 (包含父類的方法)
Method[] methods = emplClass.getMethods();
for (Method method : methods) {
System.out.println(method);
}
//輸出:
public java.lang.String fanshe.Employee.toString()
public void fanshe.Employee.getweekMoney(java.lang.String,double)
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
註意:
1.上面的方法都是public公共的;
2.會將父類(Object)類中的公共方法也輸出。
//2.獲取該類中的所有方法,以數組形式返回:
Method[] methods = emplClass.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method);
}
//輸出:
public java.lang.String fanshe.Employee.toString()
public void fanshe.Employee.getweekMoney(java.lang.String,double)
private void fanshe.Employee.getYearMoney(int)
void fanshe.Employee.getMonthMoney()
protected void fanshe.Employee.getDayMoney(java.lang.String)
//3.獲取特定的公有的方法: 只能是公共的
//參數一:要獲取的方法的名稱,參數二:方法對應的形參列表的類型
Method method = emplClass.getMethod("getweekMoney",new Class[]{String.class,double.class});
System.out.println(method); //輸出:public void fanshe.Employee.getweekMoney(java.lang.String,double)
//4.獲取特定的方法: 可以是私有的
Method method2 = emplClass.getDeclaredMethod("getYearMoney", int.class);
System.out.println(method2); //輸出:private void fanshe.Employee.getYearMoney(int)
接下來我準備獲取private void getYearMoney(int age) {}方法,然後去執行這個私有方法。
//emplClass對應的類的對象
Object obj = emplClass.getConstructor().newInstance();
//可以通過Employee類對象去直接調用非私有方法,我這裡將測試類和Employee類放在了同一個包下,所有可以訪問預設方法(包限定方法)
Employee employee = (Employee)obj;
employee.getMonthMoney();
//參數一:要調用的對象 參數二:方法具體要求傳遞的值 返回值:為調用該方法後的返回值所對應的對象,如果沒有返回值(void),則對象為null
method2.setAccessible(true);
Object result = method2.invoke(obj, 20); //執行method2方法對應的代碼
System.out.println(result);
//輸出:
/*
我是Employee預設的獲取月薪的方法,沒有參數..
我是Employee私有的的獲年薪月薪的方法,有一個參數為:20
null
*/
調用Employee類的主方法(main):
Class<?> class1 = Class.forName("fanshe.Employee");
Method methodMain = class1.getMethod("main", String[].class);
//參數一:對象類型 null 當調用的方法為靜態時,此時第一個參數可以為null
//方式一:
Object object = methodMain.invoke(null, (Object)new String[]{"a","b","c","d"});
//方式二:
Object object = methodMain.invoke(null, new Object[] {new String[]{"a","b","c","d"}});
System.out.println(object);
//輸出:
Employee中的main()方法執行了... //這裡是main方法里的輸出
[a, b, c, d] //main方法里的輸出調用了toString
Employee中的main()方法執行了...
[a, b, c, d]
null //main方法返回類型為void,所以這裡返回值為null
六、通過Method對象調用指定方法
下麵我們對Method中的invoke()方法進行詳解。
public Object invoke(Object obj, Object... args)
對帶有指定參數的指定對象調用由此 Method 對象表示的底層方法。
如果底層方法是靜態的,那麼可以忽略指定的 obj 參數。該參數可以為 null。
如果底層方法所需的形參數為 0,則所提供的 args 數組長度可以為 0 或 null。
如果底層方法是靜態的,並且尚未初始化聲明此方法的類,則會將其初始化。
如果方法正常完成,則將該方法返回的值返回給調用者;如果該值為基本類型,則首先適當地將其包裝在對象中。但是,如果該值的類型為一組基本類型,則數組元素不 被包裝在對象中;換句話說,將返回基本類型的數組。如果底層方法返回類型為 void,則該調用返回 null。
參數:
obj - 從中調用底層方法的對象
args - 用於方法調用的參數
返回:
使用參數 args 在 obj 上指派該對象所表示方法的結果
拋出:拋出的異常就不列舉了,具體可以自己查API文檔
下麵我單獨創建了一個InvokeTest類去解釋說明invoke方法的使用:
public class InvokeTest {
public static void main(String[] args) throws Exception {
Class classType = InvokeTest.class;
Object invoketest = classType.getConstructor().newInstance();
//獲取Method類對象
Method m = classType.getMethod("add", new Class[] {int.class,int.class});
/**
* Method的invoke(Object obj,Object[] args) 該方法接收的參數必須為對象
* 如果參數為基本數據類型,使用相應的包裝類對象 返回的結果總是一個對象(其結果代表調用該方法後的返回值)
*/
//如果add方法是靜態方法,那麼可以這樣寫:Object object = m.invoke(null, new Integer(10), new Integer(20));
Object object = m.invoke(invoketest, new Integer(10), new Integer(20));
System.out.println(object);
}
public int add(int param1,int param2) {
return param1+param2;
}
}
//輸出:30
七、反射的使用
1.通過反射運行配置文件內容:
通過這種方式,當我們需要訪問其他類的時候,不需要改動源碼,利用反射,直接修改配置文件即可。
public class FileDemo {
public static void main(String[] args) throws Exception {
Class aClass = Class.forName(getValue("className"));
Method m = aClass.getMethod(getValue("methodName"));
m.invoke(aClass.getConstructor().newInstance());
}
//這個方法用於接收配置文件中與key所對應的value值
public static String getValue(String key) throws Exception {
Properties pro = new Properties(); //獲取配置文件的對象
FileReader reader = new FileReader("prop.txt");
pro.load(reader); //將流載入到配置文件對象中
//從輸入流中讀取屬性列表(鍵和元素對)。通過對指定的文件(比如說上面的 prop.txt 文件)進行裝載來獲取該文件中的所有鍵 - 值對。以供 getProperty ( String key) 來搜索。
reader.close();
return pro.getProperty(key);//用指定的鍵在此屬性列表中搜索屬性。也就是通過參數 key ,得到 key 所對應的 value。
}
}
//prop.txt文件中的內容
className=fanshe.file.Apple
methodName=taste
//Apple類
public class Apple {
public void taste() {
System.out.println("蘋果真好吃啊...");
}
}
//輸出:
蘋果真好吃啊...
總結一下步驟:
1.通過Class類中的forName靜態方法獲取Apple類的Class類對象;
2.通過Class類對象獲取Method類對象,參數傳入要調用方法的方法名;
3.通過Method類的invoke非靜態方法,調用這個方法,參數傳入類對象和調用的方法的參數對象(我這裡的test方法沒有參數,所有invoke參數只有Apple類的對象)。
Java中有個比較重要的類Properties(Java.util.Properties),主要用於讀取Java的配置文件。
上面的例子是獲取text文件中的,而配置文件(.properties文件)讀取方式略有不同:
public class ReflectTest {
public static void main(String[] args) throws Exception {
//1.載入配置文件
//1.1創建Properties對象
Properties pro = new Properties();
//1.2獲取到class目錄下的配置文件
ClassLoader classLoader = ReflectTest.class.getClassLoader();
InputStream is = classLoader.getResourceAsStream("pro.properties");
//1.3載入配置文件
pro.load(is);
//2.獲取到配置文件中定義的數據
String className = pro.getProperty("className");
String methodName = pro.getProperty("methodName");
//3.有了全限定類名 有了方法名(參數) 完全可以通過反射調用該方法
//3.1:獲取到該類的Class對象
Class cls = Class.forName(className);
//3.2:獲取到該類的實例對象
Object object = cls.getConstructor().newInstance();
//3.3:獲取到該類特定的方法對象
Method method = cls.getMethod(methodName);
//4.執行方法
method.invoke(object);
}
}
//pro.properties文件內容,這個配置文件可以包->右鍵->New->File->File name:pro.properties
className=bean.Student
methodName=study
Student類和運行結果這裡就不列舉了,和上面的差不多
2.通過反射越過泛型檢查:
我們的目的是:向String泛型集合中添加一個Integer對象
List<String> list = new ArrayList<>();
list.add("我愛Java");
list.add("成功上岸");
//list.add(100); 如果直接add直接會編譯報錯
//1.獲取到List類的Class對象
Class listClass = list.getClass();
Method method = listClass.getMethod("add", Object.class);
method.invoke(list, 100);
for (Object object : list) {
System.out.println(object);
}
/*輸出:
我愛Java
成功上岸
100
*/
如有錯誤,歡迎指點