天天看點

進口稅

出自《java puzzle》

在5.0版中,Java平台引入了大量的可以使操作數組變得更加容易的工具。下面這個謎題使用了變量參數、自動包裝、靜态導入(請檢視http://java.sun.com/j2se/5.0/docs/guide/language [Java-5.0])以及便捷方法Arrays.toString(請檢視謎題60)。那麼,這個程式會列印什麼呢?

你可能會期望該程式列印[1,2,3,4,5],實際上它确實會這麼做,隻要它能編譯。令人沮喪的是,看起來編譯器找不到恰當的toString方法:

ImportDuty.java:9:Object.toString()can't be applied to(Object[])

System.out.println(toString(args));

^

是不是編譯器的了解力太差了?為什麼它會嘗試着去應用Object.toString()呢?它與調用參數清單并不比對,而Arrays.toString(Object[ ])卻可以完全比對。

編譯器在選擇在運作期将被調用的方法時,所作的第一件事就是在肯定能找到該方法的範圍内挑選[JLS 15.12.1]。編譯器将在包含了具有恰當名字的方法的最小閉合範圍内進行挑選,在我們的程式中,這個範圍就是ImportDuty類,它包含了從Object繼承而來的toString方法。在這個範圍中沒有任何可以應用于toString(args)調用的方法,是以編譯器必須拒絕該程式。

換句話說,我們想要的toString方法沒有在調用點所處的範圍内。導入的toString方法被ImportDuty從Object那裡繼承而來的具有相同名字的方法所遮蔽(shade)了[JLS 6.3.1]。遮蔽與遮掩(謎題68)非常相像,二者的關鍵差別是一個聲明隻能遮蔽類型相同的另一個聲明:一個類型聲明可以遮蔽另一個類型聲明,一個變量聲明可以遮蔽另一個變量聲明,一個方法聲明可以遮蔽另一個方法聲明。與其形成對照的是,變量聲明可以遮掩類型和包聲明,而類型聲明也可以遮掩包聲明。

當一個聲明遮蔽了另一個聲明時,簡單名将引用到遮蔽聲明中的實體。在本例中,toString引用的是從Object繼承而來的toString方法。簡單地說,本身就屬于某個範圍的成員在該範圍内與靜态導入相比具有優先權。這導緻的後果之一就是與Object的方法具有相同名字的靜态方法不能通過靜态導入工具而得到使用。

既然你不能對Arrays.toString使用靜态導入,那麼你就應該用一個普通的導入聲明來代替。下面就是Arrays.toString應該被正确使用的方式:

import java.util.Arrays;

class ImportDuty {

static void printArgs(Object... args) {

System.out.println(Arrays.toString(args));

}

}

如果你特别強烈地想避免顯式地限定Arrays.toString調用,那麼你可以編寫你自己的私有靜态轉發方法:

private static String toString(Object[] a) {

return Arrays.toString(a);

}

靜态導入工具所專門針對的情況是:程式中會重複地使用另一個類的靜态元素,而每一次用到的時候都進行限定又會使程式變得亂成一鍋粥。在這類情況中,靜态導入工具可以顯著地提高可讀性。這比通過實作接口來繼承其常量要安全得多,而實作接口這種做法是你從來都不應該采用的 [EJ Item 17]。然而,濫用靜态導入工具也會損害可讀性,因為這會使得靜态成員的類在何處被使用顯得非常不清晰。應該有節制地使用靜态導入,隻有在非常需要的情況下才應該使用它們。

對API設計者來說,要意識到當某個方法的名字已經出現在某個作用域内時,靜态導入工具并不能被有效地作用于該方法上。這意味着靜态導入不能用于那些與通用接口中的方法共享方法名的靜态方法,而且也從來不能用于那些與Object中的方法共享方法名的靜态方法。再次說明一下,本謎題所要說明的仍然是你在覆寫之外的情況中使用名字重用通常都會産生混亂。我們通過重載、隐藏和遮掩看清楚了這一點,現在我們又通過遮蔽看到了同樣的問題。