JDK中居然也有反模式接口常量

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在實際開發過程中,經常會需要定義一個文件,用於存儲一些常量,這些常量設計爲靜態公共常量(使用 "},{"type":"codeinline","content":[{"type":"text","text":"public static final"}]},{"type":"text","text":" 修飾)。這個時候就出現兩種選擇:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"在接口中定義常量,比如 JDK 1.1 中的 "},{"type":"codeinline","content":[{"type":"text","text":"java.io.ObjectStreamConstans"}]},{"type":"text","text":" 接口;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"在類中定義常量,比如 JDK 1.7 中的 "},{"type":"codeinline","content":[{"type":"text","text":"java.nio.charset.StandardCharsets"}]},{"type":"text","text":";"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這兩種方式都能夠達到要求:存儲常量、無需實例化。下面分情況討論下兩種方式孰優孰劣。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"常量接口"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先從代碼的角度分析,接口中定義的變量都必須是常量,即默認使用 "},{"type":"codeinline","content":[{"type":"text","text":"public static final"}]},{"type":"text","text":" 修飾。也就是說,在寫代碼的時候直接寫成下面這樣:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public interface ObjectStreamConstants {\n short STREAM_MAGIC = (short)0xaced;\n short STREAM_VERSION = 5;\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是在類中就必須乖乖的寫成下面這種樣子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public final class ObjectStreamConstants {\n public static final short STREAM_MAGIC = (short)0xaced;\n public static final short STREAM_VERSION = 5;\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從直觀的感受,接口寫起來方便多了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二個問題:因爲類中寫的字符比接口多,所以編譯之後文件大小也是類文件比接口文件大。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第三個問題:在JVM加載過程中,接口沒有類提供的額外特種(如重載、方法的動態綁定等),所以接口加載比類快。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"分析到此,似乎沒有什麼理由不用接口定義常量了。但是,BUT,這種做法卻是一種嚴重的"},{"type":"text","marks":[{"type":"strong"}],"text":"反模式"},{"type":"text","text":"行爲。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"引用《Effective Java》中的一段描述:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"The constant interface pattern is a poor use of interfaces. That a class uses some constants internally is an implementation detail. Implementing a constant interface causes this implementation detail to leak into the class's exported API. It is of no consequence to the users of a class that the class implements a constant interface. In fact, it may even confuse them. Worse, it represents a commitment: if in a future release the class is modified so that it no longer needs to use the constants, it still must implement the interface to ensure binary compatibility. If a nonfinal class implements a constant interface, all of its subclasses will have their namespaces polluted by the constants in the interface."}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"翻譯出來就是:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"常量接口模式是對接口的不良使用。類在內部使用某些常量,這純粹是實現細節。實現常量接口會導致把這樣的實現細節泄露到該類的導出API中。類實現常量接口,這對於類的用戶來講並沒有什麼價值。實際上,這樣做反而會使他們更加糊塗。更糟糕的是,它代表了一種承諾:如果在將來的發行版本中,這個類被修改了,它不再需要使用這些常量了,它依然必須實現這個接口,以確保兼容性。如果非final類實現了常量接口,它的所有子類的命名空間也會被接口中的常量所“污染”。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣說來就很明白透徹了:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"接口是不能阻止被實現或繼承的,也就是說子接口或實現中是能夠覆蓋掉常量的定義,這樣通過父、子接口(或實現) 去引用常量是可能不一致的;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"同樣的,由於被實現或繼承,造成在繼承樹中可以用大量的接口、類或實例去引用同一個常量,從而造成接口中定義的常量污染了命名空間;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"接口暗含的意思是:它是需被實現的,代表着一種類型,它的公有成員是要被暴露的API,但是在接口中定義的常量還算不上API。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"綜上所述:使用接口定義常量,是一種不可取的行爲。JDK中定義的接口常量(例如"},{"type":"codeinline","content":[{"type":"text","text":"java.io.ObjectStreamConstans"}]},{"type":"text","text":")應該算是反面教材。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"類接口"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然使用接口第一常量不可取,那就只能通過類定義常量了。雖然在JAVA中類不能夠多繼承,但是子類也能夠“污染”父類定義常量的命名空間。所以爲了"},{"type":"text","marks":[{"type":"strong"}],"text":"常量不可變"},{"type":"text","text":",需要將常量類定義爲"},{"type":"codeinline","content":[{"type":"text","text":"final"}]},{"type":"text","text":"的,然後再徹底點就是再定義個"},{"type":"codeinline","content":[{"type":"text","text":"private"}]},{"type":"text","text":"的構造函數。就像"},{"type":"codeinline","content":[{"type":"text","text":"java.nio.charset.StandardCharsets"}]},{"type":"text","text":"一樣:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public final class StandardCharsets {\n private StandardCharsets() {\n throw new AssertionError(\"No java.nio.charset.StandardCharsets instances for you!\");\n }\n public static final Charset US_ASCII = Charset.forName(\"US-ASCII\");\n public static final Charset ISO_8859_1 = Charset.forName(\"ISO-8859-1\");\n public static final Charset UTF_8 = Charset.forName(\"UTF-8\");\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在"},{"type":"codeinline","content":[{"type":"text","text":"java.nio.charset.StandardCharsets"}]},{"type":"text","text":"中,爲了阻止各種形式的實例化,甚至在構造函數中拋出錯誤,也是做個夠徹底的了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"枚舉類型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是,BUT,還有一種情況,比如常量中定義性別:男、女,使用上面的類常量,需要寫成:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public final class Gender {\n private Gender() {\n throw new AssertionError(\"No x.y.z.Gender instances for you!\");\n }\n public static final int MALE = 1;\n public static final int FEMALE = 0;\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲定義的性別類型實際是int,如果手賤寫成"},{"type":"codeinline","content":[{"type":"text","text":"m.setGender(3)"}]},{"type":"text","text":"也是沒有錯誤的,那"},{"type":"codeinline","content":[{"type":"text","text":"3"}]},{"type":"text","text":"又是什麼鬼?是不是還要有"},{"type":"codeinline","content":[{"type":"text","text":"4"}]},{"type":"text","text":"、"},{"type":"codeinline","content":[{"type":"text","text":"5"}]},{"type":"text","text":"、"},{"type":"codeinline","content":[{"type":"text","text":"6"}]},{"type":"text","text":"、"},{"type":"codeinline","content":[{"type":"text","text":"7"}]},{"type":"text","text":"? 那這種常量定義就失去價值了。對於這種可以歸類的常量,最好的常量定義方法應該就是枚舉了:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public enum Gender {\n MALE, \n FEMALE\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據編輯的字節碼,Gender實際是:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public final class Gender extends java.lang.Enum {\n public static final Gender MALE;\n public static final Gender FEMALE;\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣對於接受 Gender 類型參數的方法就只能傳入 MALE 或 FEMALE 了,不再有其他選項,這就是枚舉的意義。在後來的JDK中,也出現了像"},{"type":"codeinline","content":[{"type":"text","text":"java.nio.file.StandardOpenOption"}]},{"type":"text","text":"等的枚舉定義。而且枚舉的定義也不只侷限於這種,還有很多其他複雜定義,可以適用各種情況,以後再慢慢討論。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"結束語"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"定義常量不要使用接口常量,要在類中定義,最好是final類,並且定義private的構造方法,如果常量可以進行歸類,最好使用枚舉定義:枚舉 > 類 > 接口。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實從這段代碼也能看出,一個匠人也是從菜鳥成長起來的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/9e/9e305acd1cca75053c144cb28adc6061.png","alt":"公衆號:看山的小屋","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章