JAVA SE(二十二)—— Effective Java 之你真的會創建和銷燬對象嗎?

一、創建和銷燬對象

1.1 用靜態工廠方法代替構造器

1.1.1 靜態工廠方法說明
對於類而言,爲了讓客戶端獲取它自身的一個實例,最常用的方法就是提供一個公有的構造器。除此之外,還可以考慮用類提供一個公有的靜態工廠方法(static factory method),這個方法只是一個返回類的實例的靜態方法。示例如下

public static Boolean valueOf(boolean b) {
	return b ? Boolean.TRUE : Boolean.FALSE;
}

這個方法將 boolean 基本類型值轉換成了一個 Boolean 對象引用。

1.1.2 使用靜態工廠方法的優點
(1)靜態工廠方法具有名稱
如果構造器的參數本身沒有確切地描述正被返回的對象,那麼具有適當名稱的靜態工廠會更容易使用,產生的客戶端代碼也更易於閱讀。例如,構造器 BigInteger(int, int, Random) 返回的 BigInteger 可能爲素數,如果用名爲 BigInteger.probablePrime 的靜態工廠方法來表示,顯然更爲清楚。

一個類只能有一個帶有指定簽名的構造器,所以一般會通過提供兩個構造器,它們的參數列表只在參數類型的順序上有所不同,但是在實際中當構造器比較多的時候就相當不方便,往往還會造成不必要的錯誤。

(2)靜態工廠方法不必在每次調用它們的時候都創建一個新對象
靜態工廠方法能夠爲重複的調用返回相同的對象,這樣有助於類總能嚴格控制在某個時刻哪些實例應該存在,使得不可變類以使用預先構建好的實例,或者將構建好的實例緩存起來,進行重複利用,從而避免創建不必要的重複對象。

(3)靜態工廠方法可以返回原返回類型的任何子類型的對象
API可以返回對象,同時又不會使對象的類變成公有的,以這種方式隱藏實現類會時API變得非常簡潔,也使得我們在選擇返回對象的類時就有了更大的靈活性。

(4)靜態工廠方法所返回的對象的類可以隨着每次調用而發生變化,這取決於靜態工廠方法的參數值。
只要是已聲明的返回類型的子類型,都是允許的。爲了提升軟件的可維護性和性能,返回對象的類也可能隨着發行版本的不同而不同。

(5)靜態工程方法返回的對象所屬的類,在編寫包含該靜態工廠方法的類時可以不必存在。
這種靈活的靜態工廠方法構成了服務提供者框架(Service Provider Framework)的基礎,例如JDBC(Java數據庫連接, Java Databse Connectivity)API。服務提供者框架是指這樣一個系統:多個服務提供者實現一個服務,系統爲服務提供者的客戶端提供多個實現,並把他們從多個實現中解耦出來。

服務提供者框架中有三個重要的組件:服務接口(Service Interface),這是提供者實現的;提供者註冊API(Provider Registration API),這是系統用來註冊實現,讓客戶端訪問它們的;服務訪問API(Service Access API),是客戶端用來獲取服務的實例的。服務訪問API一般允許但是不要求客戶端指定某種選擇提供者的條件。如果沒有這樣的規定,API就會返回默認實現的一個實例。服務訪問API是“靈活的靜態工廠”,它構成了服務提供者框架的基礎。

服務提供者框架模式有着無數種變體,下面是一個簡單的實現示例,包含一個服務提供者接口和一個默認提供者:
Service接口

public interface Service {

}

Provider接口

public interface Provider {
	Service newService();
}

Services類

public class Services {
	private Services() {} // Prevents instantiation (Item 4)
	
	private static final Map<String, Provider> providers = new ConcurrentHashMap<String, Provider>();
	public static final String DEFAULT_PROVIDER_NAME = "<def>";
	
	public static void registerDefaultProvider(Provider p) {
		registerProvider(DEFAULT_PROVIDER_NAME, p);
	}
	public static void registerProvider(String name, Provider p) {
		providers.put(name, p);
	}
	
	public static Service newInstance() {
		return newInstance(DEFAULT_PROVIDER_NAME);
	}
	public static Service newInstance(String name) {
		Provider p = providers.get(name);
		if (p == null)
			throw new IllegalArgumentException("No provider registered with name: " + name);
		return p.newService();
	}
}

1.1.3 使用靜態工廠方法的缺點
(1)類如果不含公有的或者受受保護的構造器,就不能被子類化。
例如,想要將Collection Framework中的任何便利的實現類子類化都市不可能的。

(2)靜態工程方法很難被發現
在開發API文檔中,構造器有明確的標識和使用方法,但是靜態工廠方法沒有,所以對於提供了靜態工廠方法而不是構造器的類來說,想要查明如何實例化一個類就比較困難。

下面時靜態工廠方法的一些慣用名稱:
from:類型轉換方法,它只有單個參數,返回該類型的一個相對應的實例。

Date d = Date.from(instant);

of:聚合方法,帶有多個參數,返回該類型的一個實例,並把它們合併起來

Set<Name> names = EnumSet.of(Jack,Bob,Frank);

valueOf:比from和of更繁瑣的一種替代方法,該方法返回的實例與它的參數具有相同的值,這種靜態工廠方法實際上是類型轉換方法。

BigInteger param = BigInteger.valueOf(Integer.MAX_VALUE)

instance/getInstance:返回的實例是通過方法的參數來描述的,但是不能夠說與參數具有同樣的值。對於 Singleton 來說,該方法沒有參數,並返回唯一的實例。

StackWalker sw = StackWalker.getInstance(options);

create/newInstance:像 instance 或者 getInstance 一樣,但 create 或者 newInstance 能夠確保每次調用返回的實例都是一個新的實例。

Object newArray = Array.newInstance(classObject,arrayLen);

getType:像 getInstance 一樣,但是在工廠方法處於不同的類中的時候使用。Type表示工廠方法所返回的對象類型。

FileStore fs = Files.getFileStore(path);

newType:像 newInstance 一樣,但是在工廠方法處於不同的類中的時候使用。Type表示工廠方法所返回的對象類型。

BufferedReader br = Files.newBufferedReader(path);

type:getType和newType的簡潔版。

List<Complaint> list = Collections.list(legacyLitany);

1.2 遇到多個構造器參數時考慮是應用構建器

1.2.1 重疊構造器模式
靜態工廠和構造器有個共同的侷限性,即它們都不能很好地擴展到大量的可選參數。對於這種情況,一般可以採用重疊構造器(telescoping constructor)模式,在這種模式下,你提供第一個只有必要參數的構造器,第二個構造器有一個可選參數,第三個有兩個可選參數,以此類推,最後一個構造器包含所有可選參數。

重疊構造器模式示例

/**
 * 重疊構造器模式
 */
public class TCEntity {
    private int id;         //必選參數
    private String name;    //必選參數
    private int age;
    private String sex;
    private String city;
    private long phone;

    /* 只有兩個必選參數 */
    public TCEntity (int id, String name) {
        this.id = id;
        this.name = name;
    }

    /* 包含一個可選參數 */
    public TCEntity (int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    /* 包含兩個可選參數 */
    public TCEntity (int id, String name, int age, String sex) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    /* 包含三個可選參數 */
    public TCEntity (int id, String name, int age, String sex, String city) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.sex = sex;
        this.city = city;
    }

    /* 包含四個可選參數 */
    public TCEntity (int id, String name, int age, String sex, String city, long phone) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.sex = sex;
        this.city = city;
        this.phone = phone;
    }
}

當想要創建對象時,就直接調用列表中的構造器,示例如下

public class CreateObject {
    public static void main(String[] args) {
        TCEntity tcEntity = new TCEntity (1,"JACK",0,"MAN","",1212123);
    }
}

當我們在調用這個構造器的時候,這個構造器包含一些我們並不想設置的參數如age、city,但還是不得不爲他們傳遞值。在這個例子中,我們給 age 傳遞了一個值爲0,給 city 傳遞了一個空值,這就使得創建對象變得麻煩和消耗資源,當參數較少的時候還能清楚的知道每個參數應該如何傳遞,當參數很多的時候,創建對象就會變得難以控制。

重疊構造器模式可行,但是當有許多參數的時候,客戶端代碼會很難編寫,並且仍然較難以閱讀。如果使用者想知道那些值是什麼意思,必須很仔細地數着這些參數來探個究竟。一長串類型相同的參數會導致一些微妙的錯誤。如果客戶端不小心顛倒了其中兩個參數的順序,編譯器也不會出錯,但是程序在運行時會出現錯誤的行爲。

1.2.2 JavaBeans模式
遇到許多構造參數的時候,還有第二種代替辦法,即JavaBeans模式,在這種模式下,調用一個無參構造器來創建隊形,然後調用setter方法來設置每個必要的參數,以及每個相關的可選參數。

JavaBeans模式示例

/**
 * JavaBeans模式
 */
public class JBEntity {
    private int id;         //必選參數
    private String name;    //必選參數
    private int age;
    private String sex;
    private String city;
    private long phone;

    /* 無參構造 */
    public JBEntity() {
    }

    /* Setters*/
    public void setId(int id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public void setPhone(long phone) {
        this.phone = phone;
    }
}

創建對象示例

public class CreateObject {
    public static void main(String[] args) {
        JBEntity jbEntity = new JBEntity();
        jbEntity.setId(1);
        jbEntity.setName("ROSE");
        jbEntity.setAge(18);
        jbEntity.setSex("WOMAN");
        jbEntity.setCity("LONDON");
        jbEntity.setPhone(1212122);
    }
}

從上述的示例中可以看出,JavaBeans模式有效的彌補了重疊構造器模式的不足,使創建對象變容易,傳遞參數也簡單易讀。

但是JavaBeans模式自身也有着很嚴重的缺點,因爲構造過程被分到了幾個調用中,在構造過程中JavaBean可能處於不一致的狀態,類無法僅僅通過檢驗構造器參數的有效性來保證一致性。當試圖使用處於不一致狀態的對象,將會導致失敗,這種失敗與包含錯誤的代碼大相徑庭,因此它調試起來十分困難。與此相關的另一點不足在於,JavaBeans模式阻止了把類做成不可變的可能,這就需要我們付出額外的努力來確保它的線程安全。

當對象的構造完成,並且不允許在解凍之前使用時,通過手工“凍結”對象,可以彌補這些不足,但是這種方式十分笨拙,在實踐很少使用。此外,它甚至會在運行時導致錯誤,因爲編譯器無法確保程序員會在使用之前先在對象上調用freeze方法。

1.2.3 Builder模式
上述兩種方法創建對象都有較大的弊端,於是我們可以考慮第三種方法,這種方法既能保證像重疊構造器模式那樣的安全性,也能保證像JavaBeans模式那麼好的可讀性。這就是Builder模式的一種形式。不直接生成想要的對象,而是讓客戶端利用所有必要的參數調用構造器(或者靜態工廠),得到一個builder對象,然後客戶端調用無參的 build 方法來生成不可變的對象,這個builder是它構建的類的靜態成員類

Builder模式示例

/**
 * Builder模式
 */
public class BrEntity {
    private int id;         //必選參數
    private String name;    //必選參數
    private int age;
    private String sex;
    private String city;
    private long phone;

    public BrEntity(Builder builder) {
        id = builder.id;
        name = builder.name;
        age = builder.age;
        sex = builder.sex;
        city = builder.city;
        phone = builder.phone;
    }

    public static class Builder {
        private int id;         //必選參數
        private String name;    //必選參數
        private int age;
        private String sex;
        private String city;
        private long phone;

        /* 必選參數 */
        public Builder(int id, String name) {
            this.id = id;
            this.name = name;
        }
        
        public Builder age(int param){
            age = param;
            return this;
        }

        public Builder sex(String param){
            sex = param;
            return this;
        }

        public Builder city(String param){
            city = param;
            return this;
        }

        public Builder phone(long param){
            phone = param;
            return this;
        }
        
        public BrEntity build(){
            return new BrEntity(this);
        }
    }
}

BrEntity是不可變的,所有的默認參數值都單獨放在一個地方,builder的 setter 方法返回 builder 本身,以便可以把調用鏈接起來,下面是創建對象的示例

public class CreateObject {
    public static void main(String[] args) {
        BrEntity brEntity =new BrEntity
                .Builder(1,"JACK")
                .age(18).sex("MAN")
                .city("LONDON")
                .phone(1212123)
                .build();
    }
}

builder像個構造器一樣,可以對其參數強加約束條件, build 方法可以檢驗這些約束條件。將參數從builder拷貝到對象中之後,並在對象域而不是builder域中對它們進行檢驗,如果違反了任何約束條件, build 方法就應該拋出 IllegalStateException。

對多個參數強加約束條件的另一種方法是,用多個setter方法對某個約束條件必須持有的所有參數進行檢查。如果該約束條件沒有得到滿足,setter方法就會拋出 IllegalArgumentsException 。這有個好處,就是一旦傳遞了無效的參數,立即就會發現約束條件失敗,而不是等着調用 build 方法。

Builder模式也適用於類層次結構,使用平行層次結構的builder時,各自嵌套在相應的類中。抽象類有抽象的builder,具體類有具體類的builder。示例如下
① Annimon類:表示各種的動物

/**
 * Annimon類:表示各種的動物
 */
public abstract class Annimal {
    /* 顏色 */
    public enum Color {
        RED, BLACK, WHITE, YELLO
    }
    final Set<Color> colors;

    abstract static class Builder<T extends Builder<T>> {
        EnumSet<Color> colors = EnumSet.noneOf(Color.class);

        protected abstract T self();

        /* 設置顏色 */
        public T setColor(Color color) {
            colors.add(Objects.requireNonNull(color));
            return self();
        }

        abstract Annimal build();
    }

    /* 構造器 */
    Annimal(Builder<?> builder) {
        colors = builder.colors.clone();
    }
}

Annimal.Builder 的類型時泛型,帶有一個遞歸類型參數,它和抽象的self方法一樣,允許在子類種適當地進行方法鏈接,不需要轉換類型。這種針對Java缺乏self類型的解決方法被稱作走模擬的self類型。

② Dogs 類繼承 Annimal類
這個類種,可以設置品種和顏色

/**
 * Dogs 類繼承 Annimal類
 */
public class Dogs extends Annimal {
    /* Dogs 的品種 */
    public enum Kinds {
        KEJI, JINMAO, HASHIQI, BAGE, TAIDI
    }

    public final Kinds kinds;

    public static class Builder extends Annimal.Builder<Builder> {

        private final Kinds kinds;

        /* 設置 Dogs 的品種 */
        public Builder(Kinds kinds) {
            this.kinds = Objects.requireNonNull(kinds);
        }

        @Override
        protected Builder self() {
            return this;
        }

        @Override
        public Dogs build() {
            return new Dogs(this);
        }
    }

    /* 構造器*/
    private Dogs(Builder builder) {
        super(builder);
        kinds = builder.kinds;
    }

    @Override
    public String toString() {
        return "Dogs{" +
                "kinds=" + kinds +
                ", colors=" + colors +
                '}';
    }
}

③ Cats 類繼承 Annimal類
這個類可以設置性別和顏色

/**
 * Cats 類繼承 Annimal類
 */
public class Cats extends Annimal {
    /* Cats 是不是母貓*/
    private final boolean female;

    public static class Builder extends Annimal.Builder<Builder> {

        private boolean female = false;

        /* 更改Cats的female */
        public Builder femaleOrMale() {
            female = true;
            return this;
        }

        @Override
        protected Builder self() {
            return this;
        }

        @Override
        public Cats build() {
            return new Cats(this);
        }
    }

    /* 構造器*/
    private Cats(Builder builder) {
        super(builder);
        female = builder.female;
    }

    @Override
    public String toString() {
        return "Cats{" +
                "female=" + female +
                ", colors=" + colors +
                '}';
    }
}

每個子類的構建器種的build方法都聲明返回正確的子類:Dogs.Builder的build方法返回Dogs,Cats.Builder的build返回Cats。在該方法種,子類方法聲明返回超類種聲明的返回類型的子類型,這種叫做協變返回類型,它允許客戶端無需轉換類型就能使用這些構建器。

④ 測試類

public class CreateObject {
    public static void main(String[] args) {
        /* 創建 Dog 對象:品種柯基、顏色YELLO*/
        Dogs dog = new Dogs.Builder(KEJI)
                .setColor(YELLO)
                .build();

        /* 創建 Cat 對象:白色、female*/
        Cats cat = new Cats.Builder()
                .setColor(WHITE)
                .femaleOrMale()
                .build();

        System.out.println(dog);
        System.out.println(cat);
    }
}

⑤ 結果

Dogs{kinds=KEJI, colors=[YELLO]}
Cats{female=true, colors=[WHITE]}

與構造器想比,builder模式的略微優勢在於,builder可以有多個可變參數。構造器就像方法一樣,只能有一個可變參數。因爲builder利用單獨的方法來設置每個參數,你想要多少個可變參數,它們就可以有多少個,知道每個setter方法都有一個可變參數。

Builder模式十分靈活,可以利用單個builder構建多個對象。builder的參數可以在創建對象期間進行調整,也可以隨着不同的對象而改變。builder可以自動填充某些域,例如每次創建對象時自動增加序列號。

Builder模式的確也有它自身的不足,爲了創建對象,必須先創建它的構建器。雖然創建構建器的開銷在實踐中可能不那麼明顯,但是在某些十分注重性能的情況下,可能就成問題了。Builder模式還比重疊構造器更加冗長,因此它只有在很多參數的時候才使用,比如4個或者更多個參數。但是後期可能需要添加參數,如果一開始就使用構造器或者靜態工廠,等到類需要多個參數時才添加構建器,就會無法控制,那些過時的構造器或者靜態工廠顯得十分不協調。因此,通常最好一開始就使用構建器。

簡而言之,如果類的構造器或者靜態工廠中具有多個參數,設計這種類時,Builder模式就是種不錯的選擇,特別是當大多數參數都是可選的時候。與使用傳統的重疊構造器模式相比,使用Builder模式的客戶端代碼將更易於閱讀和編寫,構建器也比JavaBeans更加安全。

1.3 用私有構造器或枚舉類型強化Singleton屬性

1.3.1 Singleton 介紹
Singleton指僅僅被實例化一次的類,Singleton通常被用來代表一個無狀態的對象如函數,或者那些本質上唯一的系統組件。使類稱爲Singleton會使它的客戶端測試變得十分困難,因爲不可能給Singleton替換模擬實現,除非實現一個充當其類型的接口。

實現Singleton有兩種常見的方法,這兩種方法都要保持構造器爲私有的,並導出共有的靜態成員,以便允許客戶端能夠訪問該類的唯一實現。

1.3.2 公共靜態成員是個final域

public class Elvis {
	public static final Elvis INSTANCE = new Elvis();
	
	private Elvis() { ... }
	
	public void leaveTheBuilding() { ... }
}

私有構造器僅被調用一次,用來實例化公有的靜態 final 域 Elvis.INSTANCE 。由於缺少公有的或者受保護的構造器,所以保證了 Elvis 的全局唯一性:一旦 Elvis 類被實例化,只會存在一個 Elvis 實例,不多也不少。

客戶端的任何行爲都不會改變這一點,但要注意的是,享有特權的客戶端可以藉助 AccessibleObject.setAccessible 方法,通過反射機制調用私有構造器。如果需要抵禦這種攻擊,可以修改構造器,讓它在被要求創建第二個實例的時候創建異常。

優點:
(1)API很清楚地表明瞭這個類是一個Singleton:共有的靜態域是final的,所以該域總是包含相同的對象引用。。

(2)使用更簡單

1.3.3 公共的成員是個靜態工廠方法

public class Elvis {
	private static final Elvis INSTANCE = new Elvis();
	
	private Elvis() { ... }
	
	public static Elvis getInstance() { return INSTANCE }
	
	public void leaveTheBuilding() { ... }
}

對於靜態方法 Elvis.getInstance 的所有調用,都會返回同一個對象引用,所以永遠不會創建其他的 Elvis 實例。

優點:
(1)使用更具有靈活性:在不改變其API的前提下,我們可以改變該類是否應該爲Singleton的想法。工廠方法返回該類的唯一實例,但是它可以很容易被修改,比如改成爲每個調用該方法的線程返回一個唯一的實例。

(2)如果硬要程序需要,可以編寫一個泛型Singleton工廠。

(3)可以通過方法引用作爲提供者。

爲了將利用上述方法實現的Singleton類變成是可序列化的( Serializable ),僅僅在聲明中加上implements Serializable是不夠的。爲了維護並保證Singleton,必須聲明所有實例域都是瞬時的,並提供一個 readResolve 方法。否則,每次反序列化一個序列化的實例時,都會創建一個新的實例,比如說,在我們的例子中,會導致“假冒的Elvis”。爲了防止這種情況,要在 Elvis 類中加入下面這個 readResolve 方法:

private Object readResolve() {
	return INSTANCE;
}

1.3.4 聲明一個包含單個元素的枚舉類型
實現Singleton的第三種方法使聲明一個包含單個元素的枚舉類型:

public enum Elvis {
	INSTANCE;
	public void leaveTheBuilding() { ... }
}

這種方法在功能上與公共域方法相似,但是它更加簡潔,無償地提供了序列化機制,絕對防止多次實例化,即使在面對複雜的序列化或者反射攻擊的時候。雖然這種方法還沒有廣泛採用,但是單元素的枚舉類型已經成爲實現Singleton的最佳方法。要注意的是。如果Singleton必須擴展一個超類,而不是擴展Enum的時候,則不宜使用這個方法。

1.4 通過私有構造器強化不可實例化的能力

有時候,你可能需要編寫只包含靜態方法和靜態域的類。這些類的名聲很不好,因爲有些人在面向對象的語言中濫用這樣的類來編寫過程化的程序。儘管如此,它們也確實有它們特有的用處。我們可以利用這種類,以 java.lang.Math 或者 java.util.Arrays 的方式,把基本類型的值或數組類型上的相關方法組織起來。我們也可以通過 java.util.Collections 的方式,把實現特定接口的對象上的靜態方法組織起來。最後,還可以利用這種類把 final 類上的方法組織起來,以取代擴展該類的做法。

這樣的工具類(utility class)不希望被實例化,實例對它沒有任何意義。然而,在缺少顯式構造器的情況下,編譯器會自動提供一個公有的、無參的缺省構造器(default constructor)。對於用戶而言,這個構造器與其他的構造器沒有任何區別。在已發行的API中常常可以看到一些被無意識地實例化的類。

企圖通過將類做成抽象類來強制該類不可被實例化,這是行不通的。該類可以被子類化,並且該子類也可以被實例化。這樣做甚至會誤導用戶,以爲這種類是專門爲了繼承而設計的。然而,有一些簡單習慣用法可以確保類不可被實例化。由於只有當類不包含顯式的構造器時,編譯器纔會生成缺省的構造器,因此我們只要讓這個類包含私有構造器,它就不能被實例化了:

public class UtilityClass {
	// 私有化構造器,使其不能被實例化
	private UtilityClass() {
		throw new AssertionError();
	}
}

由於顯式的構造器是私有的,所以不可以在該類的外部訪問它。 AssertionError 不是必需的,但是它可以避免不小心在類的內部調用構造器。它保證該類在任何情況下都不會被實例化。這種習慣用法有點違背直覺,好像構造器就是專門設計成不能被調用一樣。因此,明智的做法就是在代碼中增加一條註釋,如上所示。

這種習慣用法也有副作用,它使得一個類不能被子類化。所有的構造器都必須顯式或隱式地調用超類(superclass)構造器,在這種情形下,子類就沒有可訪問的超類構造器可調用了。

1.5 優先考慮依賴注入來引用資源

有許多類會依賴一個或多個底層的資源,以詞典爲例,拼寫檢查器需要依賴詞典,於是這個示例會有下面的兩種做法。
(1)把類實現爲靜態工具類

public class SpellChecker {
    private static final Lexicon dictionary = ...;
    
    private SpellChecker() {
        
    }
    
    public static boolean isValid(String word) {
        
    }
    
    public static List<String> suggestions(String typo) {
        
    }
}

(2)把類實現爲Singleton

public class SpellChecker {
    private final Lexicon dictionary = ...;

    private SpellChecker() {

    }

    public  static INSTANCE = new SpellChecker();
    
    public static boolean isValid(String word) {

    }

    public static List<String> suggestions(String typo) {

    }
}

以上兩種方法實現都不是理想的方法,因爲這兩種做法都假定只有一本詞典可以用,但在實際中每一種語言都會對應有自己的詞典,特殊語言或詞彙還有特殊的詞典。所以這種假定只有一本詞典的做法不能滿足需求。

於是,我們可以考慮將dictionary域設爲nonfinal,並且添加一個方法來用於修改詞典,不過這麼做也不是很好的方式,因爲這樣顯得很笨拙,而且容易出錯,還不能並行工作,靜態工具類和Singleton類不適合於需要引用底層資源的類。

根據上述實現方法的弊端,於是我們可以有一下的實現方式:爲了實現能夠支持多個實例,讓每一個實例都使用客戶端指定的資源,可以使用依賴注入的一種形式,當創建一個新的實例時,就將該資源傳到構造器中。

public class SpellChecker {
    private final Lexicon dictionary;

    public SpellChecker(Lexicon dictionary) {
        this.dictionary = Objects.requireNonNull(dictionary);
    }

    public static boolean isValid(String word) {

    }

    public static List<String> suggestions(String typo) {

    }
}

dictionary是SpellChecker 的一個依賴,在創建SpellChecker 時就會將dictionary注入其中。

雖然這個SpellChecker 的範例只有一個資源(dictionary),但是依賴注入卻適用於任意數量的資源,以及任意的依賴形式。依賴注入的對象資源具有不可變性,因此多個客戶端可以共享對象,依賴注入也同樣適用於構造器、靜態工廠和構建器。

這種模式還有另外一種變體,即將資源工廠傳給構造器。工廠是可以重複調用來創建類型實例的一個對象,這類工廠具體表現爲工廠方法模式。

雖然依賴注入極大的提升了靈活性和可測試性,但它會導致大型項目凌亂不堪,因爲它通常包含上千個依賴。不過這種凌亂可以用一個依賴注入框架來改善,如Dagger、Guice、Spring。

1.6 避免創建不必要的對象

1.6.1 不可變對象
一般來說,最好能重用單個對象,而不是在每次需要的時候就創建一個相同功能的新對象。重用方式既快速,又流行。如果對象是不可變的,它就始終可以被重用。

例如下面的例子

String s = new String("biki”);

這是一個極端的反面例子,該語句每次被執行的時候都創建一個新的String 實例,但是這些創建對象的動作全都是不必要的。傳遞給Sting 構造器的參數(“biki”)本身就是一個String 實例,功能方面等同於構造器創建的所有對象。如果這種用法是在一個循環中,或者是在一個被頻繁調用的方法中,就會創建出成千上萬不必要的String 實例。於是我們可以改進爲下面的方式:

String s = "biki";

這個版本只用了一個String 實例,而不是每次執行的時候都創建一個新的實例。而且,它可以保證,對於所有在同一臺虛擬機中運行的代碼,只要它們包含相同的字符串字面常量,該對象就會被重用

對於同時提供了靜態工廠方法和構造器的不可變類,通常優先使用靜態工廠方法而不是構造器,以避免創建不必要的對象。構造器在每次被調用的時候都會創建一個新的對象,而靜態工廠方法則不會。除了重用不可變的對象之外,也可以重用那些已知不會被修改的可變對象。

有些對象創建的成本比其他對象要高得多。如果重複地需要這類“昂貴的對象”,建議將它緩存下來重用。遺憾的是,在創建這種對象的時候,並非總是那麼顯而易見。假設想要編寫一個方法,用它確定一個字符串是否爲一個有效的羅馬數字。下面介紹一種最容易的方法,使用一個正則表達式:

static boolean isRomanNumearal(String s) {
	return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"+"(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$")
}

雖然String.matches 方法最易於查看一個字符串是否與正則表達式相匹配, 但並不適合在注重性能的情形中重複使用。問題在於, 它在內部爲正則表達式創建了一個Pattern 實例,卻只用了一次,之後就可以進行垃圾回收了。創建Pattern 口實例的戚本很高,因爲需要將正則表達式編譯成一個有限狀態機 。

爲了提升性能,應該顯式地將正則表達式編譯成一個Pattern 口實例(不可變),讓它成爲類初始化的一部分,並將它緩存起來,每當調用isRomanNumeral 方法的時候就重用同一個實例:

public class RomanNumerals {
	private static final Pattern ROMAN 
		= Pattern.compile(^(?=.)M*(C[MD]|D?C{0,3})"+"(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$);
	static boolean isRomanNumeral(String s) {
		return ROMAN.matcher(s).matches();
	}
}

改進後的isRomanNumeral 方法如果被頻繁地調用,會顯示出明顯的性能優勢。

如果包含改進後的isRomanNumeral 方法的類被初始化了,但是該方法沒有被調用,那就沒必要初始化ROMAN 域。通過在isRomanNumeral 方法第一次被調用的時候延遲初始化這個域,有可能消除這個不必要的初始化工作,但是不建議這樣做。正如延遲初始化中常見的情況一樣,這樣做會使方法的實現更加複雜,從而無法將性能顯著提高到超過已經達到的水平

如果一個對象是不變的,那麼它顯然能夠被安全地重用,但其他有些情形則並不總是這麼明顯。考慮適配器的情形,有時也叫作視圖 。適配器是指這樣一個對象: 它把功能委託給一個後備對象,從而爲後備對象提供一個可以替代的接口。由於適配器除了後備對象之外, 沒有其他的狀態信息,所以針對某個給定對象的特定適配器而言,它不需要創建多個適配器實例。

例如, Map 接口的keySet 方法返回該Map 對象的Set 視圖,其中包含該Map 中所有的鍵 。乍看之下,好像每次調用keySet 都應該創建一個新的Set 實例,但是,對於一個給定的Map 對象,實際上每次調用keySet 都返回同樣的Set 實例。雖然被返回的Set 實例一般是可改變的,但是所有返回的對象在功能上是等同的: 當其中一個返回對象發生變化的時候,所有其他的返回對象也要發生變化,因爲它們是由同一個Map 實例支撐的。雖然創建keySet 視圖對象的多個實例並無害處, 卻是沒有必要,也沒有好處的。

1.6.2 自動裝箱(autoboxing)
另一種創建多餘對象的方法,稱作自動裝箱( autoboxing ),它允許程序員將基本類型和裝箱基本類型( Boxed Primitive Type )混用,按需要自動裝箱和拆箱。自動裝箱使得基本類型和裝箱基本類型之間的差別變得模糊起來, 但是並沒有完全消除。它們在語義上還有着微妙的差別,在性能上也有着比較明顯的差別。如下面的例子,它計算所有int 正整數值的總和。爲此,程序必須使用long 算法,因爲int 不夠大,無法容納所有int 正整數值的總和:

private static long num(){
	Long sum = 0L;
	for(long i = 0; i <= Integer.MAX_VALUE; i++){
		sum += i;
	}
	return sum;
}

使用這段代碼計算,運行效率比較慢,因爲變量sum 被聲明成Lo 口q 而不是long ,意味着程序構造了大約231 個多餘的Long 實例(大約每次往Long sum 中增加long時構造一個實例) 。將sum 的聲明從Long 改成long,就可以有效的提升效率。所以,要優先使用基本類型而不是裝箱基本類型,要當心無意識的自動裝箱。

通過維護自己的對象池( object pool )來避免創建對象並不是一種好的做法,除非池中的對象是非常重量級的。正確使用對象池的典型對象示例就是數據庫連接池。建立數據庫連接的代價是非常昂貴的,因此重用這些對象非常有意義。而且,數據庫的許可可能限制你只能使用一定數量的連接。但是,一般而言,維護自己的對象池必定會把代碼弄得很亂,同時增加內存佔用( footprint ),並且還會損害性能。現代的JVM 實現具有高度優化的垃圾回收器,其性能很容易就會超過輕量級對象池的性能。

1.7 消除過期的對象引用

1.7.1 自動回收對象
Java語言具有自動回收機制,當我們使用完一個對象後,其會被自動回收。對象自當回收讓我們的工作變得更加容易,但是也會給我們一個錯覺,讓我們覺得不需要考慮內存管理的事情。

示例

public class Stack {
    private Object[] elements;
    
    private int size = 0;
    
    private static final int CAP = 16;
    
    public Stack() {
        elements = new Object[CAP];
    }
    
    public void push(Object object) {
        ensureCapacity();
        elements[size++] = object;
    }
    
    public Object pop() {
        if(size == 0) {
            throw new EmptyStackException();
        }
        return elements[--size];
    }
    
    private void ensureCapacity() {
        if (elements.length == size){
            elements = Arrays.copyOf(elements,2 * size + 1);
        }
    }
}

這段程序中並沒有很明顯的錯誤,運行也不會有什麼錯誤,但是這個程序中隱藏着一個問題,即“內存泄漏”,隨着垃圾回收器活動的增加,或者由於內存佔用的不斷增加,程序性能的降低會逐漸表現出來。在極端的情況下,這種內存泄漏會導致磁盤交換,甚至導致程序失敗( OutOfMemoryError 錯誤),但是這種失敗情形相對比較少見。

如果一個棧先是增長,然後再收縮, 那麼從棧中彈出來的對象將不會被當作垃圾回收,即使使用棧的程序不再引用這些對象,它們也不會被回收。這是因爲棧內部維護着對這些對象的過期引用 。所謂的過期引用,是指永遠也不會再被解除的引用。在本例中,凡是在elements 數組的“活動部分”之外的任何引用都是過期的,而活動部分是指elements 中下標小於size 的那些元素。

在支持垃圾回收的語言中,內存泄漏是很隱蔽的。如果一個對象引用被無意識地保留起來了,那麼垃圾回收機制不僅不會處理這個對象,而且也不會處理被這個對象所引用的所有其他對象。即使只有少量的幾個對象引用被無意識地保留下來,也會有許許多多的對象被排除在垃圾回收機制之外,從而對性能造成潛在的重大影響。

對於這類問題,當對象引用過期時即清空這些引用即可。修改後的示例如下

public class Stack {
    private Object[] elements;

    private int size = 0;

    private static final int CAP = 16;

    public Stack() {
        elements = new Object[CAP];
    }

    public void push(Object object) {
        ensureCapacity();
        elements[size++] = object;
    }

	/* 修改 */
    public Object pop() {
        if(size == 0) {
            throw new EmptyStackException();
        }
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

    private void ensureCapacity() {
        if (elements.length == size){
            elements = Arrays.copyOf(elements,2 * size + 1);
        }
    }
}

清空過期引用的另一個好處是,如果它們以後又被錯誤地解除引用,程序就會立即拋出NullPointerException 異常,而不是悄悄地錯誤運行下去。

對於每一個對象引用,一旦程序不再用到它,就把它清空。但實際情況張,這樣做會把程序代碼弄得很亂。消除過期引用最好的方法是讓包含該引用的變量結束其生命週期。

存儲池包含了elements 數組(對象引用單元,而不是對象本身)的元素。數組活動區域(同前面的定義)中的元素是己分配的,而數組其餘部分的元素則是自由的。但是垃圾回收器並不知道這一點,對於垃圾回收器而言, elements 數組中的所有對象引用都同等有效。於是我們可以主動將失效對象的告訴垃圾回收器,一旦數組元素變成了非活動部分的一部分,就手工清空這些數組元素。一般來說, 只要類是自己管理內存,我們就應該警惕內存泄漏問題。一旦元素被釋放掉,則該元素中包含的任何對象引用都應該被清空。

內存泄漏的另一個常見來源是緩存。一旦你把對象引用放到緩存中,它就很容易被遺忘掉,從而使得它不再有用之後很長一段時間內仍然留在緩存中。對於這個問題,有幾種可能的解決方案:
(1)只要在緩存之外存在對某個項的鍵的引用,該項就有意義,那麼就可以用WeakHashMap 代表緩存;
(2)當緩存中的項過期之後,它們就會自動被刪除。
只有當所要的緩存項的生命週期是由該鍵的外部引用而不是由值決定時, WeakHashMap 纔有用處.

更爲常見的情形則是,“緩存項的生命週期是否有意義”並不是很容易確定,隨着時間的推移,其中的項會變得越來越沒有價值。在這種情況下,緩存應該時不時地清除掉沒用的項。這項清除工作可以由一個後臺線程(可能是ScheduledThreadPoolExecutor)來完成,或者也可以在給緩存添加新條目的時候順便進行清理。LinkedHashMap 類利用它的removeEldestEntry 方法可以很容易地實現後一種方案。對於更加複雜的緩存,必須直接使用java.lang.ref 。

內存泄漏的第三個常見來源是監昕器和其他回調。如果你實現了一個API,客戶端在這個API 中註冊回調,卻沒有顯式地取消註冊,那麼除非你採取某些動作,否則它們就會不斷地堆積起來。確保回調立即被當作垃圾回收的最佳方法是隻保存它們的弱引用( weakreference ) ,例如,只將它們保存成WeakHashMap 中的鍵。

由於內存泄漏通常不會表現成明顯的失敗,所以它們可以在一個系統中存在很多年。往往只有通過仔細檢查代碼,或者藉助於Heap 剖析工具( Heap Profiler )才能發現內存泄漏問題。因此,如果能夠在內存泄漏發生之前就知道如何預測此類問題,並阻止它們發生,那是最好不過的了。

1.8 避免使用終結方法和清除方法

1.8.1 終結方法
終結方法( finalizer)通常是不可預測的,也是很危險的,一般情況下是不必要的。使用終結方法會導致行爲不穩定、性能降低,以及可移植性問題。當然,終結方法也有其可用之處,但是根據經驗,一般還是應該避免使用終結方法。在Java 9中用清除方法( cleaner )代替了終結方法。清除方法沒有終結方法那麼危險,但仍然是不可預測、運行緩慢,一般情況下也是不必要的。

C++的程序員被告知“不要把終結方法當作是C++中析構器的對應物” 。在C++中,析構器是回收一個對象所佔用資源的常規方法,是構造器所必需的對應物。在Java 中,當一個對象變得不可到達的時候,垃圾回收器會回收與該對象相關聯的存儲空間,並不需要程序員做專門的工作。C++的析構器也可以被用來回收其他的非內存資源。而在Java 中,一般用try-finally 塊來完成類似的工作。

終結方法和清除方法的缺點在於不能保證會被及時執行。從一個對象變得不可到達開始,到它的終結方法被執行,所花費的這段時間是任意長的。這意味着, 注重時間的任務不應該由終結方法或者清除方法來完成。例如,用終結方法或者清除方法來關閉已經打開的文件,就是一個嚴重的錯誤,因爲打開文件的描述符是一種很有限的資源。如果系統無法及時運行終結方法或者清除方法就會導致大量的文件仍然保留在打開狀態,於是當一個程序再也不能打開文件的時候,它可能會運行失敗。

及時地執行終結方法和清除方法正是垃圾回收算法的一個主要功能,這種算法在不同的JVM 實現中會大相徑庭。如果程序依賴於終結方法或者清除方法被執行的時間點,那麼這個程序的行爲在不同的JVM 中運行的表現可能就會截然不同。一個程序在你測試用的JVM 平臺上運行得非常好,而在你最重要顧客的JVM 平臺上卻根本無法運行,這是完全有可能的。

在很少見的情況下,爲類提供終結方法,可能會隨意地延遲其實例的回收過程。Java 語言規範並不保證哪個線程將會執行終結方法,所以,除了不使用終結方法之外,並沒有很輕便的辦法能夠避免這樣的問題。在這方面,清除方法比終結方法稍好一些,因爲類的設計者可以控制自己的清除線程, 但清除方法仍然在後臺運行,處於垃圾回收器的控制之下,因此不能確保及時清除。

Java 語言規範不僅不保證終結方法或者清除方法會被及時地執行,而且根本就不保證它們會被執行。當一個程序終止的時候,某些已經無法訪問的對象上的終結方法卻根本沒有被執行,這是完全有可能的。結論是: 永遠不應該依賴終結方法或者清除方法來更新重要的持久狀態。例如,依賴終結方法或者清除方法來釋放共享資源(比如數據庫)上的永久鎖,這很容易讓整個分佈式系統垮掉。

不要被System.gc 和System.runFinalization 這兩個方法所誘惑,它們確實增加了終結方法或者清除方法被執行的機會,但是它們並不保證終結方法或者清除方法一定會被執行。唯一聲稱保證它們會被執行的兩個方法是System.runFinalizersOnExit,及其Runtime.runFinalizersOnExit 。這兩個方法都有致命的缺陷,井且已經被廢棄很久了。

使用終結方法的另一個問題是:如果忽略在終結過程中被拋出來的未被捕獲的異常,該對象的終結過程也會終止[ JLS, 12 . 6 ] 。未被捕獲的異常會使對象處於破壞的狀態( corruptstate ),如果另一個線程企圖使用這種被破壞的對象,則可能發生任何不確定的行爲。正常情況下,未被捕獲的異常將會使線程終止,並打印出戰軌跡( Stack Trace ),但是,如果異常發生在終結方法之中,則不會如此,甚至連警告都不會打印出來。清除方法沒有這個問題,因爲使用清除方法的一個類庫在控制它的線程。

終結方法有一個嚴重的安全問題: 它們爲終結方法攻擊打開了類的大門。終結方法攻擊背後的思想很簡單:如果從構造器或者它的序列化對等體拋出異常,惡意子類的終結方法就可以在構造了一部分的應該已經半途夭折的對象上運行。這個終結方法會將對該對象的引用記錄在一個靜態域中,阻止它被垃圾回收。一旦記錄到異常的對象,就可以輕鬆地在這個對象上調用任何原本永遠不允許在這裏出現的方法。從構造器拋出的異常,應該足以防止對象繼續存在;有了終結方法的存在,這一點就做不到了。這種攻擊可能造成致命的後果,final 類不會受到終結方法攻擊,因爲沒有人能夠編寫出final 類的惡意子類。爲了防止非final 類受到終結方法攻擊, 要編寫一個空的final 的finalize 方法。

如果類的對象中封裝的資源(例如文件或者線程)確實需要終止,只需讓類實現AutoCloseable,並要求其客戶端在每個實例不再需要的時候調用close 方法,一般是利用try-with-resources 確保終止,即使遇到異常也是如此。值得提及的一個細節是,該實例必須記錄下自己是否已經被關閉了: close 方法必須在一個私有域中記錄下“該對象已經不再有效” 。如果這些方法是在對象已經終止之後被調用,其他的方法就必須檢查這個域,並拋出IllegalStateException 異常。

終結方法和清除方法的用途:
(1)當資源的所有者忘記調用它的close 方法時,終結方法或者清除方法可以充當”安全網“,雖然這樣做並不能保證終結方法或者清除方法會被及時地運行,但是在客戶端無法正常結束操作的情況下,遲一點釋放資源總比永遠不釋放要好。如果考慮編寫這樣的安全網終結方法,就要認真考慮清楚,這種保護是否值得付出這樣的代價。有些Java 類(如FileinputStream 、FileOutputStream 、ThreadPoolExecutor 和j ava.sql.Connection )都具有能充當安全網的終結方法。

(2)第二種用途與對象的本地對等體有關。本地對等體是一個本地(非Java 的)對象,普通對象通過本地方法委託給一個本地對象。因爲本地對等體不是一個普通對象,所以垃圾回收器不會知道它,當它的Java 對等體被回收的時候,它不會被回收。如果本地對等體沒有關鍵資源,並且性能也可以接受的話,那麼清除方法或者終結方法正是執行這項任務最合適的工具。如果本地對等體擁有必須被及時終止的資源,或者性能無法接受,那麼該類就應該具有一個close 方法。

1.8.2 清除方法
清除方法的使用有一定的技巧。下面以一個簡單的Room 類爲例。假設房間在收回之前必須進行清除。Room 類實現了AutoCloseable ;它利用清除方法自動清除安全網的過程只不過是一個實現細節。與終結方法不同的是,清除方法不會污染類的公有API:

public class Room implements AutoCloseable{

    private static final Cleaner cleaner = Cleaner.create();
    
    private static class State implements Runnable {

        int numJuckPiles;
        
        State(int numJuckPiles) {
            this.numJuckPiles = numJuckPiles;
        }
        
        @Override
        public void run() {
            System.out.println("Cleaning Room");
            numJuckPiles = 0;
        }
    }

    private final State state;

    private final Cleaner.Cleanable cleanable;

    public Room(int numJuckPiles) {
        state = new State(numJuckPiles);
        cleanable = cleaner.register(this,state);
    }
    
    @Override
    public void close() throws Exception {
        cleanable.clean();
    }
}

內嵌的靜態類State 保存清除方法清除房間所需的資源。在這個例子中,就是numJunkPiles域,表示房間的雜亂度。更現實地說,它可以是final 的long, 包含一個指向本地對等體的指針。State 實現了Runnable 接口,它的run 方法最多被Cleanable調用一次,後者是我們在Room 構造器中用清除器註冊State 實例時獲得的。以下兩種情況之一會觸發run 方法的調用:通常是通過調用Room 的close 方法觸發的,後者又調用了Cleanable 的清除方法。如果到了Room 實例應該被垃圾回收時,客戶端還沒有調用close 方法,清除方法就會調用State 的run 方法。

關鍵是State 實例沒有引用它的Room 實例。如果它引用了,會造成循環,阻止Room實例被垃圾回收(以及防止被自動清除) 。因此State 必須是一個靜態的嵌套類,因爲非靜態的嵌套類包含了對其外圍實例的引用 。同樣地,也不建議使用lambda,因爲它們很容易捕捉到對外圍對象的引用。如前所述, Room 的清除方法只用作安全網。如果客戶端將所有的Room 實例化都包在try-with-resource 塊中,將永遠不會請求到自動清除。如下示例:

public class Adult {
    public static void main(String[] args) {
        try(Room room = new Room(7)) {
            System.out.println("Clean");
        }
    }
}

正如所期待的一樣,運行Adult 程序會打印出Goodbye ,接着是Cleaning room 。再看下面的示例

public class Adult {
    public static void main(String[] args) {
        new Room(99);
        System.out.println("Clean");
    }
}

這個示例可能並不會先打印打印出Goodbye ,接着是Cleaning room。Cleaner 規範指出:“清除方法在System.exit 期間的行爲是與實現相關的。不確保清除動作是否會被調用。”雖然規範沒有指明,其實對於正常的程序退出也是如此。

總而言之,除非是作爲安全網,或者是爲了終止非關鍵的本地資源,否則請不要使用清除方法,對於在Java 9 之前的發行版本,則儘量不要使用終結方法。若使用了終結方法或者清除方法,則要注意它的不確定性和性能後果。

1.9 try-with-resources優先於try-finally

Java 類庫中包括許多必須通過調用close 方法來手工關閉的資源。例如InputStreamOutputStreamjava.sql.Connection 。客戶端經常會忽略資源的關閉,造成嚴重的性能後果也就可想而知了。雖然這其中的許多資源都是用終結方法作爲安全網,但是效果並不理想 。

根據經驗, try-finally 語句是確保資源會被適時關閉的最佳方法,就算髮生異常或者返回也一樣:

static String firstLineOfFile(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine();
    }finally {
        br.close();
    }
}

這個示例目前看起來還沒有不好的地方,但是如果再添加第二個資源,就會變得比較糟:

static void copy(String str, String str1) throws IOException {
    InputStream in = new FileInputStream(str);
    try {
        OutputStream os = new FileOutputStream(str1);
        try {
            byte[] bytes = new byte[1024];
            int n;
            while ((n = in.read(bytes)) >= 0){
                os.write(bytes,0,n);
            }
        }finally {
            os.close();
        }
    }finally {
        in.close();
    }
}

即使用try-finally 語句正確地關閉了資源,如前兩段代碼範例所示,它也存在着些許不足。因爲在try 塊和finally 塊中的代碼,都會拋出異常。例如在firstLineOfFile方法中,如果底層的物理設備異常,那麼調用readLine 就會拋出異常,基於同樣的原因,調用close 也會出現異常。在這種情況下,第二個異常完全抹除了第一個異常。在異常堆棧軌跡中,完全沒有關於第一個異常的記錄,這在現實的系統中會導致調試變得非常複雜,因爲通常需要看到第一個異常才能診斷出問題何在。雖然可以通過編寫代碼來禁止第二個異常,保留第一個異常,但事實上沒有人會這麼做,因爲實現起來太煩瑣了。

當Java 7 引人try-with-sources 語句時,所有這些問題一下子就全部解決了。要使用這個構造的資源,必須先實現AutoCloseable 接口,其中包含了單個返回void 的close 方法。Java 類庫與第三方類庫中的許多類和接口,現在都實現或擴展了AutoCloseable 接口。如果編寫了一個類,它代表的是必須被關閉的資源,那麼這個類也應該實現AutoCloseable 。

以下就是使用try-with-resources 的第一個範例:

static String firstLineOfFile(String path) throws IOException {
    try {
        BufferedReader br = new BufferedReader(new FileReader(path));
        return br.readLine();
    }
}

以下是使用try-with -resources 的第二個範例:

static void copy(String str, String str1) throws IOException {
    try {
        InputStream in = new FileInputStream(str);
        OutputStream os = new FileOutputStream(str1);
        byte[] bytes = new byte[1024];
        int n;
        while ((n = in.read(bytes)) >= 0){
            os.write(bytes,0,n);
        }
    }
}

使用try-with-resources 不僅使代碼變得更簡潔易懂, 也更容易進行診斷。以first LineOfFile方法爲例,如果調用readLine 和close 方法都拋出異常,後一個異常就會被禁止,以保留第一個異常。事實上,爲了保留你想要看到的那個異常,即便多個異常都可以被禁止。這些被禁止的異常並不是簡單地被拋棄了,而是會被打印在堆棧軌跡中,並註明它們是被禁止的異常。通過編程調用getSuppressed 方法還可以訪問到它們。

在try-with-resources 語句中還可以使用catch 子句,就像在平時的try-finally 語句中一樣。這樣既可以處理異常,又不需要再套用一層代碼。如下示例,這個firstLineOfFile 方法沒有拋出異常但是如果它無法打開文件,或者無法從中讀取,就會返回一個默認值:

static String firstLineOfFile(String path, String val) throws IOException {
    try {
        BufferedReader br = new BufferedReader(new FileReader(path));
        return br.readLine();
    }catch (IOException e){
        return val;
    }
}

結論很明顯: 在處理必須關閉的資源時,始終要優先考慮用try-with-resources ,而不是用try-finally 。這樣得到的代碼將更加簡潔、清晰,產生的異常也更有價值。有了try-with-resources 語句,在使用必須關閉的資源時,就能更輕鬆地正確編寫代碼了。

參考文獻
[1] Effective Java 中文版(原書第3版),Joshua Bloch[美],俞黎敏 譯,機械工業出版社。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章