學會反射後,我被錄取了!(乾貨)

 

本文系小菠蘿的投稿java

 

 

反射是一個很是重要的知識點,在學習Spring 框架時,Bean的初始化用到了反射,在破壞單例模式時也用到了反射,在獲取標註的註解時也會用到反射······mysql

固然了,反射在平常開發中,咱們沒碰到過多少,至少我沒怎麼用過。但面試是造火箭現場,可愛的面試官們又怎會輕易地放過咱們呢?反射是開源框架中的一個重要設計理念,在源碼分析中少不了它的身影,因此,今天我會盡可能用淺顯易懂的語言,讓你去理解下面這幾點:面試

(1)反射的思想以及它的做用: 概念篇算法

(2)反射的基本使用及應用場景: 應用篇spring

(3)使用反射能給咱們編碼時帶來的優點以及存在的缺陷: 分析篇sql

反射的思想及做用

有反必有正,就像世間的陰和陽,計算機的0和1同樣。天道有輪迴,蒼天...(淨會在這瞎bibi)docker

在學習反射以前,先來了解正射是什麼。咱們日常用的最多的 new 方式實例化對象的方式就是一種正射的體現。假如我須要實例化一個HashMap,代碼就會是這樣子。數據庫

Map<Integer, Integer> map = new HashMap<>(); map.put(11); 

某一天發現,該段程序不適合用 HashMap 存儲鍵值對,更傾向於用LinkedHashMap存儲。從新編寫代碼後變成下面這個樣子。編程

Map<Integer, Integer> map = new LinkedHashMap<>(); map.put(11); 

假如又有一天,發現數據仍是適合用 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 類繼承了抽象類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 + "歲");     } } 

啓動類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()指定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] 

反射的應用場景

反射常見的應用場景這裏介紹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 個特色:增長程序的靈活性、破壞類的封裝性以及性能損耗

《真實世界的算法:初學者指南》 學習算法的第一本技術書;算法儘可能簡單,避免讀者有挫敗感,僅需基本數學基礎和計算機常識知識;經過真實世界須要解決的實際問題來介紹算法思想;爲各領域高效運用算法提供重要指南

 

往期精選

2w字 + 40張圖帶你參透併發編程!

好文推薦!Linux 是如何管理 IO 的?

昨天有讀者說他不會 docker,今天就給你肝出來了

Spring Cloud 20000 字總結,收藏!

 

本文分享自微信公衆號 - Java建設者(javajianshe)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索