下面這個程序的意圖是好的园匹,它試圖根據(jù)一個集合是Set蔓纠、List鸣个,還是其他的集合類性羞反,來對它進行分類:
// Broken! - What does this program print?
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));
}
}
你可能期望這個程序會打印初Set,緊接著是List囤萤,以及Unkown Collection,但實際上不是這樣的是趴。它打印了三次Unkown Collection涛舍。為什么會這樣呢?因為classify方法被重載(overloaded)了唆途,而要調(diào)用哪個重載方法是在編譯時做出決定的
富雅。對于for循環(huán)中的全部三次迭代,參數(shù)的編譯時類型都是相同的:Collection<?>肛搬。每次迭代的運行時類型都是不同的没佑,但這并不影響對重載方法的選擇。因為該參數(shù)的編譯時類型為Collection<?>
温赔,所以蛤奢,唯一合適的重載方法是classify(Collection<?>),在循環(huán)的每次迭代中,都會調(diào)用這個重載方法啤贩。
這個程序的行為有違常理待秃,因為對于重載方法的選擇是靜態(tài)的,而對于被覆蓋的方法的選擇則是動態(tài)的
痹屹。選擇被覆蓋的方法的正確版本是在運行時進行的章郁,選擇的依據(jù)是被調(diào)用方法所在對象的運行時類型。這里重新說明一下志衍,當一個子類包含的方法聲明與其祖先類中的方法聲明具有同樣的簽名時暖庄,方法就被覆蓋了。如果實例方法在子類中被覆蓋了楼肪,并且這個方法是在該子類的實例上被調(diào)用的雄驹,那么子類中的覆蓋方法將會執(zhí)行,而不管該子類實例的編譯時類型到底是什么淹辞。為了進行更具體地說明医舆,以下面地程序為例:
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) {
List<Wine> wineList = List.of(new Wine(), new SparklingWine(), new Champagne());
for (Wine wine : wineList)
System.out.println(wine.name());
}
}
name方法是在類Wine中被聲明的,但是在類SparklingWine和Champagne中被覆蓋象缀。正如你所預期的那樣蔬将,這個程序打印出wine、sparkling wine和champagne央星,盡管在循環(huán)的每次迭代中霞怀,實例的編譯時類型都為Wine。當調(diào)用被覆蓋的方法時莉给,對象的編譯時類型不會影響到哪個方法被執(zhí)行毙石;最為具體的那個覆蓋版本總是會得到執(zhí)行。這與重載的情形相比颓遏,對象的運行時類型并不影響哪個重載版本將被執(zhí)行徐矩;選擇工作是在編譯時進行的,完全基于參數(shù)的編譯時類型叁幢。
在CollectionClassifier示例中滤灯,該程序的意圖是:期望編譯器根據(jù)參數(shù)的運行時類型自動將調(diào)用分發(fā)給適當?shù)闹剌d方法,以此來識別出參數(shù)的類型曼玩,就好像Wine的例子中的name方法所做的那樣鳞骤,方法重載機制完全沒有提供這樣的功能。假設(shè)需要有個靜態(tài)方法黍判,這個程序的最佳修正方案是豫尽,用單個方法來替換這三個重載的classify方法,并在這個方法中做一個顯示的instanceof測試:
public static String classify(Collection<?> c) {
return c instanceof Set ?
"Set"
: c instanceof List ?
"List"
: "Unknown Collection";
}
因為覆蓋機制是標準規(guī)范顷帖,而重載機制是例外美旧,所以渤滞,覆蓋機制滿足了人們對方法調(diào)用行為的期望。正如CollectionClassifier例子所示陈症,重載機制很容易使這些期望落空蔼水。如果編寫出來的代碼的行為可能是程序員感到困惑,那么它就是很糟糕的實踐录肯。對于API來說尤其如此趴腋。如果API的普通用戶根本不知道對于一組給定的參數(shù),其中的哪個重載方法會被調(diào)用论咏,那么使用這樣的API就很可能導致錯誤优炬。這些錯誤要等到運行時發(fā)生了怪異的行為之后才會顯現(xiàn)出來,導致許多程序員無法診斷出這樣的錯誤厅贪。因此蠢护,應(yīng)該避免胡亂的使用重載機制
。
到底是什么造成胡亂使用重載機制呢养涮?這個問題仍有爭議葵硕。安全而保守的策略是,永遠不要導出兩個具有相同參數(shù)數(shù)目的重載方法
贯吓。如果方法使用可變參數(shù)懈凹,除第53條中所述的情形之外,保守的策略是根本不要重載它悄谐。如果你遵守這些限制介评,程序員永遠也不會陷入對于任何一組實際的參數(shù),哪個重載方法才是適用的這樣的疑問中爬舰。這項限制并不麻煩们陆,因為你始終可以給方法起不同的名稱,而不使用重載機制
情屹。
例如坪仇,以O(shè)bjectOutputStrema類為例。對于每個基本類型屁商,以及幾種引用類型烟很,它的write方法都有一種變形。這些變形方法并不是重載write方法蜡镶,而是具有諸如writeBoolean(boolean)、writeInt(int) 和 writeLong(long)這樣的簽名恤筛。與重載方案相比較官还,這種命名模式帶來的好處是,可以提供相應(yīng)名稱的讀方法毒坛,比如readBoolean()望伦、readInt()和readLong()林说。實際上,ObjectInputStream類正是提供了這樣的讀方法屯伞。
對于構(gòu)造器腿箩,你沒有選擇使用不同的名稱的機會:一個類的多個構(gòu)造器總是重載的。在許多情況下劣摇,可以選擇導出靜態(tài)工廠珠移,而不是構(gòu)造器(詳見第1條)。對于構(gòu)造器末融,還不用擔心重載和覆蓋的相互影響钧惧,因為構(gòu)造器不可能被覆蓋」聪埃或許你有可能導出多個具有相同參數(shù)數(shù)目的構(gòu)造器浓瞪,所以有必要了解一下如果安全的做到這一點。
如果對于任何一組給定的實際參數(shù)將應(yīng)用于哪個重載方法上始終非常清楚巧婶,那么導出多個具有相同參數(shù)數(shù)目的重載方法就不可能使程序員感到混淆乾颁。對于每一對重載方法,至少有一個對應(yīng)的參數(shù)在兩個重載方法中具有根本不同的類型艺栈,就屬于這種不會感到混淆的情形了英岭。如果顯然不可能把一種類型的實例轉(zhuǎn)換為另一種類型,這兩種類型就是根本不同的眼滤。在這種情況下巴席,一組給定的實際參數(shù)應(yīng)用于哪個重載方法上就完全由參數(shù)的運行時類型來決定,不可能受到其編譯時類型的影響诅需,所以主要的混淆就根本消除了漾唉。例如,ArrayList有一個構(gòu)造器帶有一個int參數(shù)堰塌,另一個構(gòu)造器帶有一個Collection參數(shù)赵刑。難以想象在任何情況下,這兩個構(gòu)造器被調(diào)用時哪一個會產(chǎn)生混淆场刑。
在Java5發(fā)行版本之前般此,所有的基本類型都根本不同于所有的引用類型,但是當自動裝箱出現(xiàn)之后牵现,就不再如此了铐懊,它會導致真正的麻煩。以下面這個程序為例:
public class SetList {
public static void main(String[] args) {
Set<Integer> set = new TreeSet<>();
List<Integer> list = new ArrayList<>();
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);
}
}
首先瞎疼,程序?qū)?3至2之間的整數(shù)添加到了排序好的集合和列表中科乎,然后在集合和列表中進行3次相同的remvoe調(diào)用。如果像大多數(shù)人一樣贼急,希望程序從集合和列表中去除非整數(shù)值(0茅茂、1和2)捏萍,并打印出[-3,-2空闲,-1] [-3令杈,-2,-1]碴倾。事實上逗噩,程序從集合中去除了非整數(shù),還從列表中去除了奇數(shù)值影斑,打印出[-3给赞,-2,-1] [-2矫户,0片迅,2]。我們將這種行為稱之為混亂皆辽,已是保守的說法柑蛇。
實際發(fā)生的情況是:set.remove(i)調(diào)用選擇重載方法remvoe(E),這里的E是集合(Integer)的元素類型驱闷,將i從int自動裝箱到Integer中耻台。這是你所期望的行為,因此程序不會從集合中去除正值空另。另一方面盆耽,list.remove(i)調(diào)用選擇重載方法remvoe(int i),它從列表的指定位置上去除元素扼菠。如果從列表[-3摄杂,-2,-1循榆,0析恢,1,2]開始秧饮,去除第零個元素映挂,接著去除第一個、第二個盗尸,得到的是[-2柑船,0,2]泼各,這個秘密被揭開了椎组。為了解決這個問題,要將lis.remove的參數(shù)轉(zhuǎn)換成Integer历恐,迫使選擇正確的重載方法寸癌。另一種方法是調(diào)用Integer.valueOf(i),并將結(jié)果傳給list.remove弱贼。這兩種方法都如我們所料蒸苇,打印[-3,-2吮旅,-1] [-3溪烤,-2,-1]:
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove((Integer) i); // or remove(Integer.valueOf(i))
}
Thread構(gòu)造器調(diào)用和submit方法調(diào)用看起來很相似庇勃,但前者會進行編譯檬嘀,而后者不會。參數(shù)都是一樣的(System.out::println)责嚷,構(gòu)造器和方法都帶有一個有Runnable的重載鸳兽。這里發(fā)生了什么呢?令人感到意外的是:submit方法有一個帶有Callable<T>的重載罕拂,而Thread構(gòu)造器則沒有揍异。也許你會認為這應(yīng)該沒什么區(qū)別,因為所有的println重載都返回void爆班,因此這個方法引用或許不會是一個Callable衷掷。這種想法是完美的,但是重載方案的算法卻不是這么做的柿菩。也許同樣令人感到驚奇的是戚嗅,如果println方法也沒有被重載,submit方法調(diào)用則是合法的枢舶。這是被引用的方法(println)的重載懦胞,與被調(diào)用方法(submit)的結(jié)合,阻止了重載方案算法按你預期的方式完成祟辟。
從技術(shù)的角度來看医瘫,問題在于,System.out::println是一個不精確的方法引用旧困,而且某些包含隱式類型lambda表達式或者不精確方法引用的參數(shù)表達式會被可用性測試忽略醇份,因為它們的含義會要到選擇好目標類型之后才能確定。如果你不理解這段話的意思也沒關(guān)系吼具,這是針對編譯器作者而言的僚纷。重點是在同一個參數(shù)位置,重載帶有不同函數(shù)接口的方法或者構(gòu)造器會造成混淆拗盒。因此怖竭,不要在相同的參數(shù)位置調(diào)用帶有不同函數(shù)接口的方法
。按照本條目的說法陡蝇,不同的函數(shù)接口并非根本不同痊臭。如果傳入命令行參數(shù):-Xlint:overloads哮肚,Java編譯器會對這種問題的重載發(fā)出警告。
數(shù)組類型和Object之外的類截然不同广匙。數(shù)組類型和Serializable與Cloneable之外的接口也截然不同允趟。如果兩個類都不是對方的后代,這兩種獨特的類就是不相關(guān)的鸦致。例如潮剪,String和Throwable就是不相關(guān)的。任何對象都不可能是兩個不相關(guān)的類的實例分唾,因此不相關(guān)的類也是根本不同的抗碰。
還有其他一些類型對的例子也是不能相互轉(zhuǎn)換的,但是绽乔,一旦超出了上述這些簡單的情形弧蝇,大多數(shù)程序員要想搞清楚一組實際的參數(shù)應(yīng)用于哪個重載方法上
就會非常困難。確定選擇哪個重載方法的規(guī)則是非常復雜的迄汛,這些規(guī)則在每個發(fā)行版本中都變得越來越復雜捍壤。很少有程序員能夠理解其中的所有微妙之處。
有時候鞍爱,尤其是在更新現(xiàn)有類的時候鹃觉,可能會被迫違反本條目的指導原則。例如睹逃,自從Java4發(fā)行版本以來盗扇,String類就已經(jīng)有一個contenEquals(StringBuffer)方法。在Java5版本中沉填,新增了一個稱作CharSequence的接口疗隶,用來為StringBuffer、StringBuilder翼闹、String斑鼻、CharBuffer以及其他類似的類型提供接口。在Java平臺中增加CharSequence的同時猎荠,String也配備了重載的contenEquals方法坚弱,即contenEquals(CharSequence)方法。
盡管這樣的重載顯然違反了本條目的指導原則关摇,但是只要當這兩個重載方法在同樣的參數(shù)上被調(diào)用時荒叶,它們執(zhí)行的是相同的功能,重載就不會帶來危害输虱。程序員可能并不知道哪個重載函數(shù)會被調(diào)用些楣,但只要這兩個方法返回相同的結(jié)果就行。確保這種行為的標準做法是,讓更具體地重載方法把調(diào)用轉(zhuǎn)發(fā)給更一般化的重載方法:
// Ensuring that 2 methods have identical behavior by forwarding
public boolean contentEquals(StringBuffer sb) {
return contentEquals((CharSequence) sb);
}
雖然Java平臺類庫很大程度上遵循了本條目中的建議愁茁,但是也有諸多的類違背了蚕钦。例如,String類導出兩個重載的靜態(tài)工廠方法:valueOf(char[])和valueOf(Object)埋市,當這兩個方法被傳遞了同樣的對象引用時冠桃,它們所做的事情完全不同。沒有正當?shù)睦碛煽梢越忉屵@一點道宅,它應(yīng)該被看作是一種反常行為,有可能會造成真正的混淆胸蛛。
簡而言之污茵,能夠重載方法并不意味就應(yīng)該重載方法。一般情況下葬项,對于多個具有相同參數(shù)數(shù)目的方法來說泞当,應(yīng)該盡量避免重載方法。在某些情況下民珍,特別是涉及構(gòu)造器的時候襟士,要遵循這條建議也許是不可能的。在這種情況下嚷量,至少應(yīng)該避免這樣的情形:同一組參數(shù)只需經(jīng)過類型轉(zhuǎn)換就可以傳遞給不同的重載方法
陋桂。如果不能避免這種情形,例如蝶溶,因為正在改造一個現(xiàn)有的類以實現(xiàn)新的接口嗜历,就應(yīng)該保證:當傳遞同樣的參數(shù)時,所有重載方法的行為必須一致
抖所。如果不能做到這一點梨州,程序員就很難有效的使用被重載的方法或者構(gòu)造器,同時也不能理解它為什么不能正常工作田轧。