反射是一個非常重要的知識點,在學習Spring 框架
時,Bean的初始化用到了反射,在破壞單例模式
時也用到了反射,在獲取標注的注解
時也會用到反射······
當然了,反射在日常開發中,我們沒碰到過多少,至少我沒怎么用過。但面試是造火箭現場,可愛的面試官們又怎會輕易地放過我們呢?反射是開源框架中的一個重要設計理念,在源碼分析中少不了它的身影,所以,今天我會盡量用淺顯易懂的語言,讓你去理解下面這幾點:
(1)反射的思想以及它的作用 👉 概念篇
(2)反射的基本使用及應用場景 👉 應用篇
(3)使用反射能給我們編碼時帶來的優勢以及存在的缺陷 👉 分析篇
反射的思想及作用
有反必有正,就像世間的陰和陽,計算機的0和1一樣。天道有輪回,蒼天...(凈會在這瞎bibi)
在學習反射之前,先來了解正射是什么。我們平常用的最多的 new
方式實例化對象的方式就是一種正射的體現。假如我需要實例化一個HashMap
,代碼就會是這樣子。
Map<Integer, Integer> map = new HashMap<>();
map.put(1, 1);
某一天發現,該段程序不適合用 HashMap 存儲鍵值對,更傾向於用LinkedHashMap
存儲。重新編寫代碼后變成下面這個樣子。
Map<Integer, Integer> map = new LinkedHashMap<>();
map.put(1, 1);
假如又有一天,發現數據還是適合用 HashMap來存儲,難道又要重新修改源碼嗎?
發現問題了嗎?我們每次改變一種需求,都要去重新修改源碼,然后對代碼進行編譯,打包,再到 JVM 上重啟項目。這么些步驟下來,效率非常低。
對於這種需求頻繁變更但變更不大的場景,頻繁地更改源碼肯定是一種不允許的操作,我們可以使用一個開關
,判斷什么時候使用哪一種數據結構。
public Map<Integer, Integer> getMap(String param) {
Map<Integer, Integer> map = null;
if (param.equals("HashMap")) {
map = new HashMap<>();
} else if (param.equals("LinkedHashMap")) {
map = new LinkedHashMap<>();
} else if (param.equals("WeakHashMap")) {
map = new WeakHashMap<>();
}
return map;
}
通過傳入參數param
決定使用哪一種數據結構,可以在項目運行時,通過動態傳入參數決定使用哪一個數據結構。
如果某一天還想用TreeMap
,還是避免不了修改源碼,重新編譯執行的弊端。這個時候,反射就派上用場了。
在代碼運行之前,我們不確定將來會使用哪一種數據結構,只有在程序運行時才決定使用哪一個數據類,而反射
可以在程序運行過程中動態獲取類信息和調用類方法。通過反射構造類實例,代碼會演變成下面這樣。
public Map<Integer, Integer> getMap(String className) {
Class clazz = Class.forName(className);
Consructor con = clazz.getConstructor();
return (Map<Integer, Integer>) con.newInstance();
}
無論使用什么 Map,只要實現了Map接口
,就可以使用全類名路徑
傳入到方法中,獲得對應的 Map 實例。例如java.util.HashMap / java.util.LinkedHashMap····如果要創建其它類例如WeakHashMap
,我也不需要修改上面這段源碼。
我們來回顧一下如何從 new
一個對象引出使用反射
的。
- 在不使用反射時,構造對象使用 new 方式實現,這種方式在編譯期就可以把對象的類型確定下來。
- 如果需求發生變更,需要構造另一個對象,則需要修改源碼,非常不優雅,所以我們通過使用
開關
,在程序運行時判斷需要構造哪一個對象,在運行時可以變更開關來實例化不同的數據結構。 - 如果還有其它擴展的類有可能被使用,就會創建出非常多的分支,且在編碼時不知道有什么其他的類被使用到,假如日后
Map
接口下多了一個集合類是xxxHashMap
,還得創建分支,此時引出了反射:可以在運行時
才確定使用哪一個數據類,在切換類時,無需重新修改源碼、編譯程序。
第一章總結:
- 反射的思想:在程序運行過程中確定和解析數據類的類型。
- 反射的作用:對於在
編譯期
無法確定使用哪個數據類的場景,通過反射
可以在程序運行時構造出不同的數據類實例。
反射的基本使用
Java 反射的主要組成部分有4個:
Class
:任何運行在內存中的所有類都是該 Class 類的實例對象,每個 Class 類對象內部都包含了本來的所有信息。記着一句話,通過反射干任何事,先找 Class 准沒錯!Field
:描述一個類的屬性,內部包含了該屬性的所有信息,例如數據類型,屬性名,訪問修飾符······Constructor
:描述一個類的構造方法,內部包含了構造方法的所有信息,例如參數類型,參數名字,訪問修飾符······Method
:描述一個類的所有方法(包括抽象方法),內部包含了該方法的所有信息,與Constructor
類似,不同之處是 Method 擁有返回值類型信息,因為構造方法是沒有返回值的。
我總結了一張腦圖,放在了下面,如果用到了反射,離不開這核心的4
個類,只有去了解它們內部提供了哪些信息,有什么作用,運用它們的時候才能易如反掌。
我們在學習反射的基本使用時,我會用一個SmallPineapple
類作為模板進行說明,首先我們先來熟悉這個類的基本組成:屬性,構造函數和方法
public class SmallPineapple {
public String name;
public int age;
private double weight; // 體重只有自己知道
public SmallPineapple() {}
public SmallPineapple(String name, int age) {
this.name = name;
this.age = age;
}
public void getInfo() {
System.out.print("["+ name + " 的年齡是:" + age + "]");
}
}
反射中的用法有非常非常多,常見的功能有以下這幾個:
- 在運行時獲取一個類的 Class 對象
- 在運行時構造一個類的實例化對象
- 在運行時獲取一個類的所有信息:變量、方法、構造器、注解
獲取類的 Class 對象
在 Java 中,每一個類都會有專屬於自己的 Class 對象,當我們編寫完.java
文件后,使用javac
編譯后,就會產生一個字節碼文件.class
,在字節碼文件中包含類的所有信息,如屬性
,構造方法
,方法
······當字節碼文件被裝載進虛擬機執行時,會在內存中生成 Class 對象,它包含了該類內部的所有信息,在程序運行時可以獲取這些信息。
獲取 Class 對象的方法有3
種:
類名.class
:這種獲取方式只有在編譯
前已經聲明了該類的類型才能獲取到 Class 對象
Class clazz = SmallPineapple.class;
實例.getClass()
:通過實例化對象獲取該實例的 Class 對象
SmallPineapple sp = new SmallPineapple();
Class clazz = sp.getClass();
Class.forName(className)
:通過類的全限定名獲取該類的 Class 對象
Class clazz = Class.forName("com.bean.smallpineapple");
拿到 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));
內存中只有一個 Class 對象的原因要牽扯到
JVM 類加載機制
的雙親委派模型
,它保證了程序運行時,加載類
時每個類在內存中僅會產生一個Class對象
。在這里我不打算詳細展開說明,可以簡單地理解為 JVM 幫我們保證了一個類在內存中至多存在一個 Class 對象。
構造類的實例化對象
通過反射構造一個類的實例方式有2
種:
- Class 對象調用
newInstance()
方法
Class clazz = Class.forName("com.bean.SmallPineapple");
SmallPineapple smallPineapple = (SmallPineapple) clazz.newInstance();
smallPineapple.getInfo();
// [null 的年齡是:0]
即使 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]
通過 getConstructor(Object... paramTypes) 方法指定獲取指定參數類型的 Constructor, Constructor 調用 newInstance(Object... paramValues) 時傳入構造方法參數的值,同樣可以構造一個實例,且內部屬性已經被賦值。
通過Class
對象調用 newInstance() 會走默認無參構造方法,如果想通過顯式構造方法構造實例,需要提前從Class中調用getConstructor()
方法獲取對應的構造器,通過構造器去實例化對象。
這些 API 是在開發當中最常遇到的,當然還有非常多重載的方法,本文由於篇幅原因,且如果每個方法都一一講解,我們也記不住,所以用到的時候去類里面查找就已經足夠了。
獲取一個類的所有信息
Class 對象中包含了該類的所有信息,在編譯期我們能看到的信息就是該類的變量、方法、構造器,在運行時最常被獲取的也是這些信息。
獲取類中的變量(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));
不知道你有沒有發現一件有趣的事情,如果父類的屬性用
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 類繼承了抽象類Pineapple
,getInfo()
方法上標識有 @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 + "歲");
}
}
啟動類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)
通過反射調用方法
通過反射獲取到某個 Method 類對象后,可以通過調用invoke
方法執行。
invoke(Oject obj, Object... args)
:參數``1指定調用該方法的**對象**,參數
2`是方法的參數列表值。
如果調用的方法是靜態方法,參數1只需要傳入null
,因為靜態方法不與某個對象有關,只與某個類有關。
可以像下面這種做法,通過反射實例化一個對象,然后獲取Method
方法對象,調用invoke()
指定SmallPineapple
的getInfo()
方法。
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]
反射的應用場景
反射常見的應用場景這里介紹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>
在定義好上面的文件后,通過ClassPathXmlApplicationContext
加載該配置文件,程序啟動時,Spring 會將該配置文件中的所有bean
都實例化,放入 IOC 容器中,IOC 容器本質上就是一個工廠,通過該工廠傳入 <bean> 標簽的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]
}
}
Spring 在實例化對象的過程經過簡化之后,可以理解為反射實例化對象的步驟:
- 獲取Class對象的構造器
- 通過構造器調用 newInstance() 實例化對象
當然 Spring 在實例化對象時,做了非常多額外的操作,才能夠讓現在的開發足夠的便捷且穩定。
在之后的文章中會專門寫一篇文章講解如何利用反射實現一個
簡易版
的IOC
容器,IOC容器原理很簡單,只要掌握了反射的思想,了解反射的常用 API 就可以實現,我可以提供一個簡單的思路:利用 HashMap 存儲所有實例,key 代表 <bean> 標簽的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<>();
} // ···
}
}
利用反射和工廠模式相結合,在產生新的子類時,工廠類不用修改任何東西,可以專注於子類的實現,當子類確定下來時,工廠也就可以生產該子類了。
反射 + 抽象工廠的核心思想是:
- 在運行時通過參數傳入不同子類的全限定名獲取到不同的 Class 對象,調用 newInstance() 方法返回不同的子類。細心的讀者會發現提到了子類這個概念,所以反射 + 抽象工廠模式,一般會用於有繼承或者接口實現關系。
例如,在運行時才確定使用哪一種 Map
結構,我們可以利用反射傳入某個具體 Map 的全限定名,實例化一個特定的子類。
public class MapFactory {
/**
* @param className 類的全限定名
*/
public Map<Object, Object> produceMap(String className) {
Class clazz = Class.forName(className);
Map<Object, Object> map = clazz.newInstance();
return map;
}
}
className
可以指定為 java.util.HashMap,或者 java.util.TreeMap 等等,根據業務場景來定。
JDBC 加載數據庫驅動類
在導入第三方庫時,JVM不會主動去加載外部導入的類,而是等到真正使用時,才去加載需要的類,正是如此,我們可以在獲取數據庫連接時傳入驅動類的全限定名,交給 JVM 加載該類。
public class DBConnectionUtil {
/** 指定數據庫的驅動類 */
private static final String DRIVER_CLASS_NAME = "com.mysql.jdbc.Driver";
public static Connection getConnection() {
Connection conn = null;
// 加載驅動類
Class.forName(DRIVER_CLASS_NAME);
// 獲取數據庫連接對象
conn = DriverManager.getConnection("jdbc:mysql://···", "root", "root");
return conn;
}
}
在我們開發 SpringBoot 項目時,會經常遇到這個類,但是可能習慣成自然了,就沒多大在乎,我在這里給你們看看常見的application.yml
中的數據庫配置,我想你應該會恍然大悟吧。
這里的 driver-class-name,和我們一開始加載的類是不是覺得很相似,這是因為MySQL版本不同引起的驅動類不同,這體現使用反射的好處:不需要修改源碼,僅加載配置文件就可以完成驅動類的替換。
在之后的文章中會專門寫一篇文章詳細地介紹反射的應用場景,實現簡單的
IOC
容器以及通過反射實現工廠模式的好處。在這里,你只需要掌握反射的基本用法和它的思想,了解它的主要使用場景。
反射的優勢及缺陷
反射的優點:
- 增加程序的靈活性:面對需求變更時,可以靈活地實例化不同對象
但是,有得必有失,一項技術不可能只有優點沒有缺點,反射也有兩個比較隱晦的缺點:
- 破壞類的封裝性:可以強制訪問 private 修飾的信息
- 性能損耗:反射相比直接實例化對象、調用方法、訪問變量,中間需要非常多的檢查步驟和解析步驟,JVM無法對它們優化。
增加程序的靈活性
這里不再用 SmallPineapple 舉例了,我們來看一個更加貼近開發
的例子:
- 利用反射連接數據庫,涉及到數據庫的數據源。在 SpringBoot 中一切約定大於配置,想要定制配置時,使用
application.properties
配置文件指定數據源
角色1 - Java的設計者:我們設計好DataSource
接口,你們其它數據庫廠商想要開發者用你們的數據源
監控數據庫,就得實現我的這個接口
!
角色2 - 數據庫廠商:
- MySQL 數據庫廠商:我們提供了 com.mysql.cj.jdbc.MysqlDataSource 數據源,開發者可以使用它連接 MySQL。
- 阿里巴巴廠商:我們提供了 com.alibaba.druid.pool.DruidDataSource 數據源,我這個數據源更牛逼,具有頁面監控,慢SQL日志記錄等功能,開發者快來用它監控 MySQL吧!
- SQLServer 廠商:我們提供了 com.microsoft.sqlserver.jdbc.SQLServerDataSource 數據源,如果你想實用SQL Server 作為數據庫,那就使用我們的這個數據源連接吧
角色3 - 開發者:我們可以用配置文件
指定使用DruidDataSource
數據源
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
需求變更:某一天,老板來跟我們說,Druid 數據源不太符合我們現在的項目了,我們使用 MysqlDataSource 吧,然后程序猿就會修改配置文件,重新加載配置文件,並重啟項目,完成數據源的切換。
spring.datasource.type=com.mysql.cj.jdbc.MysqlDataSource
在改變連接數據庫的數據源時,只需要改變配置文件即可,無需改變任何代碼,原因是:
- Spring Boot 底層封裝好了連接數據庫的數據源配置,利用反射,適配各個數據源。
下面來簡略的進行源碼分析。我們用ctrl+左鍵
點擊spring.datasource.type
進入 DataSourceProperties 類中,發現使用setType() 將全類名轉化為 Class 對象注入到type
成員變量當中。在連接並監控數據庫時,就會使用指定的數據源操作。
private Class<? extends DataSource> type;
public void setType(Class<? extends DataSource> type) {
this.type = type;
}
Class
對象指定了泛型上界DataSource
,我們去看一下各大數據源的類圖結構
。
上圖展示了一部分數據源,當然不止這些,但是我們可以看到,無論指定使用哪一種數據源,我們都只需要與配置文件打交道,而無需更改源碼,這就是反射的靈活性!
破壞類的封裝性
很明顯的一個特點,反射可以獲取類中被private
修飾的變量、方法和構造器,這違反了面向對象的封裝特性,因為被 private 修飾意味着不想對外暴露,只允許本類訪問,而setAccessable(true)
可以無視訪問修飾符的限制,外界可以強制訪問。
還記得單例模式
一文嗎?里面講到反射破壞餓漢式和懶漢式單例模式,所以之后用了枚舉
避免被反射KO。
回到最初的起點,SmallPineapple 里有一個 weight 屬性被 private 修飾符修飾,目的在於自己的體重並不想給外界知道。
public class SmallPineapple {
public String name;
public int age;
private double weight; // 體重只有自己知道
public SmallPineapple(String name, int age, double weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
}
雖然 weight 屬性理論上只有自己知道,但是如果經過反射,這個類就像在裸奔一樣,在反射面前變得一覽無遺
。
SmallPineapple sp = new SmallPineapple("小菠蘿", 21, "54.5");
Clazz clazz = Class.forName(sp.getClass());
Field weight = clazz.getDeclaredField("weight");
weight.setAccessable(true);
System.out.println("窺覷到小菠蘿的體重是:" + weight.get(sp));
// 窺覷到小菠蘿的體重是:54.5 kg
性能損耗
在直接 new 對象並調用對象方法和訪問屬性時,編譯器會在編譯期提前檢查可訪問性,如果嘗試進行不正確的訪問,IDE會提前提示錯誤,例如參數傳遞類型不匹配,非法訪問 private 屬性和方法。
而在利用反射操作對象時,編譯器無法提前得知對象的類型,訪問是否合法,參數傳遞類型是否匹配。只有在程序運行時調用反射的代碼時才會從頭開始檢查、調用、返回結果,JVM也無法對反射的代碼進行優化。
雖然反射具有性能損耗的特點,但是我們不能一概而論,產生了使用反射就會性能下降的思想,反射的慢,需要同時調用上100W
次才可能體現出來,在幾次、幾十次的調用,並不能體現反射的性能低下。所以不要一味地戴有色眼鏡看反射,在單次調用反射的過程中,性能損耗可以忽略不計。如果程序的性能要求很高,那么盡量不要使用反射。
反射基礎篇文末總結
- 反射的思想:反射就像是一面鏡子一樣,在運行時才看到自己是誰,可獲取到自己的信息,甚至實例化對象。
- 反射的作用:在運行時才確定實例化對象,使程序更加健壯,面對需求變更時,可以最大程度地做到不修改程序源碼應對不同的場景,實例化不同類型的對象。
- 反射的應用場景常見的有
3
個:Spring的 IOC 容器,反射+工廠模式 使工廠類更穩定,JDBC連接數據庫時加載驅動類 - 反射的
3
個特點:增加程序的靈活性、破壞類的封裝性以及性能損耗
你好,我是 cxuan,我自己手寫了四本 PDF,分別是 Java基礎總結、HTTP 核心總結、計算機基礎知識,操作系統核心總結,我已經整理成為 PDF,可以關注公眾號 Java建設者 回復 PDF 領取優質資料。