【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