在JAVA中,子類覆寫父類的中的方法很常見,這樣做既可以修正bug,也可以提供擴展的業務功能支持,同時還符合開閉原則(Open-Closed Principle)。
符合開閉原則(Open-Closed Principle)的主要特征:
1.對於擴展是開放的(Open for extension)。這意味著模塊的行為是可以擴展的。當應用的需求改變時,我們可以對模塊進行擴展,使其具有滿足那些改變的新行為。也就是說,我們可以改變模塊的功能。
2.對於修改是關閉的(Closed for modification)。對模塊行為進行擴展時,不必改動模塊的源代碼或者二進制代碼。模塊的二進制可執行版本,無論是可鏈接的庫、DLL或者.EXE文件,都無需改動。
下面我們看一下覆寫必須滿足的條件:
看下面這段代碼:
1 public class Client6 { 2 public static void main(String[] args) { 3 // 向上轉型 4 Base base = new Sub(); 5 base.fun(100, 50); 6 // 不轉型 7 Sub sub = new Sub(); 8 sub.fun(100, 50); 9 } 10 } 11 12 // 基類 13 class Base { 14 void fun(int price, int... discounts) { 15 System.out.println("Base......fun"); 16 } 17 } 18 19 // 子類,覆寫父類方法 20 class Sub extends Base { 21 @Override 22 void fun(int price, int[] discounts) { 23 System.out.println("Sub......fun"); 24 } 25 }
該程序中sub.fun(100, 50)報錯,提示找不到fun(int,int)方法。這太奇怪了:子類繼承了父類的所有屬性和方法,甭管是私有的還是公開的訪問權限,同樣的參數,同樣的方法名,通過父類調用沒有任何問題,通過子類調用,卻編譯不過,為啥?難到是沒繼承下來?或者子類縮小了父類方法的前置條件?如果是這樣,就不應該覆寫,@Override就應該報錯呀。
事實上,base對象是把子類對象做了向上轉型,形參列表由父類決定,由於是變長參數,在編譯時,base.fun(100, 50);中的50這個實參會被編譯器"猜測"而編譯成"{50}"數組,再由子類Sub執行。我們再來看看直接調用子類的情況,這時編譯器並不會把"50"座類型轉換因為數組本身也是一個對象,編譯器還沒有聰明到要在兩個沒有繼承關系的類之間轉換,要知道JAVA是要求嚴格的類型匹配的,類型不匹配編譯器自然就會拒絕執行,並給予錯誤提示。
這是個特例,覆寫的方法參數列表竟然與父類不相同,這違背了覆寫的定義,並且會引發莫名其妙的錯誤。所以讀者在對變長參數進行覆寫時,如果要使用次類似的方法,請仔細想想是不是要一定如此。
注意:覆寫的方法參數與父類相同,不僅僅是類型、數量,還包括顯示形式.
記得大學剛開始學C語言時,老師就說:自增有兩種形式,分別是i++和++i,i++表示的先賦值後加1,++i是先加1後賦值,這樣理解了很多年也木有問題,直到遇到如下代碼,我才懷疑我的理解是不是錯了:
1 public class Client7 { 2 public static void main(String[] args) { 3 int count=0; 4 for(int i=0; i<10;i++){ 5 count=count++; 6 } 7 System.out.println("count = "+count); 8 } 9 }
這個程序輸出的count等於幾?是count自加10次嗎?答案等於10?可以肯定的說,這個運行結果是count=0。為什麼呢?
count++是一個表達式,是由返回值的,它的返回值就是count自加前的值,Java對自加是這樣處理的:首先把count的值(注意是值,不是引用)拷貝到一個臨時變量區,然後對count變量+1,最後返回臨時變量區的值。程序第一次循環處理步驟如下:
"count=count++"這條語句可以按照如下代碼理解:
1 public static int mockAdd(int count) { 2 // 先保存初始值 3 int temp = count; 4 // 做自增操作 5 count = count + 1; 6 // 返回原始值 7 return temp; 8 }
於是第一次循環後count的值為0,其它9次循環也是一樣的,最終你會發現count的值始終沒有改變,仍然保持著最初的狀態.
此例中代碼作者的本意是希望count自增,所以想當然的賦值給自身就可以了,不曾想到調到Java自增的陷阱中了,解決辦法很簡單,把"count=count++"改為"count++"即可。該問題在不同的語言環境中有著不同的實現:C++中"count=count++"與"count++"是等效的,而在PHP中保持著與JAVA相同的處理方式。每種語言對自增的實現方式各不相同。
1 public class Client8 { 2 public static void main(String[] args) { 3 // 數據定義初始化 4 int fee = 200; 5 // 其它業務處理 6 saveDefault: save(fee); 7 } 8 9 static void saveDefault() { 10 System.out.println("saveDefault...."); 11 } 12 13 static void save(int fee) { 14 System.out.println("save...."); 15 } 16 }
這段代碼分析一下,輸出結果,以及語法含義:
從Java5開始引入了靜態導入語法(import static),其目的是為了減少字符的輸入量,提高代碼的可閱讀性,以便更好地理解程序。我們先倆看一個不用靜態導入的例子,也就是一般導入:
1 public class Client9 { 2 // 計算圓面積 3 public static double claCircleArea(double r) { 4 return Math.PI * r * r; 5 } 6 7 // 計算球面積 8 public static double claBallArea(double r) { 9 return 4 * Math.PI * r * r; 10 } 11 }
這是很簡單的兩個方法,我們再這兩個計算面積的方法中都引入了java.lang.Math類(該類是默認導入的)中的PI(圓周率)常量,而Math這個類寫在這裡有點多余,特別是如果Client9類中的方法比較多時。如果每次輸入都需要敲入Math這個類,繁瑣且多余,靜態導入可以解決此問題,使用靜態導入後的程序如下:
1 import static java.lang.Math.PI; 2 3 public class Client9 { 4 // 計算圓面積 5 public static double claCircleArea(double r) { 6 return PI * r * r; 7 } 8 9 // 計算球面積 10 public static double claBallArea(double r) { 11 return 4 * PI * r * r; 12 } 13 }
靜態導入的作用是把Math類中的Pi常量引入到本類中,這會是程序更簡單,更容易閱讀,只要看到PI就知道這是圓周率,不用每次都把類名寫全了。但是,濫用靜態導入會使程序更難閱讀,更難維護,靜態導入後,代碼中就不需要再寫類名了,但我們知道類是"一類事物的描述",缺少了類名的修飾,靜態屬性和靜態方法的表象意義可以被無限放大,這會讓閱讀者很難弄清楚其屬性或者方法代表何意,繩子哪一類的屬性(方法)都要思考一番(當然IDE的友好提示功能另說),把一個類的靜態導入元素都引入進來了,那簡直就是噩夢。我們來看下面的例子:
1 import static java.lang.Math.*; 2 import static java.lang.Double.*; 3 import static java.lang.Integer.*; 4 import static java.text.NumberFormat.*; 5 6 import java.text.NumberFormat; 7 8 public class Client9 { 9 10 public static void formatMessage(String s) { 11 System.out.println("圓面積是: " + s); 12 } 13 14 public static void main(String[] args) { 15 double s = PI * parseDouble(args[0]); 16 NumberFormat nf = getInstance(); 17 nf.setMaximumFractionDigits(parseInt(args[1])); 18 formatMessage(nf.format(s)); 19 20 } 21 }
就這麼一段程序,看著就讓人惱火,常量PI,這知道是圓周率;parseDouble方法可能是Double類的一個轉換方法,這看名稱可以猜的到。那緊接著getInstance()方法是哪個類的?是Client9本地類?不對呀,本地沒有這個方法,哦,原來是NumberFormat類的方法,這個和formatMessage本地方法沒有任何區別了---這代碼太難閱讀了,肯定有人罵娘。
所以,對於靜態導入,一定要追尋兩個原則:
何為具有明確、清晰表象意義的工具類,我們看看Junit中使用靜態導入的例子:
1 import static org.junit.Assert.*; 2 class DaoTest{ 3 @Test 4 public void testInsert(){ 5 //斷言 6 assertEquals("foo","foo"); 7 assertFalse(Boolean.FALSE); 8 } 9 }
我們從程序中很容易判斷出assertEquals方法是用來斷言兩個值是否相等的,assertFalse方法則是斷言表達式為假,如此確實減少了代碼量,而且代碼的可讀性也提高了,這也是靜態導入用到正確的地方帶來的好處。
如果在一個類中的方法及屬性與靜態導入的方法及屬性相同會出現什麼問題呢?看下面的代碼
1 import static java.lang.Math.PI; 2 import static java.lang.Math.abs; 3 4 public class Client10 { 5 // 常量名於靜態導入的PI相同 6 public final static String PI = "祖沖之"; 7 //方法名於靜態導入的方法相同 8 public static int abs(int abs) { 9 return 0; 10 } 11 12 public static void main(String[] args) { 13 System.out.println("PI = "+PI); 14 System.out.println("abs(-100) = "+abs(-100)); 15 } 16 }
以上代碼中定義了一個String類型的常量PI,又定義了一個abs方法,與靜態導入的相同。首先說好消息,代碼沒有報錯,接下來是壞消息:我們不知道那個屬性和方法別調用了,因為常量名和方法名相同,到底調用了那一個方法呢?運行之後結果為:
PI = "祖沖之",abs(-100) = 0;
很明顯是本地的方法被調用了,為何不調用Math類中的屬性和方法呢?那是因為編譯器有一個"最短路徑"原則:如果能夠在本類中查找到相關的變量、常量、方法、就不會去其它包或父類、接口中查找,以確保本類中的屬性、方法優先。
因此,如果要變更一個被靜態導入的方法,最好的辦法是在原始類中重構,而不是在本類中覆蓋.