天天看點

【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

        return "Child.m1";

        return "Child.m2";

        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,這很好了解

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

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

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

首先,父類方法分别在父類和子類對象上反射調用:

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

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

Parent.m3

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

其次,子類方法分别在父類和子類對象上反射調用:

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

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

Child.m3

子類方法在子類對象上反射調用時輸出Child.m3。

抽象方法的繼承與重寫

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

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

public abstract class Parent2 {

    public abstract String m1();

    protected abstract String m2();

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

public class Child2 extends Parent2 {

        return "Child2.m1";

        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()

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

由于父類是抽象類,不能執行個體化,是以隻能在子類對象上反射調用這兩個方法:

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

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

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 {

        return "Impl.m1";

    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));

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

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

把它們分别在實作類上反射調用:

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

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

Impl.m2

因為實作類重寫了接口預設方法,是以輸出的都是重寫後的内容。

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

同樣從接口和實作類分别擷取這個方法,并輸出:

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

im3 == cm3 -> false

im3.equals(cm3) -> true

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

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

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

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

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

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

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

Child3.f1

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

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

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

結論

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

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

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

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

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

對于public和protected的方法,由于可以被繼承與重寫,是以真正起作用的是運作時對象,跟方法(反射擷取的Method)無關。

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

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

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

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

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

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

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

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

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

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

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

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

示例完整源碼:

https://github.com/coding-new-talking/java-code-demo.git

原文位址

https://www.cnblogs.com/lixinjie/p/combine-reflect-and-oo-in-java.html