【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進行測試,得到相同的結果,因為protectedpublic對於繼承與重寫的規則是一樣的。


接口方法的實現與繼承


膽子漸漸大起來,再用接口來試試。

准備一個接口,包含抽象方法,默認方法和靜態方法。

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)。因為實現類只是簡單的繼承,並沒有重寫。

這兩個方法都在實現類的對象上反射調用,輸出如下:

Inter.m3
Inter.m3


都輸出的是接口的默認實現。

因為接口也可以包含靜態方法,索性都測試了吧。

m4就是接口靜態方法,也分別從接口和實現類來獲取方法m4,並進行輸出:

Method im4 = Inter.class.getDeclaredMethod("m4");
Method cm4 = Impl.class.getMethod("m4");


輸出如下:

public static java.lang.String org.cnt.java.reflect.method.Inter.m4()
NoSuchMethodException


接口獲取靜態方法正常,從實現類獲取靜態方法報錯。表明實現類不會繼承接口的靜態方法

通過反射調用接口靜態方法:

Log.log(im4.invoke(null));


靜態方法屬於(也稱類型)本身,調用時需要對象,所以參數傳null(或任意對象都行)即可。

也可以使用接口直接調用靜態方法:

Log.log(Inter.m4());


輸出結果自然都是Inter.m4。

編程新說注:實現類不能調用接口的靜態方法,接口的靜態方法只能由接口本身調用,但子類可以調用父類的靜態方法。


字段的繼承問題


我也是腦洞大開,竟然想到用字段進行測試。那就開始吧。

先准備一個父類,含有三個字段。

public class Parent3 {

    public String f1 = "Parent3.f1";

    protected String f2 = "Parent3.f2";

    private String f3 = "Parent3.f3";
}


再准備一個子類,繼承父類,且含有三個相同的字段。

public class Child3 extends Parent3 {

    public String f1 = "Child3.f1";

    protected String f2 = "Child3.f2";

    private String f3 = "Child3.f3";
}


納尼,子類可以定義和父類同名的字段,而且也報錯,關鍵IDE也沒有提示。

請允許我吐槽幾句,人們都說C#是一門優雅的語言,優雅在哪里呢?來見識下。

先寫基類(C#里喜歡叫基類,Java里喜歡叫父類):

public class CsBase {
    public string name = "李新傑";
}


再寫繼承類:

public class CsInherit : CsBase {
    new public string name = "編程新說";
}


看到了吧,子類要想覆蓋(即遮罩)父類里的成員,需要加一個new關鍵字,提示一下寫代碼的人,讓他知道自己在干什么,別無意間弄錯了。

這就是優雅,而Java呢,啥玩意兒都沒有,存在出錯的風險吧,當然其實一般也沒有問題。

一吐為快

C#就是一杯咖啡,即使不加奶不加糖不需要攪拌的時候也會給你一把小勺子,讓你隨意的攪動兩下,體現一下優雅。

Java就是一個大蒜,不僅聽到后就掉了檔次,而且有人吃的時候連蒜皮都不剝,直接用嘴咬,然后再把皮吐出來。

這是以前郭德綱和周立波互噴的時候說的喝咖啡的高雅,吃大蒜的低俗,我這里借鑒過來再演繹一下,哈哈。

簡單自嗨一下,不必當真,Java和C#在語法上的細節差異,主要是語言之父們的哲學思維不同,但是都說得通。

這就像是,靠左走還是靠右走好呢?沒啥區別,定好規則即可。

言歸正傳,分別獲取子類和父類的f1字段並進行輸出:

public java.lang.String org.cnt.java.reflect.method.Parent3.f1
public java.lang.String org.cnt.java.reflect.method.Child3.f1

pf1.equals(cf1-false


這兩個字段不相等。

然后分別實例化父類和子類:

Parent3 p = new Parent3();
Child3 c = new Child3();


父類字段分別在父類和子類實例上反射調用:

Log.log(pf1.get(p));
Log.log(pf1.get(c));


輸出如下:

Parent3.f1
Parent3.f1


可以看到,輸出的都是父類的字段值。

子類字段分別在父類和子類對象上反射調用:

Log.log(cf1.get(p));
Log.log(cf1.get(c));


輸出如下:

IllegalArgumentException
Child3.f1


子類字段在父類對象上反射調用時報錯

子類字段在子類對象上反射調用時輸出的是子類的字段值。

用相同的方法對字段f2f3進行測試,得到的結果是一樣的。即使一個是protected的,一個是private的。


結論


看了這么多,相信都已迫不及待的想知道結論了。那就一起總結下吧。

總的來看,反射調用輸出的結果和直接使用對象調用是一樣的,說明反射調用也是支持面向對象的多態特性的。不然就亂套了嘛。

使用對象調用時,會根據運行時對象的具體類型,找出該類型對父類方法的重寫版本或繼承版本,然后再在對象上調用這個版本的方法。

對於反射也是完全一樣的,它也關注這兩個東西,哪個方法和哪個運行時對象

反射調用與繼承重寫結合后的規則是這樣的:

對於publicprotected方法,由於可以被繼承與重寫,所以真正起作用的是運行時對象,跟方法(反射獲取的Method)無關。

無論它是從接口獲取的,還是從父類獲取的,或是從子類獲取的,或者說是抽象的,都無所謂,關鍵看在哪個對象上調用。

對於private方法,由於不能被繼承與重寫,所以真正起作用的就是方法(反射獲取的Method)本身,而與運行時對象無關。

對於publicprotected字段,可以被繼承,但是面向對象規定字段是不可以被重寫的,所以真正起作用的就是字段(反射獲取的Field)本身,而與運行時對象無關。

對於private字段,不可以被繼承,也不能被重寫,所以真正起作用的就是字段(反射獲取的Field)本身,而與運行時對象無關。


哈哈,應該明白過來了吧,這不就是面向對象的特性嘛,誰說不是呢。因為反射調用也是要遵從面向對象的規則的。

還有一點,父類的字段和方法可以在子類對象上反射調用,因為子類是父類的一個特殊分支,子類繼承了父類嘛。

但是,子類自己定義的字段與方法或者重寫了的方法,不可以在父類對象上反射調用,因為父類不能轉換為子類。

好比,可以說人是動物,但反過來,說動物是人就不對了。測試中遇到的報錯就屬於這種情況,這種規則也是面向對象規定的。

這就是反射和面向對象結合的驚艷,如果都明白了文章中的示例,那也就明白了這種驚艷。

此外,反射至少還有以下兩個好處

1)寫法統一,不管什么類的什么方法,都是method.invoke(..)來調用,很適合用作框架開發,因為框架要求的就是統一模型或寫法。

2)支持了面向對象的特征,且突破了面向對象的限制,因為反射可以調用父類的私有方法和私有字段,還可以在類的外面調用它的私有受保護的方法和字段。

我之前寫過一篇分析面向對象的文章《三個臭皮匠的OO哲學,from C++、C# and Java》,那里有比較深刻的思考,推薦一看。

示例完整源碼
https://github.com/coding-new-talking/java-code-demo.git

 

>>> 熱門文章集錦 <<<

 

畢業10年,我有話說

【面試】我是如何面試別人List相關知識的,深度有點長文

我是如何在畢業不久只用1年就升為開發組長的

爸爸又給Spring MVC生了個弟弟叫Spring WebFlux

【面試】我是如何在面試別人Spring事務時“套路”對方的

【面試】Spring事務面試考點吐血整理(建議珍藏)

【面試】我是如何在面試別人Redis相關知識時“軟懟”他的

【面試】吃透了這些Redis知識點,面試官一定覺得你很NB(干貨 | 建議珍藏)

【面試】如果你這樣回答“什么是線程安全”,面試官都會對你刮目相看(建議珍藏)

【面試】迄今為止把同步/異步/阻塞/非阻塞/BIO/NIO/AIO講的這么清楚的好文章(快快珍藏)

【面試】一篇文章幫你徹底搞清楚“I/O多路復用”和“異步I/O”的前世今生(深度好文,建議珍藏)

【面試】如果把線程當作一個人來對待,所有問題都瞬間明白了

Java多線程通關———基礎知識挑戰

品Spring:帝國的基石

 

 

作者是工作超過10年的碼農,現在任架構師。喜歡研究技術,崇尚簡單快樂。追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂並記住。下面是公眾號的二維碼,歡迎關注!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM