1. 程式人生 > >Effective Java 3rd 條目21 為後代設計介面

Effective Java 3rd 條目21 為後代設計介面

在Java8之前,新增方法到介面而沒有破壞已存實現,是不可能的。如果你添加了一個新方法到介面,已存實現通常缺少這個方法,導致編譯時期錯誤。在Java8中,添加了預設方法構造(default method construct)[JLS 9.4],讓方法新增到已存介面。但是新增新方法到已存介面充滿了危險性。

預設方法的宣告包括一個預設實現(default implementation),實現介面的所有類使用了這個預設實現,而不是實現預設方法。雖然預設方法的新增使得新增方法到已存介面變得可能,但是這些方法是否在先存實現中起作用,是沒有保證的。在它們的實現者不知情下,預設方法“注入”到已存實現。早於Java8中,編寫這些實現是完全知情的:它們的介面將不再獲得任何新方法。

許多新預設方法新增到了Java8中的核心集合介面,主要是為了方便lambda的使用(第6章)。Java庫的預設方法是通用目的高質量實現,而且在大多數情況下它們正常工作。但是編寫一個預設方法,這個方法維護了每個可能實現的所有變數,這不總是可行的

例如,考慮removeIf方法,在Java8中它被新增到Collection介面。這個方法移除給定boolean函式(或者謂詞(predicate))返回true的所有元素。預設實現被指定使用它的迭代器遍歷這個集合,在每個元素上呼叫這個謂詞,使用這個迭代器的remove方法移除謂詞返回true的元素。這個宣告大概看上去如下:

// 在Java8中新增到Collection介面的預設方法
default boolean removeIf(Predicate<? super E> filter) { Objects.requireNonNull(filter); boolean result = false; for (Iterator<E> it = iterator(); it.hasNext(); ) { if (filter.test(it.next())) { it.remove(); result = true; } } return
result; }

這是為removeIf方法可以編寫的通用目的最好實現,但是可悲的是,它在一些現實的Collection實現上失敗了。例如,考慮org.apache.commons.collections4.collection.SynchronizedCollection。這個類是來自Apache Commons庫中,和java.util中Collections.synchronizedCollection靜態工廠返回的那個,是相似的。Apache版本為鎖定額外提供了這個功能:代替集合,使用客戶端提供的物件。換句話說,它是一個包裝類(條目18),它的所有方法,在代理到包裝的集合之前,在一個鎖定物件上同步。

Apache SynchronizedCollection類還在積極地維護著,但是本文撰寫時,它沒有覆寫removeIf方法。如果這個類連同Java8一起使用,那麼因此它將會繼承removeIf的預設實現,這沒有,實際上不能,維持這個類的基本承諾:為每個方法呼叫自動同步。這個預設實現不知道同步,而且不能夠訪問包含鎖定物件的域。如果一個客戶端,面對由另外執行緒並行修改這個集合時,呼叫SynchronizedCollection例項的removeIf方法,那麼可能導致SynchronizedCollection或者其他未指定行為。

在相似的Java平臺庫實現中,比如,Collections.synchronizedCollection返回的包私有類,為了防止這個發生,JDK維護者不得不覆寫預設removeIf實現和像這個方法的其他方法,在呼叫預設實現之前進行必要的同步。不屬於Java平臺部分的先存集合實現,沒有機會隨著介面改變做相似的修改,所以其他的人不得不這麼做。

對於預設方法,一個介面的現存實現可能正常編譯,沒有錯誤或者警告,但是在執行時失敗。雖然不是非常常見,但是這個問題也不是一個孤立的事件。新增到Java8中集合介面的一些方法,被認為是易感染的,而且一些已存實現被認為是受感染的。

通常應該避免使用預設方法新增新方法到已存介面,除非這個需求是緊要關頭,這種情況下你應該仔細考慮,預設方法實現是否破壞已存介面實現。然而預設方法是相當有用的,當介面建立時它提供了標準方法實現,以減輕實現這個介面的任務(條目20)。

還值得注意的是,預設方法不是設計來支援從介面中移除方法或者改變已存方法的簽名。這些介面改變都是不可能沒有破壞已存客戶端的。

這個寓意是明確的。即使預設方法現在是Java平臺的一部分,但是非常小心設計介面仍舊至關重要。雖然預設方法使得新增放到已存介面變得可能,但是這樣做有很大的危險。如果一個介面含有一個微小的缺陷,那麼這可能永遠會激怒它的使用者;如果介面有嚴重缺陷,那麼它可能使得包含它的API失敗。

所以,在釋出介面之前,測試每個新介面極其重要。許多程式設計師應該以不同的方式實現每個介面。至少,你應該爭取三個不同的實現。同樣重要的是,編寫多個客戶端程式,使用每個新介面的例項執行各種任務。保證每個介面滿足它的所有預期用途,有很長的路要走。這些步驟讓你在釋出介面之前發現它們中的錯誤,而且你仍舊可以輕易地改正。雖然在一個介面釋出之後改正一個介面缺陷是可能的,但是你不可能依靠於此