學會反射的基礎,開源新作

程序員ioms 2021-09-18 03:44:35 阅读数:31

反射 新作

獲取 Class 對象的方法有3種:

  • 類名.class:這種獲取方式只有在編譯前已經聲明了該類的類型才能獲取到 Class 對象
Class clazz = SmallPineapple.class;

  • 1.
  • 2.
  • 實例.getClass():通過實例化對象獲取該實例的 Class 對象
SmallPineapple sp = new SmallPineapple();
Class clazz = sp.getClass();

  • 1.
  • 2.
  • Class.forName(className):通過類的全限定名獲取該類的 Class 對象
Class clazz = Class.forName("com.bean.smallpineapple");

  • 1.
  • 2.

拿到 Class對象就可以對它為所欲為了:剝開它的皮(獲取類信息)、指揮它做事(調用它的方法),看透它的一切(獲取屬性),總之它就沒有隱私了。

不過在程序中,每個類的 Class 對象只有一個,也就是說你只有這一個奴隸。我們用上面三種方式測試,通過三種方式打印各個 Class 對象都是相同的。

Class clazz1 = Class.forName("com.bean.SmallPineapple");
Class clazz2 = SmallPineapple.class;
SmallPineapple instance = new SmallPineapple();
Class clazz3 = instance.getClass();
System.out.println("Class.forName() == SmallPineapple.class:" + (clazz1 == clazz2));
System.out.println("Class.forName() == instance.getClass():" + (clazz1 == clazz3));
System.out.println("instance.getClass() == SmallPineapple.class:" + (clazz2 == clazz3));

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

學會反射的基礎,開源新作_Java

內存中只有一個 Class 對象的原因要牽扯到 JVM 類加載機制雙親委派模型,它保證了程序運行時,加載類時每個類在內存中僅會產生一個Class對象。在這裏我不打算詳細展開說明,可以簡單地理解為 JVM 幫我們保證了一個類在內存中至多存在一個 Class 對象

構造類的實例化對象

通過反射構造一個類的實例方式有2種:

  • Class 對象調用newInstance()方法
Class clazz = Class.forName("com.bean.SmallPineapple");
SmallPineapple smallPineapple = (SmallPineapple) clazz.newInstance();
smallPineapple.getInfo();
// [null 的年齡是:0]

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

即使 SmallPineapple 已經顯式定義了構造方法,通過 newInstance() 創建的實例中,所有屬性值都是對應類型的初始值,因為 newInstance() 構造實例會調用默認無參構造器

  • Constructor 構造器調用newInstance()方法
Class clazz = Class.forName("com.bean.SmallPineapple");
Constructor constructor = clazz.getConstructor(String.class, int.class);
constructor.setAccessible(true);
SmallPineapple smallPineapple2 = (SmallPineapple) constructor.newInstance("小菠蘿", 21);
smallPineapple2.getInfo();
// [小菠蘿 的年齡是:21]

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

通過 getConstructor(Object… paramTypes) 方法指定獲取指定參數類型的 Constructor, Constructor 調用 newInstance(Object… paramValues) 時傳入構造方法參數的值,同樣可以構造一個實例,且內部屬性已經被賦值。

通過Class對象調用 newInstance() 會走默認無參構造方法,如果想通過顯式構造方法構造實例,需要提前從Class中調用getConstructor()方法獲取對應的構造器,通過構造器去實例化對象。

這些 API 是在開發當中最常遇到的,當然還有非常多重載的方法,本文由於篇幅原因,且如果每個方法都一一講解,我們也記不住,所以用到的時候去類裏面查找就已經足够了。

獲取一個類的所有信息

Class 對象中包含了該類的所有信息,在編譯期我們能看到的信息就是該類的變量、方法、構造器,在運行時最常被獲取的也是這些信息。

學會反射的基礎,開源新作_後端_02

獲取類中的變量(Field)

  • Field[] getFields():獲取類中所有被public修飾的所有變量
  • Field getField(String name):根據變量名獲取類中的一個變量,該變量必須被public修飾
  • Field[] getDeclaredFields():獲取類中所有的變量,但無法獲取繼承下來的變量
  • Field getDeclaredField(String name):根據姓名獲取類中的某個變量,無法獲取繼承下來的變量

獲取類中的方法(Method)

  • Method[] getMethods():獲取類中被public修飾的所有方法

  • Method getMethod(String name, Class…<?> paramTypes):根據名字和參數類型獲取對應方法,該方法必須被public修飾

  • Method[] getDeclaredMethods():獲取所有方法,但無法獲取繼承下來的方法

  • Method getDeclaredMethod(String name, Class…<?> paramTypes):根據名字和參數類型獲取對應方法,無法獲取繼承下來的方法

獲取類的構造器(Constructor)

  • Constuctor[] getConstructors():獲取類中所有被public修飾的構造器
  • Constructor getConstructor(Class…<?> paramTypes):根據參數類型獲取類中某個構造器,該構造器必須被public修飾
  • Constructor[] getDeclaredConstructors():獲取類中所有構造器
  • Constructor getDeclaredConstructor(class…<?> paramTypes):根據參數類型獲取對應的構造器

每種功能內部以 Declared 細分為2類:

Declared修飾的方法:可以獲取該類內部包含的所有變量、方法和構造器,但是無法獲取繼承下來的信息

Declared修飾的方法:可以獲取該類中public修飾的變量、方法和構造器,可獲取繼承下來的信息

如果想獲取類中**所有的(包括繼承)**變量、方法和構造器,則需要同時調用getXXXs()getDeclaredXXXs()兩個方法,用Set集合存儲它們獲得的變量、構造器和方法,以防兩個方法獲取到相同的東西。

例如:要獲取SmallPineapple獲取類中所有的變量,代碼應該是下面這樣寫。

Class clazz = Class.forName("com.bean.SmallPineapple");
// 獲取 public 屬性,包括繼承
Field[] fields1 = clazz.getFields();
// 獲取所有屬性,不包括繼承
Field[] fields2 = clazz.getDeclaredFields();
// 將所有屬性匯總到 set
Set<Field> allFields = new HashSet<>();
allFields.addAll(Arrays.asList(fields1));
allFields.addAll(Arrays.asList(fields2));

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

不知道你有沒有發現一件有趣的事情,如果父類的屬性用protected修飾,利用反射是無法獲取到的。

protected 修飾符的作用範圍:只允許同一個包下或者子類訪問,可以繼承到子類。

getFields() 只能獲取到本類的public屬性的變量值;

getDeclaredFields() 只能獲取到本類的所有屬性,不包括繼承的;無論如何都獲取不到父類的 protected 屬性修飾的變量,但是它的的確確存在於子類中。

獲取注解

獲取注解單獨擰了出來,因為它並不是專屬於 Class 對象的一種信息,每個變量,方法和構造器都可以被注解修飾,所以在反射中,Field,Constructor 和 Method 類對象都可以調用下面這些方法獲取標注在它們之上的注解。

  • Annotation[] getAnnotations():獲取該對象上的所有注解
  • Annotation getAnnotation(Class annotaionClass):傳入注解類型,獲取該對象上的特定一個注解
  • Annotation[] getDeclaredAnnotations():獲取該對象上的顯式標注的所有注解,無法獲取繼承下來的注解
  • Annotation getDeclaredAnnotation(Class annotationClass):根據注解類型,獲取該對象上的特定一個注解,無法獲取繼承下來的注解

只有注解的@Retension標注為RUNTIME時,才能够通過反射獲取到該注解,@Retension 有3種保存策略:

  • SOURCE:只在**源文件(.java)**中保存,即該注解只會保留在源文件中,編譯時編譯器會忽略該注解,例如 @Override 注解
  • CLASS:保存在字節碼文件(.class)中,注解會隨著編譯跟隨字節碼文件中,但是運行時不會對該注解進行解析
  • RUNTIME:一直保存到運行時用得最多的一種保存策略,在運行時可以獲取到該注解的所有信息

像下面這個例子,SmallPineapple 類繼承了抽象類PineapplegetInfo()方法上標識有 @Override 注解,且在子類中標注了@Transient注解,在運行時獲取子類重寫方法上的所有注解,只能獲取到@Transient的信息。

public abstract class Pineapple {
public abstract void getInfo();
}
public class SmallPineapple extends Pineapple {
@Transient
@Override
public void getInfo() {
System.out.print("小菠蘿的身高和年齡是:" + height + "cm ; " + age + "歲");
}
}

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

啟動類Bootstrap獲取 SmallPineapple 類中的 getInfo() 方法上的注解信息:

public class Bootstrap {
/**
* 根據運行時傳入的全類名路徑判斷具體的類對象
* @param path 類的全類名路徑
*/
public static void execute(String path) throws Exception {
Class obj = Class.forName(path);
Method method = obj.getMethod("getInfo");
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation.toString());
}
}
public static void main(String[] args) throws Exception {
execute("com.pineapple.SmallPineapple");
}
}
// @java.beans.Transient(value=true)

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

通過反射調用方法

通過反射獲取到某個 Method 類對象後,可以通過調用invoke方法執行。

  • invoke(Oject obj, Object... args):參數``1指定調用該方法的**對象**,參數2`是方法的參數列錶值。

如果調用的方法是靜態方法,參數1只需要傳入null,因為靜態方法不與某個對象有關,只與某個類有關。

可以像下面這種做法,通過反射實例化一個對象,然後獲取Method方法對象,調用invoke()指定SmallPineapplegetInfo()方法。

Class clazz = Class.forName("com.bean.SmallPineapple");
Constructor constructor = clazz.getConstructor(String.class, int.class);
constructor.setAccessible(true);
SmallPineapple sp = (SmallPineapple) constructor.newInstance("小菠蘿", 21);
Method method = clazz.getMethod("getInfo");
if (method != null) {
method.invoke(sp, null);
}
// [小菠蘿的年齡是:21]

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

反射的應用場景

反射常見的應用場景這裏介紹3個:

  • Spring 實例化對象:當程序啟動時,Spring 會讀取配置文件applicationContext.xml並解析出裏面所有的 標簽實例化到IOC容器中。
  • 反射 + 工廠模式:通過反射消除工廠中的多個分支,如果需要生產新的類,無需關注工廠類,工廠類可以應對各種新增的類,反射可以使得程序更加健壯。
  • JDBC連接數據庫:使用JDBC連接數據庫時,指定連接數據庫的驅動類時用到反射加載驅動類

Spring 的 IOC 容器

在 Spring 中,經常會編寫一個上下文配置文件applicationContext.xml,裏面就是關於bean的配置,程序啟動時會讀取該 xml 文件,解析出所有的 <bean>標簽,並實例化對象放入IOC容器中。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="smallpineapple" class="com.bean.SmallPineapple">
<constructor-arg type="java.lang.String" value="小菠蘿"/>
<constructor-arg type="int" value="21"/>
</bean>
</beans>

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

在定義好上面的文件後,通過ClassPathXmlApplicationContext加載該配置文件,程序啟動時,Spring 會將該配置文件中的所有bean都實例化,放入 IOC 容器中,IOC 容器本質上就是一個工廠,通過該工廠傳入 \ 標簽的id屬性獲取到對應的實例。

public class Main {
public static void main(String[] args) {
ApplicationContext ac =
new ClassPathXmlApplicationContext("applicationContext.xml");
SmallPineapple smallPineapple = (SmallPineapple) ac.getBean("smallpineapple");
smallPineapple.getInfo(); // [小菠蘿的年齡是:21]
}
}

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

Spring 在實例化對象的過程經過簡化之後,可以理解為反射實例化對象的步驟:

  • 獲取Class對象的構造器
  • 通過構造器調用 newInstance() 實例化對象

當然 Spring 在實例化對象時,做了非常多額外的操作,才能够讓現在的開發足够的便捷且穩定

在之後的文章中會專門寫一篇文章講解如何利用反射實現一個簡易版IOC容器,IOC容器原理很簡單,只要掌握了反射的思想,了解反射的常用 API 就可以實現,我可以提供一個簡單的思路:利用 HashMap 存儲所有實例,key 代錶 \ 標簽的 id,value 存儲對應的實例,這對應了 Spring IOC容器管理的對象默認是單例的。

反射 + 抽象工廠模式

傳統的工廠模式,如果需要生產新的子類,需要修改工廠類,在工廠類中增加新的分支

public class MapFactory {
public Map<Object, object> produceMap(String name) {
if ("HashMap".equals(name)) {
return new HashMap<>();
} else if ("TreeMap".equals(name)) {
return new TreeMap<>();
} // ···
}
}

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

利用反射和工廠模式相結合,在產生新的子類時,工廠類不用修改任何東西,可以專注於子類的實現,當子類確定下來時,工廠也就可以生產該子類了。

Ending

Tip:由於文章篇幅有限制,下面還有20個關於MySQL的問題,我都複盤整理成一份pdf文檔了,後面的內容我就把剩下的問題的目錄展示給大家看一下

 CodeChina開源項目:【一線大廠Java面試題解析+核心總結學習筆記+最新講解視頻】

如果覺得有幫助不妨【轉發+點贊+關注】支持我,後續會為大家帶來更多的技術類文章以及學習類文章!(阿裏對MySQL底層實現以及索引實現問的很多)

學會反射的基礎,開源新作_後端_03

學會反射的基礎,開源新作_Java_04

吃透後這份pdf,你同樣可以跟面試官侃侃而談MySQL。其實像阿裏p7崗比特的需求也沒那麼難(但也不簡單),紮實的Java基礎+無短板知識面+對某幾個開源技術有深度學習+閱讀過源碼+算法刷題,這一套下來p7崗差不多沒什麼問題,還是希望大家都能拿到高薪offer吧。

版权声明:本文为[程序員ioms]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/09/20210918034434809g.html