第2章 建立和銷燬對象

本章的主題是建立和銷燬對象:什麼時候以及如何建立對象,什麼時候以及如何避免建立對象,如何確保它們可以適時地銷燬,以及如何管理對象銷燬以前必須進行的各類清理動做。 ###第1條:考慮用靜態工廠方法代替構造器###   獲取類的實例有兩個方法,類提供一個公有的構造器和提供一個公有的只返回類的實例靜態工廠方法(static factory method)。
  例如將boolean基本類型值轉換成一個Boolean對象引用:java

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

  這裏的靜態工廠方法與設計模式中的工廠方法模式不一樣。
  靜態工廠方法比公有的構造器有幾大優點:
1.它們有名稱
  若是構造器的參數自己沒有確切的描述正被返回的對象那麼具備適當名稱的靜態工廠方法會更容易使用。例如構造器BigInteger(int, int, Random)返回的BigInteger可能爲素數,若是用名爲BigInteger.probablePrime的靜態工廠方法表示顯然更清楚。
2.沒必要在每次調用它們的時候都建立一個新對象
  這使得不可變類可使用預先構建好的實例,或者將構建好的實例緩存起來進行重複利用,從而避免建立沒必要要的重複對象。
  靜態工廠方法可以爲重複的調用返回相同對象,這樣有助於類總能嚴格控制在某個時刻哪些實例應該存在。這種類被稱做實例受控的類(instance-controlled),實例受控使得類能夠確保它是一個Singleton或者是不可實例化的。它還使得不可變的類能夠確保不會存在兩個相等的實例,即當且僅當a==b的時候纔有a.equals(b)爲true。若是能夠保證這一點就可使用==代替equals()方法,這樣能夠提高性能,枚舉(enum)保證了這一點。
3.它們能夠返回原返回類型的任何子類型對象
  公有的靜態工廠方法所返回的對象的類不只能夠是非公有的,並且該類還能夠隨着每次調用而發生變化,這取決於靜態工廠方法的參數值。只要是已聲明的返回類型的子類型,都是容許的。
  例如java.util.enumSet沒有公有構造器,只有靜態工廠方法程序員

/**
     * Creates an empty enum set with the specified element type.
     *
     * @param elementType the class object of the element type for this enum
     *     set
     * @throws NullPointerException if <tt>elementType</tt> is null
     */
    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }

  它返回兩種實現類之一,具體則取決於底層枚舉類型的大小:若是它的元素有64個或者更小會返回一個RegularEnumSet實例不然返回JumboEnumSet實例。
  靜態工廠方法返回的對象所屬的類在編寫該靜態工廠方法的類的時候能夠沒必要存在,這種靈活的靜態工廠方法構成了服務提供者框架(Service Provider Framework)(多個服務提供者實現一個服務,系統爲服務提供者的客戶端提供多個實現,並把他們從多個實現中解耦出來)的基礎。
  服務提供者框架中有三個重要的組件:服務接口(Service Interface),這是提供者實現的;提供者註冊API(Provider Registration API),這是系統用來註冊實現讓客戶端訪問他們的;服務訪問API(Service Access API),是客戶端用來獲取服務的實例的。第四個組件是可選的:服務提供者接口(Service Provider Interface),這些提供者負責建立其服務實現的實例。若是沒有服務提供者接口,實現就按照類名稱註冊,並經過反射方式進行實例化。
  對於JDBC來講,Connection就是它的服務接口,DriverManager.registerDriver是提供者註冊API,DriverManager.getConnection是服務訪問API,Driver就是服務提供者接口。
  客戶端調用的方式sql

Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);

  Connection是一個接口提供了操做數據庫的各類方法:數據庫

public interface Connection extends Wrapper, AutoCloseable {
	public abstract Statement createStatement() throws SQLException;
	public abstract PreparedStatement prepareStatement(String s) throws SQLException;
	public abstract CallableStatement prepareCall(String s) throws SQLException;
	... ...
}

  DriverManager的registerDriver方法以下:設計模式

public static synchronized void registerDriver(Driver driver) throws SQLException
{
    if(driver != null)
        registeredDrivers.addIfAbsent(new DriverInfo(driver));
    else
        throw new NullPointerException();
    println((new StringBuilder()).append("registerDriver: ").append(driver).toString());
}

  Driver是一個接口:數組

public interface Driver 
{
	public abstract Connection connect(String s, Properties properties) throws SQLException;
	public abstract boolean acceptsURL(String s) throws SQLException;
	public abstract DriverPropertyInfo[] getPropertyInfo(String s, Properties properties) throws SQLException;
	public abstract int getMajorVersion();
	public abstract int getMinorVersion();
	public abstract boolean jdbcCompliant();
	public abstract Logger getParentLogger() throws SQLFeatureNotSupportedException;
}

  其中DriverManager.getConnection(String s, String s1, String s2)經過下面代碼得到Connection緩存

Connection connection = driverinfo.driver.connect(s, properties);

  服務提供者框架模式有着無數種變體,例如服務訪問API能夠利用適配器(Adapter)模式返回比提供者須要的更豐富的服務接口。下面是一個簡單實現:安全

// 服務接口
public interface Service
{
	...//服務操做的方法
}

// 服務提供者接口
public interface Provider
{
	Service newService();// 返回具體的服務實例
}

// 用於註冊和訪問服務的不可實例化的類
public class Services 
{
	// 構造函數聲明爲私有的不可實例化
	private Services()
	{
	}
	
	// 存儲服務名字和對應的服務
	private static final Map<String, Provider> providers = new ConcurrentHashMap<String, Provider>();
	public static final String DEFAULT_PROVIDER_NAME = "<def>";
	
	// 提供註冊接口
	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();
	}
}

4.在建立參數化類型實例的時候它使代碼更簡潔   在調用參數化類的構造器的時候一般要求提供兩次類型參數,若是參數不少會比較冗長。經過靜態工廠方法,編譯器能夠找到類型參數,這被稱做類型推倒(type inference),例如:app

Map<String, List<String>> m = New HashMap<String, List<String>>();

  假設HashMap提供了這個靜態工廠:框架

public static <K, V> HashMap<K, V> newInstance()
{
	return new HashMap<K, V>();
}
Map<String, List<String>> m = HashMap.newInstance();

  靜態工廠方法也有缺點:
1.類若是不含有public或protected的構造器就不能被子類化。
  可是也能夠塞翁失馬,由於鼓勵使用複合(composition)而不是繼承。
2.它們與其餘的靜態方法實際上沒有任何區別從而不容易知道如何實例化一個類。 ###第2條:遇到多個構造器參數時要考慮使用構建器###   靜態工廠和構造器都不能很好的擴展到大量的可選參數,若是可選參數多的話一般有兩種方式建立類的實例。
1.重載構造器
  須要寫太多個構造器並且客戶端的代碼也會很難編寫
2.使用JavaBeans模式
  調用一個無參的構造器建立對象而後調用setter方法來設置每一個必要的參數以及相關的可選參數,這種方式彌補了重載構造器模式的不足代碼讀起來也容易。可是也有一些缺點,在構造過程當中JavaBean可能處於不一致的狀態,類沒法僅僅經過檢驗構造器參數的有效性來保證一致性。例如new了一個類的兩個實例,一個只set了A屬性,一個只設置了B屬性,這兩個實例不一致,不能保證經過該類的同一個構造器構造出來的對象是屬性相同的。另外,這樣類就不是不可變類(不可變對象指對象一旦被建立,狀態就不能再改變。任何修改都會建立一個新的對象,如 String、Integer及其它包裝類)了,就須要付出額外的努力來確保它的線程安全。
3.Builder模式既能保證像重疊構造器模式那樣的安全性也能保證像JavaBeans模式那麼好的可讀性。
  例如:

public class TestBuilder 
{
	public static void main(String[] args) 
	{
		Student student = new Student.Builder("victor", 1).age(25).address("上海").Builder();
	}
}

/**
 * @author victor
 * 學生類。姓名和性別是必填參數,年齡和住址是可選的
 */
class Student 
{
	private String name;
	private int gender;
	private int age;
	private String address;
	
	public static class Builder
	{
		private String name;
		private int gender;
		// 初始化可選參數
		private int age = 12;
		private String address = "北京";
		
		public Builder(String name, int gender)
		{
			this.name = name;
			this.gender = gender;
		}
		
		public Builder age(int age)
		{
			this.age = age;
			return this;
		}
		public Builder address(String address)
		{
			this.address = address;
			return this;
		}
		
		public Student Builder()
		{
			return new Student(this);
		}
	}
	
	private Student(Builder builder)
	{
		this.name = builder.name;
		this.gender = builder.gender;
		this.age = builder.age;
		this.address = builder.address;
	}
}

  若是類的構造器或者靜態工廠中具備多個參數能夠思考一下是否用Builder模式更適合。 ###第3條:用私有構造器或者枚舉類型強化Singleton屬性###   Singleton指僅僅被實例化一次的類。
  在Java 1.5以前實現Singleton有兩種方法,這兩種方法都要把構造器保持爲私有的並導出公有的靜態成員。
  第一種方法:

public class Singleton 
{
	public static final Singleton INSTANCE = new Singleton();
	
	private Singleton()
	{
		
	}
}

  私有構造器僅被調用一次,用來實例化公有的靜態final域,保證了Singleton的全局惟一性。可是享有特權的客戶端能夠經過反射機制調用私有構造器,若是須要抵禦這種攻擊,能夠修改構造器在建立第二個實例的時候拋出異常。
  第二種方法:
  公有的成員是個靜態工廠方法

public class Singleton 
{
	private static final Singleton INSTANCE = new Singleton();
	
	private Singleton()
	{
		
	}
	
	public static Singleton getInstance()
	{
		return INSTANCE;
	}
}

  第一種公有域的方法主要好處在於組成類的成員的聲明很清楚的代表了這個類是一個Singleton:公有的靜態域是final的,因此該域老是包含相同的對象引用。公有域方法在性能上再也不有任何優點:現代的JVM實現幾乎都可以將靜態工廠方法的調用內聯化。
  第二種工廠方法的優點之一在於,它提供了靈活性:在不改變其API的前提下,咱們能夠改變該類是否應該爲Singleton的想法。第二個優點與泛型有關。
  使用其中一種方法實現的Singleton類變成可序列化(Serializable)僅僅在聲明中加上"implement Serializable"是不夠的。爲了維護並保證Singleton,必須聲明全部實例域都是瞬時(transient)的,並提供一個readResolve方法。不然,每次反序列化一個序列化的實例時都會建立一個新的實例。
  從Java 1.5版本起實現Singleton還有第三種方法。只需編寫一個包含單個元素的枚舉類型:

public enum People 
{
	INSTANCE;
	public void speak()
	{
		System.out.println(this + " is speaking! ");
	}
}
public class Singleton 
{
	public static void main(String[] args)
	{
		People s1 = People.INSTANCE;
	    s1.speak();
	    People s2 = People.INSTANCE;
	    s2.speak();
	    System.out.println(s1 == s2);
	}
}

  運行結果:

INSTANCE is speaking! 
INSTANCE is speaking! 
true

  這種方法在功能上與公有域方法相近,可是更加簡潔而且無償的提供了序列化機制,絕對防止屢次實例化,即便是在面對複雜的序列化或者反射攻擊的時候。單元素的枚舉類型已經成爲實現Singleton的最佳方法。 ###第4條:經過私有構造器強化不可實例化的能力###   有一些工具類不但願被實例化,實例對它沒有任何意義。因爲只有當類不包含顯示的構造器時,編譯器纔會生成缺省的構造器,所以只要讓這個類包含私有構造器就不能被實例化了。

public class UtilityClass 
{
	// Suppress default constructor for non-instantiability
	private UtilityClass()
	{
		throw new AssertionError();
	}
}

  反作用就是使得該類不能被子類化,由於全部的構造器都必須顯示或隱式的調用超類構造器,在這種情形下,子類就沒有可訪問的超類構造器可調用了。 ###第5條:避免建立沒必要要的對象###   通常來講,最好能重用對象而不是在每次須要的時候就建立一個相同功能的新對象。重用方式既快速,又流行。若是對象是不可變(immutable)的它始終能夠被重用。
  一個極端的例子:String s = new String("stringette");
  "stringette"自己就是一個String實例,功能方面等同於構造器建立的全部對象。
改進後的版本:String s = "stringette";能夠保證對於全部在同一臺虛擬機中運行的代碼,只要它們包含相同的字符串字面常量該對象就會被重用。
  對於同時提供了靜態工廠方法和構造器的不可變類,一般可使用靜態工廠方法而不是構造器以免建立沒必要要的對象。例如,靜態工廠方法Boolean.valueOf(String)老是優於構造器Boolean(String)。構造器在每次被調用的時候都會建立一個新的對象,而靜態工廠方法則歷來不要求這樣作。
  除了重用不可變的對象以外也能夠重用那些已知不會被修改的可變對象,下面是一個常見的反例,檢驗一我的是否出生於1946年至1964年期間:

public class Person 
{
	private final Date birthDate = null;
	
	public boolean isBabyBoomer()
	{
		Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
		gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
		Date boomStart = gmtCal.getTime();
		gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
		Date boomEnd = gmtCal.getTime();
		return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
	}
}

  isBabyBoomer每次被調用的時候都會新建一個Calendar、一個TimeZone和兩個Date實例,這是沒必要要的。改進後代碼的以下:

public class Person 
{
	private final Date birthDate = null;
	private static final Date BOOM_START;
	private static final Date BOOM_END;
	
	static
	{
		Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
		gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
		BOOM_START = gmtCal.getTime();
		gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
		BOOM_END = gmtCal.getTime();
	}
	
	public boolean isBabyBoomer()
	{
		return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0;
	}
}

  在Java1.5以後有一種建立多餘對象的新方法稱做自動裝箱(autoboxing),它容許程序員將基本類型和裝箱基本類型混用,按須要自動裝箱和拆箱。
  例如以下程序計算全部int正值的總和:

public static void main(String[] args)
{
		Long sum = 0L;
		for(long i = 0; i < Integer.MAX_VALUE; i++)
		{
			sum += i;
		}
		System.out.println(sum);
}

  sum聲明爲Long而不是long程序就會構造大約2^31個多餘的Long實例。
  優先使用基本類型而不是裝箱基本類型,要小心無心識的自動裝箱。
###第6條:消除過時的對象引用###   雖然Java具備自動垃圾回收機制,可是也須要考慮內存管理的事情。
  例如以下代碼:

public class Stack 
{
	private Object[] elements;
	private int size = 0;
	private static final int DEFAULT_INITIAL_CAPACITY = 16;
	
	public Stack()
	{
		elements = new Object[DEFAULT_INITIAL_CAPACITY];
	}
	
	public void push(Object e)
	{
		ensureCapacity();
		elements[size++] = e;
	}
	
	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);
		}
	}
}

  這是一個棧先增加而後再收縮,可是從棧中彈出來的對象將不會被看成垃圾回收,即便使用棧的程序再也不引用這些對象。這是由於棧內部維護着對這些對象的過時引用,所謂的過時引用是指永遠也不會再被解除的引用。在上述程序中凡是在數組活動部分以外的任何引用都是過時的,活動部分是指elements中下標小魚size的那些元素。
  pop方法的修復版本以下:

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

  只要類是本身管理內存,就應該警戒內存泄露的問題。 ###第7條:避免使用終結方法###   終結方法(finalizer)一般是不可預測的也是很危險的,通常狀況下是不可預測的。   終結方法的缺點在於不能保證會被及時地執行,從一個對象變得不可到達開始,到它的終結方法被執行所花費的這段時間是任意長的。Java語言規範不只不保證終結方法會被即便地執行並且根本就不保證它們會被執行。   不該該依賴終結方法來更新重要的持久狀態。例如,依賴終結方法來釋放共享資源(例如數據庫)上的永久鎖很容易讓整個分佈式系統垮掉。   另外,若是在終結方法之中發生了異常則該異常不會使線程終止也不能打印出棧軌跡,不利於問題定位分析。   始終終結方法有一個很是嚴重的性能損失,用終結方法建立和銷燬對象很是慢。   若是類的對象中封裝的資源(例如文件或者線程)確實須要終止只需提供一個顯示的終止方法,例如InputStream、OutputStream和java.sql.Connection上的close方法以及java.util.Timer上的cancel方法。顯示的終止方法一般與try-finally結構結合起來使用,以保證及時執行。

相關文章
相關標籤/搜索