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

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

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
子類字段在父類對象上反射調用時報錯。

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

用相同的方法對字段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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章