java4個技巧:從繼承和覆蓋,到最終的類和方法

日復一日,我們編寫的大多數Java只使用了該語言全套功能的一小部分。我們實例化的每個流以及我們在實例變量前面加上的每個@Autowired註解都足以完成我們的大部分目標。然而,有些時候,我們必須求助於語言中那些很少使用的部分:語言中為特定目的而隱藏的部分。

本文探索了四種技術,它們可以在綁定時使用,並將其引入到代碼庫中,以提高開發的易用性和可讀性。並非所有這些技術都適用於所有情況,甚至大多數情況。例如,可能只是有一些方法,只會讓自己協變返回類型或一些泛型類適合使用區間的泛型類型的模式,而其他人,如最終方法和類和try-with-resources塊,將提高可讀性和清潔度的大多數種代碼基底的意圖。無論哪種情況,重要的是不僅要知道這些技術的存在,還要知道何時明智地應用它們。

java4個技巧:從繼承和覆蓋,到最終的類和方法

1. 協變返回類型

即使是最介紹性的Java操作手冊也會包含關於繼承、接口、抽象類和方法重寫的內容,但是即使是高級的文本也很少會在重寫方法時探索更復雜的可能性。例如,下面的代碼片段即使是最初級的Java開發人員也不會感到驚訝:

public interface Animal {    public String makeNoise();}public class Dog implements Animal {    @Override    public String makeNoise() {        return "Woof";    }}public class Cat implements Animal {    @Override    public String makeNoise() {        return "Meow";    }}

這是多態性的基本概念:可以根據對象的接口(Animal::makeNoise)調用方法,但是方法調用的實際行為取決於實現類型(Dog::makeNoise)。例如,下面方法的輸出會根據是否將Dog對象或Cat對象傳遞給該方法而改變:

public class Talker {    public static void talk(Animal animal) {        System.out.println(animal.makeNoise());    }}Talker.talk(new Dog()); //輸出:低音Talker.talk(new Cat()); //輸出:喵

雖然這是許多Java應用程序中常用的一種技術,但是在重寫方法時可以採取一個不太為人所知的操作:更改返回類型。雖然這看起來是一種覆蓋方法的開放方式,但是對於被覆蓋方法的返回類型有一些嚴格的限制。根據Java 8 SE語言規範(第248頁):

如果方法聲明d 1返回類型為R 1覆蓋或隱藏另一個方法d的聲明 2返回類型為R 2,那麼d 1是否可以用返回類型替換d 2,或者出現編譯時錯誤。

返回類型替換表(同上,第240頁)定義為

  1. 如果R1是空的,那麼R2無效
  2. 如果R1那麼原始類型是R嗎2等於R1
  3. 如果R1是一種引用類型,則下列其中之一為真:R1適用於d的類型參數2是R的子類型嗎2.R1可以轉換為R的子類型嗎2通過無節制的轉換d1沒有與d相同的簽名嗎2和R1R = |2|

可以說最有趣的案例是規則3.a。和3. b。:重寫方法時,可以將返回類型的子類型聲明為被重寫的返回類型。例如:

public interface CustomCloneable {    public Object customClone();}public class Vehicle implements CustomCloneable {    private final String model;    public Vehicle(String model) {        this.model = model;    }    @Override    public Vehicle customClone() {        return new Vehicle(this.model);    }    public String getModel() {        return this.model;    }}Vehicle originalVehicle = new Vehicle("Corvette");Vehicle clonedVehicle = originalVehicle.customClone();System.out.println(clonedVehicle.getModel());

雖然clone()的原始返回類型是Object,但是我們能夠在克隆的車輛上調用getModel()(不需要顯式的強制轉換),因為我們已經將Vehicle::clone的返回類型重寫為Vehicle。這消除了對混亂類型強制轉換的需要,我們知道我們要尋找的返回類型是一個載體,即使它被聲明為一個對象(這相當於基於先驗信息的安全類型強制轉換,但嚴格來說是不安全的):

Vehicle clonedVehicle = (Vehicle) originalVehicle.customClone();

注意,我們仍然可以將車輛類型聲明為對象,而返回類型將恢復為對象的原始類型:

Object clonedVehicle = originalVehicle.customClone();System.out.println(clonedVehicle.getModel()); //錯誤:getModel不是一個對象方法

注意,對於泛型參數不能重載返回類型,但是對於泛型類可以重載。例如,如果基類或接口方法返回一個列表 ,則可以將子類的返回類型重寫為ArrayList ,但不能將其重寫為List 。

2. 區間的泛型類型

創建泛型類是創建一組以類似方式與組合對象交互的類的最佳方法。例如,一個列表 只是存儲和檢索類型為T的對象,而不瞭解它所包含元素的性質。在某些情況下,我們希望約束泛型類型參數(T)使其具有特定的特徵。例如,給定以下接口

public interface Writer {    public void write(); }

我們可能想創建一個特定的作家集合如下與複合模式:

public class WriterComposite implements Writer {    private final List writers;    public WriterComposite(List writers) {        this.writers = writer;    }    @Override    public void write() {        for (Writer writer: this.writers) {            writer.write();         }    }} 

我們現在可以遍歷一個Writer樹,不知道我們遇到的特定Writer是一個獨立的Writer(一個葉子)還是一個Writer集合(一個組合)。如果我們還想讓我們的組合作為讀者和作者的組合呢?例如,如果我們有以下接口

public interface Reader {    public void read(); }

如何將WriterComposite修改為ReaderWriterComposite?一種技術是創建一個新的接口ReaderWriter,將Reader和Writer接口融合在一起:

public interface ReaderWriter extends Reader, Writer {}

然後我們可以修改現有的WriterComposite如下:

public class ReaderWriterComposite implements ReaderWriter {    private final List readerWriters;    public WriterComposite(List readerWriters) {        this.readerWriters = readerWriters;    }    @Override    public void write() {        for (Writer writer: this.readerWriters) {            writer.write();         }    }    @Override    public void read() {        for (Reader reader: this.readerWriters) {            reader.read();         }    }}

雖然這確實實現了我們的目標,但是我們在代碼中創建了膨脹:我們創建了一個接口,其惟一目的是將兩個現有接口合併在一起。隨著接口越來越多,我們可以開始看到膨脹的組合爆炸。例如,如果我們創建一個新的修飾符接口,我們現在需要創建ReaderModifier、WriterModifier和ReaderWriter接口。注意,這些接口沒有添加任何功能:它們只是合併現有的接口。

為了消除這種膨脹,我們需要能夠指定ReaderWriterComposite接受泛型類型參數(當且僅當它們既是讀寫器又是寫器時)。交叉的泛型類型允許我們這樣做。為了指定泛型類型參數必須實現讀寫接口,我們在泛型類型約束之間使用&操作符:

public class ReaderWriterComposite implements Reader, Writer {    private final List readerWriters;    public WriterComposite(List readerWriters) {        this.readerWriters = readerWriters;    }    @Override    public void write() {        for (Writer writer: this.readerWriters) {            writer.write();         }    }    @Override    public void read() {        for (Reader reader: this.readerWriters) {            reader.read();         }    }}

在不擴展繼承樹的情況下,我們現在可以約束泛型類型參數來實現多個接口。注意,如果其中一個接口是抽象類或具體類,則可以指定相同的約束。例如,如果我們將Writer接口更改為類似下面的抽象類。

public abstract class Writer {    public abstract void write();}

我們仍然可以約束我們的泛型類型參數是讀者和作家,但是作者(因為它是一個抽象類,而不是一個接口)必須首先指定(也請注意,我們現在ReaderWriterComposite擴展了寫信人抽象類並實現了接口,而不是實現兩個):

public class ReaderWriterComposite extends Writer implements Reader {  //與前面一樣的類} 

還需要注意的是,這種交互的泛型類型可以用於兩個以上的接口(或一個抽象類和多個接口)。例如,如果我們想要我們的組合也包括修飾符接口,我們可以寫我們的類定義如下:

public class ReaderWriterComposite implements Reader, Writer, Modifier {    private final List things;    public ReaderWriterComposite(List things) {        this.things = things;    }    @Override    public void write() {        for (Writer writer: this.things) {            writer.write();        }    }    @Override    public void read() {        for (Reader reader: this.things) {            reader.read();        }    }    @Override    public void modify() {        for (Modifier modifier: this.things) {            modifier.modify();        }    }}

儘管執行上述操作是合法的,但這可能是代碼氣味的一種標誌(作為讀取器、寫入器和修飾符的對象可能是更具體的東西,比如文件)。

有關交互式泛型類型的更多信息,請參見Java 8語言規範。

java4個技巧:從繼承和覆蓋,到最終的類和方法

3.Auto-Closeable類

創建資源類是一種常見的實踐,但是維護資源的完整性可能是一個具有挑戰性的前景,特別是在涉及異常處理時。例如,假設我們創建了一個資源類resource,並希望對該資源執行一個可能拋出異常的操作(實例化過程也可能拋出異常):

public class Resource {    public Resource() throws Exception {        System.out.println("Created resource");    }    public void someAction() throws Exception {        System.out.println("Performed some action");    }    public void close() {        System.out.println("Closed resource");    }}

無論是哪種情況(如果拋出或不拋出異常),我們都希望關閉資源以確保沒有資源洩漏。正常的過程是將我們的close()方法封裝在finally塊中,確保無論發生什麼,我們的資源在封閉的執行範圍完成之前是關閉的:

Resource resource = null;try {    resource = new Resource();    resource.someAction();} catch (Exception e) {    System.out.println("Exception caught");}finally {    resource.close();}

通過簡單的檢查,有很多樣板代碼會降低對資源對象執行someAction()的可讀性。為了糾正這種情況,Java 7引入了try-with-resources語句,通過該語句可以在try語句中創建資源,並在保留try執行範圍之前自動關閉資源。要使類能夠使用try-with-resources,它必須實現AutoCloseable接口:

public class Resource implements AutoCloseable {    public Resource() throws Exception {        System.out.println("Created resource");    }    public void someAction() throws Exception {        System.out.println("Performed some action");    }    @Override    public void close() {        System.out.println("Closed resource");    }}

我們的資源類現在實現了AutoCloseable接口,我們可以清理我們的代碼,以確保我們的資源是關閉之前離開的嘗試執行範圍:

try (Resource resource = new Resource()) {    resource.someAction();} catch (Exception e) {    System.out.println("Exception caught");}

與不使用資源進行嘗試的技術相比,此過程要少得多,並且維護了相同的安全性(在完成try執行範圍後,資源總是關閉的)。如果執行上述try-with-resources語句,則得到以下輸出:

Created resourcePerformed some actionClosed resource

為了演示這種使用資源的嘗試技術的安全性,我們可以更改someAction()方法來拋出一個異常:

public class Resource implements AutoCloseable {    public Resource() throws Exception {        System.out.println("Created resource");    }    public void someAction() throws Exception {        System.out.println("Performed some action");        throw new Exception();    }    @Override    public void close() {        System.out.println("Closed resource");    }}

如果我們再次運行try-with-resources語句,我們將得到以下輸出:

Created resourcePerformed some actionClosed resourceException caught

注意,即使在執行someAction()方法時拋出了一個異常,我們的資源還是關閉了,然後捕獲了異常。這確保在離開try執行範圍之前,我們的資源被保證是關閉的。同樣重要的是,資源可以實現close - able接口,並且仍然使用try-with-resources語句。實現AutoCloseable接口和Closeable接口之間的區別在於close()方法簽名拋出的異常類型:exception和IOException。在我們的例子中,我們只是更改了close()方法的簽名,以避免拋出異常。

4. 最後的類和方法

在幾乎所有的情況下,我們創建的類都可以由另一個開發人員擴展並定製以滿足該開發人員的需求(我們可以擴展自己的類),即使我們並沒有打算要擴展我們的類。雖然這對於大多數情況已經足夠了,但是有時我們可能不希望覆蓋某個方法,或者更一般地說,擴展某個類。例如,如果我們創建一個文件類,封裝了文件系統上的文件的閱讀和寫作,我們可能不希望任何子類覆蓋讀(int字節)和寫(字符串數據)方法(這些方法中的邏輯是否改變,它可能導致文件系統會損壞)。在這種情況下,我們將不可擴展的方法標記為final:

public class File {    public final String read(int bytes) {       //對文件系統執行讀操作        return "Some read data";    }    public final void write(String data) {      //執行對文件系統的寫操作    }}

現在,如果另一個類希望覆蓋讀或寫方法,則會拋出編譯錯誤:無法覆蓋文件中的最終方法。我們不僅記錄了不應該重寫我們的方法,而且編譯器還確保了在編譯時強制執行這個意圖。

將這個想法擴展到整個類,有時我們可能不希望我們創建的類被擴展。這不僅使類的每個方法都不可擴展,而且還確保了類的任何子類型都不會被創建。例如,如果我們正在創建一個使用密鑰生成器的安全框架,我們可能不希望任何外部開發人員擴展我們的密鑰生成器並覆蓋生成算法(自定義功能可能在密碼方面較差並危及系統):

public final class KeyGenerator {    private final String seed;    public KeyGenerator(String seed) {        this.seed = seed;    }    public CryptographicKey generate() {      //…做一些加密工作來生成密鑰…    }}

通過使我們的KeyGenerator類成為final,編譯器將確保沒有類可以擴展我們的類並將自己作為有效的密鑰生成器傳遞給我們的框架。雖然簡單地將generate()方法標記為final似乎就足夠了,但這並不能阻止開發人員創建自定義密鑰生成器並將其作為有效的生成器傳遞。由於我們的系統是面向安全的,所以最好儘可能地不信任外部世界(如果我們提供了KeyGenerator類中的其他方法,聰明的開發人員可能會通過更改它們的功能來更改生成算法)。

儘管這看起來是對開放/封閉原則的公然漠視(事實的確如此),但這樣做是有充分理由的。正如我們在上面的安全性示例中所看到的,很多時候,我們無法允許外部世界對我們的應用程序做它想做的事情,我們必須在關於繼承的決策中非常慎重。像Josh Bolch這樣的作者甚至說,一個類應該被有意地設計成可擴展的,或者應該顯式地對擴展關閉(有效的Java)。儘管他故意誇大了這個想法(參見記錄繼承或不允許繼承),但他提出了一個很好的觀點:我們應該仔細考慮哪些類應該擴展,哪些方法可以重寫。

結論

雖然我們編寫的大多數代碼只利用了Java的一小部分功能,但它足以解決我們遇到的大多數問題。有時候,我們需要更深入地研究語言,重新拾起那些被遺忘或未知的部分來解決特定的問題。其中一些技術,如協變返回類型和交互式泛型類型,可以在一次性的情況下使用,而其他技術,如自動關閉的資源和最終方法和類,可以而且應該更頻繁地使用,以生成更具可讀性和更精確的代碼。將這些技術與日常編程實踐相結合不僅有助於更好地理解我們的意圖,而且有助於更好地編寫更好的Java。

本文譯自:Catalogic軟件公司軟件工程師Justin Albano的博客。


分享到:


相關文章: