1. 程式人生 > >【Java】反射呼叫與面向物件結合使用產生的驚豔

【Java】反射呼叫與面向物件結合使用產生的驚豔



緣起


我在看Spring的原始碼時,發現了一個隱藏的問題,就是父類方法(Method)在子類例項上的反射(Reflect)呼叫。

初次看到,感覺有些奇特,因為父類方法可能是抽象的或私有的,但我沒有去懷疑什麼,這可是Spring的原始碼,肯定不會有錯。

不過我去做了測試,發現確實是正確的,那一瞬間竟然給我了一絲的驚豔。

這其實是面向物件(繼承與重寫,即多型)和反射結合的產物。下面先來看測試,最後再進行總結。

友情提示:測試內容較多,不過還是值得一看。


具體方法的繼承與重寫


先準備一個父類,有三個方法,分別是public,protected,private。

public class Parent {

    public String m1() {
        return "Parent.m1";
    }

    protected String m2() {
        return "Parent.m2";
    }

    private String m3() {
        return "Parent.m3";
    }
}


再準備一個子類,繼承上面的父類,也有三個相同的方法。

public class Child extends Parent {

    @Override
    public String m1() {
        return "Child.m1";
    }

    @Override
    protected String m2() {
        return "Child.m2";
    }

    private String m3() {
        return "Child.m3";
    }
}


public和protected是對父類方法的重寫,private自然不能重寫。

首先,通過反射獲取父類和子類的方法m1,並輸出:

Method pm1 = Parent.class.getDeclaredMethod("m1");
Method cm1 = Child.class.getDeclaredMethod("m1");

Log.log(pm1);
Log.log(cm1);


輸出如下:

public java.lang.String org.cnt.java.reflect.method.Parent.m1()
public java.lang.String org.cnt.java.reflect.method.Child.m1()


可以看到,一個是父類的方法,一個是子類的方法。

其次,比較下這兩個方法是否相同或相等:

Log.log("pm1 == cm1 -> {}", pm1 == cm1);
Log.log("pm1.equals(cm1) -> {}", pm1.equals(cm1));


輸入如下:

pm1 == cm1 -> false
pm1.equals(cm1) -> false


它們既不相同也不相等,因為一個在父類裡,一個在子類裡,它們各有各的原始碼,互相獨立。

然後,例項化父類和子類物件:

Parent p = new Parent();
Child c = new Child();


接著,父類方法分別在父類和子類物件上反射呼叫:

Log.log(pm1.invoke(p));
Log.log(pm1.invoke(c));


輸出如下:

Parent.m1
Child.m1


父類方法在父類物件上反射呼叫輸出Parent.m1,這很好理解。

父類方法在子類物件上反射呼叫輸出Child.m1,初次看到的話,還是有一些新鮮的。

明明呼叫的是父類版本的Method,輸出的卻是子類重寫版本的結果。

然後,子類方法分別在父類和子類物件上反射呼叫:

Log.log(cm1.invoke(p));
Log.log(cm1.invoke(c));


輸出如下:

IllegalArgumentException
Child.m1


子類方法在父類物件上反射呼叫時報錯。

子類方法在子類物件上反射呼叫時輸出Child.m1,這很好理解


按照同樣的方式,對方法m2進行測試,得到的結果和m1一樣。

它們一個是public的,一個是protected的,對於繼承與重寫來說是一樣的。

然後再對方法m3進行測試,它是private的,看看會有什麼不同。

首先,父類方法分別在父類和子類物件上反射呼叫:

Log.log(pm3.invoke(p));
Log.log(pm3.invoke(c));


輸入如下:

Parent.m3
Parent.m3


可以看到,輸出的都是父類裡的內容,和上面確實有所不同。

其次,子類方法分別在父類和子類物件上反射呼叫:

Log.log(cm3.invoke(p));
Log.log(cm3.invoke(c));


輸出如下:

IllegalArgumentException
Child.m3


子類方法在父類物件上反射呼叫時報錯。

子類方法在子類物件上反射呼叫時輸出Child.m3。


抽象方法的繼承與重寫


再大膽一點,使用抽象方法來測試下。

先準備一個抽象父類,有兩個抽象方法。

public abstract class Parent2 {

    public abstract String m1();

    protected abstract String m2();
}


再準備一個子類,繼承這個父類,並重寫抽象方法。

public class Child2 extends Parent2 {

    @Override
    public String m1() {
        return "Child2.m1";
    }

    @Override
    protected String m2() {
        return "Child2.m2";
    }
}


使用反射分別獲取父類和子類的方法m1,並輸出下:

public abstract java.lang.String org.cnt.java.reflect.method.Parent2.m1()
public java.lang.String org.cnt.java.reflect.method.Child2.m1()

pm1 == cm1 -> false
pm1.equals(cm1) -> false


可以看到父類方法是抽象的,子類重寫後變為非抽象的,這兩個方法既不相同也不相等。

由於父類是抽象類,不能例項化,因此只能在子類物件上反射呼叫這兩個方法:

Log.log(pm1.invoke(c2));
Log.log(cm1.invoke(c2));


輸出如下:

Child2.m1
Child2.m1


沒有報錯。且輸出正常,是不是又有一絲新鮮感,抽象方法也可以被反射呼叫。

對方法m2進行測試,得到相同的結果,因為protected和public對於繼承與重寫的規則是一樣的。


介面方法的實現與繼承


膽子漸漸大起來,再用介面來試試。

準備一個介面,包含抽象方法,預設方法和靜態方法。

public interface Inter {

    String m1();

    default String m2() {
        return "Inter.m2";
    }

    default String m3() {
        return "Inter.m3";
    }

    static String m4() {
        return "Inter.m4";
    }
}


準備一個實現類,實現這個介面,實現方法m1,重寫方法m2。

public class Impl implements Inter {

    @Override
    public String m1() {
        return "Impl.m1";
    }

    @Override
    public String m2() {
        return "Impl.m2";
    }

    public static String m5() {
        return "Impl.m5";
    }
}


分別從介面和實現類獲取方法m1,並輸出:

public abstract java.lang.String org.cnt.java.reflect.method.Inter.m1()
public java.lang.String org.cnt.java.reflect.method.Impl.m1()

im1 == cm1 -> false
im1.equals(cm1) -> false


可以看到介面中的方法是抽象的。因為它沒有方法體。

因為介面不能例項化,所以這兩個方法只能在實現類上反射呼叫:

Impl c = new Impl();

Log.log(im1.invoke(c));
Log.log(cm1.invoke(c));


輸出如下:

Impl.m1
Impl.m1


沒有報錯,輸出正常,又一絲的新鮮,接口裡的方法也可以通過反射呼叫。

對m2進行測試,m2是介面的預設方法,且被實現類重新實現了。

輸出下介面中的m2和實現類中的m2,如下:

public default java.lang.String org.cnt.java.reflect.method.Inter.m2()
public java.lang.String org.cnt.java.reflect.method.Impl.m2()

im2 == cm2 -> false
im2.equals(cm2) -> false


這兩個方法既不相同也不相等。

把它們分別在實現類上反射呼叫:

Impl c = new Impl();

Log.log(im2.invoke(c));
Log.log(cm2.invoke(c));


輸出如下:

Impl.m2
Impl.m2


因為實現類重寫了介面預設方法,所以輸出的都是重寫後的內容。

對m3進行測試,m3也是介面的預設方法,不過實現類沒有重新實現它,而是選擇使用介面的預設實現。

同樣從介面和實現類分別獲取這個方法,並輸出:

public default java.lang.String org.cnt.java.reflect.method.Inter.m3()
public default java.lang.String org.cnt.java.reflect.method.Inter.m3()

im3 == cm3 -> false
im3.equals(cm3) -> true


發現輸出的都是介面的方法,它們雖然不相同(same),但是卻相等(equal)。因為實現類只是簡單的繼承,並沒有重寫。

這兩個方法都在實現類的物件上反射呼叫,輸出如下:

相關推薦

Java反射呼叫面向物件結合使用產生

緣起我在看Spring的原始碼時,發現了一個隱藏的問題,就是父類方法(Method)在子類例項上的反射(Reflect)呼叫。初次看到,感覺有些奇特,因為父類方法可能是抽象的或私有的,但我沒有去懷疑什麼,這可是Spring的原始碼,肯定不會有錯。不過我去做了測試,發現確實是正確的,那一瞬間竟然給我了一絲的

Java 類鎖物件鎖加鎖 synchronized 小解

  最近遇到多執行緒處理的問題,原來只使用過synchronized的方法鎖,對於其中的物件鎖和類鎖瞭解,但是沒仔細研究過。所以回去查了相關資料進行整理。 基礎知識   首先介紹一下物件鎖(也叫方法鎖)與類鎖有那些不同。下文中使用物件鎖稱呼代替方法鎖。   對於物件鎖,

Java連結串列中儲存物件的問題

  在刷《劍指OFFER》的時候,自己犯了一個錯誤,發現:在連結串列中儲存一個物件時,如果該物件是不斷變化的,則應該建立一個新的物件複製該物件的內容(而不是指向同一個物件),將這個新的物件儲存到連結串列中。如果直接儲存該物件的話,連結串列中的物件也會不斷變化。基本資料型別和String則沒有這種問題。 其實

Java反射 & 反射效能 & 反射操作泛型 & 反射操作註解

反射機制 執行時載入,探知,使用編譯期間完全未知的類。 程式在執行狀態中,可以動態載入一個只有名稱的類,對於任意一個已載入的類,都能夠知道這個類的所有屬性和方法,對於任意一個物件,都能夠呼叫他的任意一個方法和屬性。 jvm載入完類之後,在堆記憶體中,就產生了一個對應的C

Java構造器setter/getter區別的簡單理解

構造器 在類的例項化時,構造器的作用為**物件屬性的初始化** 初始化時,物件既可以是有參構造,也可以是無參構造 有參構造時,在建立物件時傳參 無參構造或是未明確寫明構造方法時,環境會預設呼叫 例如 /** * 無參構造 */ public cl

java抽象類介面小結

一、抽象類 抽象類的定義與使用 定義: 抽象類只是在普通類的基礎上擴充了一些抽象方法而已,所謂的抽象方法指的是隻宣告而未實現的方法(即沒有方 法體)。所有抽象方法要求使用abstract關鍵字來定義,並且抽象方法所在的類也一定要使用abstract關鍵字來 定義,表示抽象類。 定義一

Java消除fastjson對同一物件迴圈引用的問題

傳入SerializerFeature.DisableCircularReferenceDetect引數 JSON.toJSONString([data],SerializerFeature.DisableCircularReferenceDetect);

Java如何呼叫MySQL儲存過程

Java呼叫MySQL的儲存過程,需要用JDBC連線,環境eclipse 首先檢視MySQL中的資料庫的儲存過程,接著編寫程式碼呼叫 mysql> show procedure status; +------+-------------+-----------+-

Java-JSP九大內建物件,作用分別是什麼? 分別有什麼方法?

一、pageContext表示頁容器     pageContext物件的作用是取得任何範圍的引數,通過pageContext物件可以獲取JSP頁面的out、request、response、session、application等物件,或者可以重新定向客戶的請求等,較少使

JavaWebSocket協議 SpringMVC整合WebSocket demo

WebSocket協議WebSocket協議是基於TCP的一種新的網路協議。它實現了瀏覽器與伺服器全雙工(full-duplex)通訊——允許伺服器主動傳送資訊給客戶端。WebSocket通訊協議於2011年被IETF定為標準RFC 6455,並被RFC7936所補充規範。W

java——連結串列陣列的區別

連結串列 陣列 記憶體佔用 不需要連續的記憶體空間 需要連續的記憶體空間 大小可變 連結串列的大小可動態變化 陣列大小固定,不能動態擴充套件 增刪 較快,只需要修改前一個元素的指標即可 較慢,需要移動修改元素只有的

Java程式設計細節技巧

本文用以歸納記錄學習過程中瞭解到的零碎知識,隨時更新。 【控制檯與編譯器】 關於常用軟體的快捷鍵、提示、技巧等。 控制檯: 1. Ctrl + C 停止程式執行(適用於程式進入死迴圈或沒有設定退出程式碼的程式) Eclipse: 快捷鍵: 1. Ctrl + D刪除本

java面向物件程式設計——類物件的繼承和多型

一、程式碼塊 程式碼塊定義:使用 {} 定義的一段程式碼。 根據程式碼塊定義的位置以及關鍵字,又可分為以下四種: 普通程式碼塊(定義在方法中,除錯) 構造塊(定義在類中的(不加修飾符)) 靜態塊 同步程式碼塊 構造塊 構造塊在每次產生一個新的物件就呼叫一次構

java面向物件程式設計——類物件的封裝

Java比C更方便的地方在於Java是面向物件的。面向物件的特徵: 1、封裝 ; 2、繼承 ; 3、多型; 以下對面向物件的封裝特徵進行簡述: 一、類與物件的定義與使用 類是指共性的概念,物件是一個具體的、可以使用的事物。 首先產生類,而後才可以產生物件。 類的組成: 1.

新手向面向過程面向物件的區別

 我大學的專業是跟硬體息息相關的自動化專業,因此最開始接觸的程式語言是C語言,典型的面向過程語言。後來自學java和前端的時候感覺和C差距還是蠻大的,“類”、“封裝”、“繼承”這些概念都不瞭解是什麼意思,教程讓怎麼用自己就怎麼寫,對“面向物件”和“面向過程”這兩個詞沒有概念,現在回顧一下這二

Java 基礎18 反射內省

1.反射和內省的概念定義    Java 反射機制容許程式在執行時載入、探知、使用編譯期間完全未知的 class,核心類 java.lang.Class。    通過把指定的類中各種元素成分都對映成 java.lang.reflect 反射包中的

JAVA面向物件程式—描述物件(修改補充)

前言 上一篇文章主要介紹了有關面向物件的知識與程式設計,這篇短小的文章主要是針對面向物件的一個補充。 知識點: 類的主方法 主方法是類的入口點,它定義了程式從何處開始以及提供對程式流向的控制,在java中,程式設計都是通過主方法來執行程式。 public st

Java物件總結

概述:物件在程式中是通過一種抽象資料型別來描述的,這種抽象資料型別稱為類;類表示一個有共同特徵的物件集合,物件是一個具體的概念,是類的一個具體的例項。 類中包含靜態屬性和動態特徵,作為一種複合資料型別,可以在程式中進行類的定義,類定義完成後,通過類來定義類的物

JAVA基礎:面向物件:(抽象、封裝、繼承、多型)、方法重寫、訪問修飾符、關鍵字(this,super,static,final)、抽象、介面

面向物件概念: 面向過程:完成一件事情任何過程自己親力親為。 面向物件:找能完成這件事的物件。在java中就是找物件,調方法。 面向物件特徵:抽象,封裝,繼承,多型 類和物件的關係: 類:生活中事物的統稱,如動物,車,服裝,食品… 物件:就是類下面實實在在存在的個體,如動物類的物件就是:

JAVA面向物件設計七個原則

    程式設計編碼理論上只要跑得起來就沒有問題了,但實際上程式是為需求服務的,需求無時無刻都有可能變更,程式也需要做出相應改變,這時如果程式設計混亂,那麼大多數時候都是沒有辦法輕微改動程式就達到需求目標的,而此時給程式大開刀也不允許,最後進入了兩難境地。所以最好一開始就