1.Java反射機制
JAVA反射機制是在運行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個對象,都能夠調用它的任意一個方法和屬性;這種動態獲取的信息以及動態調用對象的方法的功能稱為java語言的反射機制。
要想解剖一個類,必須先要獲取到該類的字節碼文件對象。而解剖使用的就是Class類中的方法.所以先要獲取到每一個字節碼文件對應的Class類型的對象.
簡單的來說,反射機制指的是程序在運行時能夠獲取自身的信息。在Java中,只要給定類的名字, 那么就可以通過反射機制來獲得類的所有信息。
2.類加載的過程
Class對象的由來是將class文件讀入內存,並為之創建一個Class對象
3.Java反射的作用與解決的問題
Java中編譯類型有兩種:
- 靜態編譯:在編譯時確定類型,綁定對象即通過。
- 動態編譯:運行時確定類型,綁定對象。動態編譯最大限度地發揮了Java的靈活性,體現了多態的應用,可以減低類之間的耦合性。
Java反射是Java被視為動態(或准動態)語言的一個關鍵性質。這個機制允許程序在運行時透過Reflection APIs取得任何一個已知名稱的class的內部信息,包括其modifiers(諸如public、static等)、superclass(例如Object)、實現之interfaces(例如Cloneable),也包括fields和methods的所有信息,並可於運行時改變fields內容或喚起methods。
Reflection可以在運行時加載、探知、使用編譯期間完全未知的classes。即Java程序可以加載一個運行時才得知名稱的class,獲取其完整構造,並生成其對象實體、或對其fields設值、或喚起其methods。
一句話概括就是使用反射可以賦予jvm動態編譯的能力,否則類的元數據信息只能用靜態編譯的方式實現,例如熱加載,Tomcat的classloader等等都沒法支持。
3.1 編譯期和運行期區別
編譯期:檢查是否有語法錯誤,如果沒有就將其翻譯成字節碼文件。即.class文件。
運行期:java虛擬機分配內存,解釋執行字節碼文件。
3.2 Java 重寫(Override)與重載(Overload)
3.2.1 重寫
重寫是子類對父類的允許訪問的方法的實現過程進行重新編寫, 返回值和形參都不能改變。即外殼不變,核心重寫!
重寫的好處在於子類可以根據需要,定義特定於自己的行為。 也就是說子類能夠根據需要實現父類的方法。
重寫方法不能拋出新的檢查異常或者比被重寫方法申明更加寬泛的異常。例如: 父類的一個方法申明了一個檢查異常 IOException,但是在重寫這個方法的時候不能拋出 Exception 異常,因為 Exception 是 IOException 的父類,拋出 IOException 異常或者 IOException 的子類異常。
重寫是父類與子類之間多態性的表現,在運行時起作用(動態多態性,譬如實現動態綁定)
方法的重寫是在運行時進行的。
在面向對象原則里,重寫意味着可以重寫任何現有方法。實例如下:
class Animal{ public void move(){ System.out.println("動物可以移動"); } } class Dog extends Animal{ public void move(){ System.out.println("狗可以跑和走"); } } public class TestDog{ public static void main(String args[]){ Animal a = new Animal(); // Animal 對象 Animal b = new Dog(); // Dog 對象 a.move();// 執行 Animal 類的方法 b.move();//執行 Dog 類的方法 } }
在上面的例子中可以看到,盡管 b 屬於 Animal 類型,但是它運行的是 Dog 類的 move方法。
這是由於在編譯階段,只是檢查參數的引用類型。
然而在運行時,Java 虛擬機(JVM)指定對象的類型並且運行該對象的方法。
方法的重寫規則:
-
參數列表與被重寫方法的參數列表必須完全相同。
-
返回類型與被重寫方法的返回類型可以不相同,但是必須是父類返回值的派生類(java5 及更早版本返回類型要一樣,java7 及更高版本可以不同)。
-
訪問權限不能比父類中被重寫的方法的訪問權限更低。例如:如果父類的一個方法被聲明為 public,那么在子類中重寫該方法就不能聲明為 protected。
-
父類的成員方法只能被它的子類重寫。
-
聲明為 final 的方法不能被重寫。
-
構造方法不能被重寫。
3.2.2 重載(Overload)
重載(overloading) 是在一個類里面,方法名字相同,而參數不同。返回類型可以相同也可以不同。
每個重載的方法(或者構造函數)都必須有一個獨一無二的參數類型列表。
最常用的地方就是構造器的重載。
方法重載是在編譯時執行的。
重載規則:
被重載的方法必須改變參數列表(參數列表中的參數順序、個數、類型不一樣);
- 被重載的方法可以改變返回類型;
- 被重載的方法可以改變訪問修飾符;
- 被重載的方法可以聲明新的或更廣的檢查異常;
- 方法能夠在同一個類中或者在一個子類中被重載。
- 無法以返回值類型作為重載函數的區分標准。
實例:
public class Overloading { public int test(){ System.out.println("test1"); return 1; } public void test(int a){ System.out.println("test2"); } //以下兩個參數類型順序不同 public String test(int a,String s){ System.out.println("test3"); return "returntest3"; } public String test(String s,int a){ System.out.println("test4"); return "returntest4"; } public static void main(String[] args){ Overloading o = new Overloading(); System.out.println(o.test()); o.test(1); System.out.println(o.test(1,"test3")); System.out.println(o.test("test4",1)); } }
3.2.3 重寫與重載之間的區別
區別點 | 重載方法 | 重寫方法 |
---|---|---|
參數列表 | 必須修改 | 一定不能修改 |
返回類型 | 可以修改 | 返回類型與被重寫方法的返回類型可以不相同,但是必須是父類返回值的派生類 |
異常 | 可以修改 | 可以減少或刪除,一定不能拋出新的或者更廣的異常 |
訪問 | 可以修改 | 一定不能做更嚴格的限制(可以降低限制) |
方法重載:
1、同一個類中
2、方法名相同,參數列表不同(參數順序、個數、類型)
3、方法返回值、訪問修飾符任意
4、與方法的參數名無關
方法重寫:
1、有繼承關系的子類中
2、方法名相同,參數列表相同(參數順序、個數、類型),方法返回值相同
3、訪問修飾符,訪問范圍需要大於等於父類的訪問范圍
4、與方法的參數名無關
4.反射的使用
4.1 獲取Class對象的三種方式
- Object ——> getClass();
- 任何數據類型(包括基本數據類型)都有一個“靜態”的class屬性
- 通過Class類的靜態方法:forName(String className)(常用)
測試案例:
/** * @Author lucky * @Date 2021/12/28 16:09 */ public class ReflectTest { public static void main(String[] args) { //第一種方式獲取Class對象 Student stu1 = new Student();//這一new 產生一個Student對象,一個Class對象。 Class stuClass = stu1.getClass();//獲取Class對象 System.out.println(stuClass.getName()); //第二種方式獲取Class對象 Class stuClass2 = Student.class; System.out.println(stuClass == stuClass2);//判斷第一種方式獲取的Class對象和第二種方式獲取的是否是同一個 //第三種方式獲取Class對象 try { Class stuClass3 = Class.forName("com.ttbank.flep.core.entity.Student");//注意此字符串必須是真實路徑,就是帶包名的類路徑,包名.類名 System.out.println(stuClass3 == stuClass2);//判斷三種方式是否獲取的是同一個Class對象 } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
注意:在運行期間,一個類,只有一個Class對象產生。
4.2 通過反射獲取構造方法並使用
Teacher類:
package com.ttbank.flep.core.entity; /** * @Author lucky * @Date 2021/12/28 16:30 */ public class Teacher { //---------------構造方法------------------- //(默認的構造方法) Teacher(String str){ System.out.println("(默認)的構造方法 s = " + str); } //無參構造方法 public Teacher(){ System.out.println("調用了公有、無參構造方法執行了。。。"); } //有一個參數的構造方法 public Teacher(char name){ System.out.println("姓名:" + name); } //有多個參數的構造方法 public Teacher(String name ,int age){ System.out.println("姓名:"+name+"年齡:"+ age);//這的執行效率有問題,以后解決。 } //受保護的構造方法 protected Teacher(boolean n){ System.out.println("受保護的構造方法 n = " + n); } //私有構造方法 private Teacher(int age) { System.out.println("私有的構造方法 年齡:" + age); } public void sayHello(){ System.out.println("hello"); } }
典型應用代碼:
public static void main(String[] args) { try { //01 獲取Class對象 Class<?> teacherClass = Class.forName("com.ttbank.flep.core.entity.Teacher"); //02 獲取構造方法 Constructor<?> constructor = teacherClass.getConstructor(String.class,int.class); //03 利用構造方法創建聲明類Teacher的新實例 Teacher teacher = (Teacher) constructor.newInstance("lucky", 25); teacher.sayHello(); } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) { e.printStackTrace(); } }
控制台輸出:
測試代碼:
package com.ttbank.flep.core.test; import java.lang.reflect.Constructor; /** * @Author lucky * @Date 2021/12/28 16:35 */ public class ConstructorTest { public static void main(String[] args) throws Exception { //1.加載Class對象 Class clazz = Class.forName("com.ttbank.flep.core.entity.Teacher"); //2.獲取所有公有構造方法 System.out.println("**********************所有公有構造方法*********************************"); Constructor[] conArray = clazz.getConstructors(); for(Constructor c : conArray){ System.out.println(c); } System.out.println("************所有的構造方法(包括:私有、受保護、默認、公有)***************"); conArray = clazz.getDeclaredConstructors(); for(Constructor c : conArray){ System.out.println(c); } System.out.println("*****************獲取公有、無參的構造方法*******************************"); Constructor con = clazz.getConstructor(null); //1>、因為是無參的構造方法所以類型是一個null,不寫也可以:這里需要的是一個參數的類型,切記是類型 //2>、返回的是描述這個無參構造函數的類對象。 System.out.println("con = " + con); //調用構造方法 Object obj = con.newInstance(); // System.out.println("obj = " + obj); // Student stu = (Student)obj; System.out.println("******************獲取私有構造方法,並調用*******************************"); con = clazz.getDeclaredConstructor(char.class); System.out.println(con); //調用構造方法 con.setAccessible(true);//暴力訪問(忽略掉訪問修飾符) obj = con.newInstance('男'); } }
控制台輸出:
**********************所有公有構造方法********************************* public com.ttbank.flep.core.entity.Teacher(java.lang.String,int) public com.ttbank.flep.core.entity.Teacher(char) public com.ttbank.flep.core.entity.Teacher() ************所有的構造方法(包括:私有、受保護、默認、公有)*************** private com.ttbank.flep.core.entity.Teacher(int) protected com.ttbank.flep.core.entity.Teacher(boolean) public com.ttbank.flep.core.entity.Teacher(java.lang.String,int) public com.ttbank.flep.core.entity.Teacher(char) public com.ttbank.flep.core.entity.Teacher() com.ttbank.flep.core.entity.Teacher(java.lang.String) *****************獲取公有、無參的構造方法******************************* con = public com.ttbank.flep.core.entity.Teacher() 調用了公有、無參構造方法執行了。。。 ******************獲取私有構造方法,並調用******************************* public com.ttbank.flep.core.entity.Teacher(char) 姓名:男
調用方法:
<1>獲取構造方法:
1).批量的方法:
public Constructor[] getConstructors() //所有"公有的"構造方法 public Constructor[] getDeclaredConstructors()//獲取所有的構造方法(包括私有、受保護、默認、公有)
2).獲取單個的方法,並調用:
public Constructor getConstructor(Class... parameterTypes) //獲取單個的"公有的"構造方法: public Constructor getDeclaredConstructor(Class... parameterTypes) //獲取"某個構造方法"可以是私有的,或受保護、默認、公有;
<2>newInstance是 Constructor類的方法(管理構造函數的類)
api的解釋為:
newInstance(Object... initargs)
使用此 Constructor 對象表示的構造方法來創建該構造方法的聲明類的新實例,並用指定的初始化參數初始化該實例。
它的返回值是T類型,所以newInstance是創建了一個構造方法的聲明類的新實例對象。並為之調用
5.Java中new和反射創建對象
5.1 Java中new創建對象
如果我們在代碼中如果寫了一段
A a = new A();
在JVM中會幫你做的事情有以下:
- JVM把類的.java文件編譯為一個.class的字節碼文件
- 類加載器把.class文件加載進jvm的內存中,一個Class對象生成,並放入方法區中,這Class對象對於任何類都是唯一一個。
做完這些之后,才是new字段的工作:
- 判斷內存中是否已經有那個唯一的Class對象
- 如果沒有,則進行上述操作。
- 如果有,則在內存中申請空間開辟,即根據Class對象獲取有關的構造器進行實例化,在這里我們假設是一個無參數構造,那么只需要newInstance()。
5.2 Java中使用反射創建對象
這次的代碼是我們最常見的反射代碼
Class c = Class.forName("A的全類名");
當JVM編譯到這段代碼的時候,他的步驟是:
1、使用類加載,將對應.class加載入內存的方法區中,並返回Class對象。
這時候,我們可以查看這個類對象里面的構造器方法,並使用無參數構造器進行構造實例,即如下代碼
Constructor constructor = c.getConstructor();
Object obj = constructor.newInstance();
用同樣的圖,我們可以畫出來。
到這里,我們幾乎可以知道無論是反射,還是New,其實都是通過類加載器對.class文件加載進內存中,創建了Class對象。‘’
Java中反射屬於動態編譯,而new屬於靜態編譯。
粗俗解釋:
1、靜態編譯相當於把所有需要的東西都在初始化的時候加載了,如果程序一大,就很有可能會跑得慢。
2、動態編譯,在編譯的時候,需要的模塊都沒有編譯進去,啟動程序的時候,模塊不會被加載而是在運行的時候,需要哪個模塊就調用哪個模塊。
上面的過程告訴我們,我們如果用new,那么我們要創建的類都是已經“寫死”在.class文件里面了,我們無法控制JVM幫我們加載類的這一項工作。
但是如果我們用反射創建類對象,我們是相當於親自“指導”JVM,我們“按需加載”.class文件,如果內存里面沒有這個類的.class文件,那么我們用Class.forName()去叫類加載器幫忙加載就行了,而不是把程序停下來,再打一段代碼,再讓類加載器進行加載,從而體現出了Java的“動態性”。
因為Spring在加載類的實例時,我們知道其實是用工廠的方式,給出一個個實例,而在工廠里面,用了單例,但是真正實例化,則是反射的newInstance來創建對象,而不是new。
那么,為什么是反射的newInstance,而不是new呢?
那么首先我們必須明白,Java里面,反射的目的是什么?
高內聚,低耦合。
進一步,反射比new的好處是什么?
反射不用知道類名,可以直接實例化類,也就是不用硬編碼。
有人問了,不知道類名那怎么反射呢?
例子:
通過new,我們這么寫:
A a = New A();
通過反射,我們這么寫:
Class c = Class.forName(“A”);
factory = (AInterface)c.newInstance();
其中AInterface是類A的接口,如果下面這樣寫,你可能會理解:
String className = “A”; Class c = Class.forName(className); factory = (AInterface)c.newInstance();
進一步,如果下面寫,你可能會理解:
String className = readfromXMlConfig;//從xml 配置文件中獲得字符串 Class c = Class.forName(className); factory = (AInterface)c.newInstance();
上面代碼就消滅了A類名稱,優點:無論A類怎么變化,上述代碼不變,甚至可以更換A的兄弟類B , C , D….等,只要他們繼承Ainterface就可以。
參考文獻:https://blog.csdn.net/qq_26558047/article/details/109745018
https://blog.csdn.net/weixin_34672875/article/details/115074666
https://blog.csdn.net/sinat_38259539/article/details/71799078----經典