給方法的參數(shù)加上限制是很常見的,比如參數(shù)代表索引時不能為負(fù)數(shù)、對于某個關(guān)鍵對象引用不能為null,否則會進(jìn)行一些處理,比如拋出相應(yīng)的異常信息。
對于這些參數(shù)限制,方法的提供者必須在文檔中注明,并且在方法開頭時檢查參數(shù),并在失敗時提供明確的信息,即:
detect errors as soon as possible after they occur
這將成為準(zhǔn)確定位錯誤的一大保障。
如果沒有做到這一點,最好的情況是方法在處理過程中失敗并拋出了莫名其妙的異常,錯誤的源頭變得難以定位,但這是最好的情況。更差的情況是方法執(zhí)行通過,沒有發(fā)生任何錯誤,只是得出的結(jié)果和方法描述完全不符,最后在某個關(guān)鍵的部分看到奇怪的數(shù)據(jù)時才亡羊補牢。
對于參數(shù)違反有效性時使用的異常類,我們通常拋出IllegalArgumentException,IndexOutOfBoundsException,NullPointerException。而對于違反約束時拋出的異常類型,需要用Javadoc的@throws標(biāo)簽對其進(jìn)行說明。另外,并不是所有參數(shù)檢查都需要做到這種地步。如果方法或構(gòu)造器不對外導(dǎo)出,則可以簡單使用assert來保證參數(shù)的有效性。比如java.util.Collections$CopiesList:
PRivate static class CopiesList<E> extends AbstractList<E> implements Randomaccess, Serializable{ //... CopiesList(int n, E e) { assert n >= 0; this.n = n; element = e; } //...}但是,有效性檢查并不都是簡單的,這一操作的代價也可能非常大甚至不切實際,于是有些操作直接將參數(shù)檢查隱含在計算過程中。比如java.util.Collections的sort()方法,列表中的元素當(dāng)然是都可比較的,而方法并沒有在開頭檢查有效性,而是計算中遇到問題時拋出異常。與這種方式相比,提前檢查有效性的意義確實不大。但在計算途中檢查參數(shù)的有效性需要考慮一點,即方法的原子性。
說道參數(shù)就不得不說方法重載,首先上一段例子:
import java.math.BigInteger;import java.util.ArrayList;import java.util.Collection;import java.util.HashMap;import java.util.HashSet;import java.util.List;import java.util.Set;public class CollectionClassifier { public static String classify(Set<?> s) { return "Set"; } public static String classify(List<?> lst) { return "List"; } public static String classify(Collection<?> c) { return "Unknown Collection"; } public static void main(String[] args) { Collection<?>[] collections = { new HashSet<String>(), new ArrayList<BigInteger>(), new HashMap<String, String>().values() }; for (Collection<?> c : collections) System.out.println(classify(c)); }}很常見的筆試題,會輸出三次"Unknown Collection",編譯時已決定了將調(diào)用的方法。與重載方法的靜態(tài)選擇(為什么當(dāng)初會這樣設(shè)計重載? overriding is the norm and overloading is the exception,似乎沒有人希望這種做法 )相對的是覆蓋方法,overridden method的選擇是動態(tài)的。
即,選擇方法是在運行時進(jìn)行的,子類用同樣的方法簽名覆蓋了上級類的方法,如果這個方法是實例方法則會在子類的實例上被調(diào)用。比如下面這個例子,實例的編譯時類型對其沒有造成影響:
class Wine { String name() { return "wine"; }}class SparklingWine extends Wine { @Override String name() { return "sparkling wine"; }}class Champagne extends SparklingWine { @Override String name() { return "champagne"; }}public class Overriding { public static void main(String[] args) { Wine[] wines = { new Wine(), new SparklingWine(), new Champagne() }; for (Wine wine : wines) System.out.println(wine.name()); }}鑒于重載方法的這種特征,而語言本身對其也沒有特別的限制,作者建議<不要提供相同參數(shù)數(shù)量的重載方法>,而對于可變參數(shù)則不要考慮重載。關(guān)于這個建議的不錯的例子就是ObjectOutputStream,對于其write方法,設(shè)計者并沒有提供相同參數(shù)數(shù)量的重載,而是提供了諸如writeInt,writeBoolean,writeLong等方法,而且read方法也是與write對稱的。這種方式不適用于構(gòu)造器,我們無法對構(gòu)造器進(jìn)行命名,但我們可以對構(gòu)造器進(jìn)行私有化并導(dǎo)出靜態(tài)工廠。但也并不能說必須嚴(yán)格遵守這種規(guī)則,比如為fetchSalaryInfo()提供了兩種方法重載,分別是int uid和User userInfo,很難想象會出現(xiàn)調(diào)用錯誤的情況。
破壞<不要提供相同參數(shù)數(shù)量的重載方法>這一規(guī)則不僅僅是出現(xiàn)在導(dǎo)出某個方法的時候,更新現(xiàn)有類的時候也有可能。比如String類有一個since 1.4的contentEquals(StringBuffer),另外還有一個since 1.5的contentEquals(CharSequence)。(Java 1.5版本中增加了CharSequence接口,并作為StringBuffer,StringBuilder,CharBuffer,String等類的公共接口)但這并不會帶來危害,因為這個例子的兩個重載方法的行為是完全一樣的。
對于泛型還有下面這種有趣的情況:
import java.util.ArrayList;import java.util.List;import java.util.Set;import java.util.TreeSet;public class SetList { public static void main(String[] args) { Set<Integer> set = new TreeSet<Integer>(); List<Integer> list = new ArrayList<Integer>(); for (int i = -3; i < 3; i++) { set.add(i); list.add(i); } for (int i = 0; i < 3; i++) { set.remove(i); list.remove(i); } System.out.println(set + " " + list); }}槽點:remove里的參數(shù)是index還是element?執(zhí)行結(jié)果是[-3,-2,-1][-2,0,2],也就是說 list.remove中的參數(shù)是index,而不是被自動裝箱。應(yīng)該說問題根本原因是List同時提供了remove(int)和remove(E)嗎?但作為API的使用者,我們能做的僅僅是對Java 5的這一大特性持更謹(jǐn)慎的態(tài)度。
新聞熱點
疑難解答