【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