【讀書筆記】《Effective Java》——創建和銷毀對象
Item 1. 考慮用靜態工廠方法替代構造器
獲得一個類的實例時我們都會采取一個公有的構造器。Foo x = new Foo();
同時我們應該掌握另一種方法就是靜態工廠方法(static factory method)。
一句話總結,靜態工廠方法其實就是一個返回類的實例的靜態方法。
書中給出的例子是Boolean的valueOf方法:
通過valueOf方法將boolean基本類型轉換成了一個Boolean類型,返回了一個新的對象引用。
除valueOf外,像Java中的getInstance和newInstance等方法都為靜態工廠方法。
靜態工廠方法不同於設計模式中的工廠方法。
那麽為什麽要使用靜態工廠方法呢?下面是它的幾大優勢:
它們有名字
給構造器起名字,增強了代碼的可讀性。
如果一個構造器的參數並不能確切描述它返回的對象,這時候可以考慮靜態工廠方法。
或者你的多個構造器只是在參數列表中的參數順序上有所不同,那麽除非你提供了詳盡的文檔說明,否則你下次使用時就會一臉懵逼,這幾個構造器到底要選哪個???
例如下面這個例子,一個RandomIntGenerator類,從類名可以看出這是個用來產生整型隨機數的類。
public class RandomIntGenerator { private final int min; privatefinal int max; public int next(){...} }
隨機數的大小介於min和max兩個參數之間,我們需要構造器去對它們進行初始化。
public RandomIntGenerator(int min, int max) { this.min = min; this.max = max; }
很好,現在我們又想提供一個新的功能,用戶只需要指定一個最小值即可,生成的隨機數會介於指定的最小值和整型默認的最大值之間。
所以,我們可能會添加一個新的構造器:
public RandomIntGenerator(intmin) { this.min = min; this.max = Integer.MAX_VALUE; }
到這裏事情進展很順利,但是有指定最小值的功能,相對的我們還要添加一個指定最大值的方法。
public RandomIntGenerator(int max) { this.min = Integer.MIN_VALUE; this.max = max; }
但是創建完之後你會得到一個編譯錯誤,原因是兩個構造器有相同的方法簽名(方法名和參數類型)。
這時靜態工廠方法就派上用場了,重新構造如下:
public class RandomIntGenerator { private final int min; private final int max; private RandomIntGenerator(int min, int max) { this.min = min; this.max = max; } public static RandomIntGenerator between(int max, int min) { return new RandomIntGenerator(min, max); } public static RandomIntGenerator biggerThan(int min) { return new RandomIntGenerator(min, Integer.MAX_VALUE); } public static RandomIntGenerator smallerThan(int max) { return new RandomIntGenerator(Integer.MIN_VALUE, max); } public int next() {...} }
不僅沒有了之前的錯誤,而且它們有著不同的名字,很清晰地描述了方法的功能。
總之,由於靜態工廠方法有名稱,所以他們不受那些限制。
當你有多個簽名相同的構造器時,用幾個名字有區分度的靜態工廠方法代替可能是更好的解決辦法。
不必在每次調用它們的時候創建一個新對象
每次調用構造器都會創建一個新對象,而靜態工廠方法則不會。
這使得不可變類可以使用預先定義好的實例,或者將構建好的實例緩存起來,進行重復利用,避免創建不必要的重復對象。
public class BooleanGenerator { public static void main(String[] args) { Boolean b1 = Boolean.valueOf(true); Boolean b2 = Boolean.valueOf(true); Boolean b3 = new Boolean(true); Boolean b4 = new Boolean(true); System.out.println(b1 == b2); System.out.println(b3 == b4); } } //output: //true //false
可以看到使用valueOf並不會創建新的對象,對於一些經常創建相同對象的程序,並且創建對象的代價很高,靜態工廠方法可以極大地提升性能。
可以返回原返回類型的任何子類型的對象
在選擇返回對象的類時有了更大的靈活性。
API可以返回對象,同時不會使對象的類變成公有的既可以是非公有類,這樣做的目的可以隱藏實現類。
公有的靜態工廠方法所返回的對象的類不僅可以是非公有的,而且該類還可以隨著每次調用發生變化,這取決於靜態工廠方法的參數值。
參考java.util.EnumSet中的noneOf方法,根據不同的參數類型選擇返回的是RegularEnumSet還是JumboEnumSet:
接下來,書中通過服務提供者框架(Service Provider Framework)來說明了靜態工廠方法的另一個用法。
利用的是靜態工廠方法返回的對象所屬的類,在編寫包含該靜態工廠方法的類時可以不必存在。
看起來有點繞,下面來通過代碼來看一下。
//服務接口 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>"; //服務提供者註冊API //默認的註冊方法 public static void registerDefaultProvider(Provider p){ registerProvider(DEFAULT_PROVIDER_NAME,p); } //真正的註冊方法 public void registerProvider(String name, Provider p) { providers.put(name, p); } //服務訪問API 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();//返回服務實例 } }
服務提供者框架是指這樣一個系統:多個服務提供者實現一個服務,系統為服務提供者的客戶端提供多個實現,並把他們從實現中解耦出來。
這塊有點難理解,先來看一下UML圖:
JDBC就是利用的服務提供者框架,當我們創建數據庫連接時,需要先加載對應的驅動,然後獲取連接。
Class.forName(jdbcDriver);
conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPasswd);
對於JDBC來說Connection就是它的服務接口,裏面的方法,不同的數據庫需要自己實現。
DriverManager就是Services類,其中包含的registerDriver和getConnection方法對應的就是註冊和訪問。
Driver是一個服務提供者接口。
最後返回的服務實際上是通過服務提供者接口,實現了解耦。
在創建參數化類型實例的時候,使得代碼變得更加簡潔
如果你用的是JDK1.7之前的版本在定義一個HashMap,那你不得不這麽寫:
Map<String, String> map = new HashMap<String, String>();
在使用構造器時必須再寫一遍類型參數,因為不支持類型推到,每次都要幹重復性的工作。
假設HashMap提供了靜態工廠方法,事情就變得簡單:
public static <K,V> HashMap<K,V> newInstance(){ return new HashMap<K,V>(); }
你就可以通過下面的代碼代替上面繁瑣的聲明:
Map<String,String> map = HashMap.newInstance();
顯然作者在寫這本書時已經考慮到了這個問題(那時候JDK的版本是1.6),JDK1.7之後的版本有了類型推導。
當然凡事都有兩面性,除了上述的優點,靜態工廠方法同樣存在不足。
類如果不含共有的或者受保護的構造器,就不能被子類化
如果沒有公有構造器,當然這個類就不能被子類繼承。
這也許是一個優點,因為鼓勵程序使用組合而不是繼承。
他們與其他的靜態方法實際上沒有任何區別
在API文檔中它們沒有像構造器那樣被明確標識出來,因此對於一個使用靜態工廠方法而不是構造器的類來說,要想弄明白如何實例化,就需要費點事了。
你可以使用註釋或者如下的命名規則讓用戶知道這是一個靜態工廠方法:
- valueOf——返回的實例與它的參數具有相同的值,被用來做類型轉換。e.g. String.valueOf()。
- of——valueOf的一種更加簡潔的替代,在EnumSet中使用並流行起來。
- getInstance——通過方法的參數來描述返回實例。
- newInstance——和getInstance一樣,每次返回新的實例。
- getType、newType——和上面兩個方法類似,在工廠方法處於不同的類中時使用,Type表示工廠方法返回的對象類型。
總之,靜態工廠方法和構造器各有優勢,使用時需要衡量那種方法更好。
Item 2. 遇到多個構造器參數時要考慮用構建器
上一節介紹了靜態工廠方法,雖然相對構造器來說有一定的優勢,但是兩者都有一個局限,就是存在大量可選參數時表現不是很好。
重疊構造器
當面對大量可選參數時,一些人可能會選擇重疊構造器(telescoping constructor)。
文中舉了一個食品營養成分表的例子,表中有些參數是必選的,有些參數是可選的。
對於重疊構造器來說,第一個構造器只包含必選參數,第二個構造器有一個可選參數,第三個構造器有兩個,以此類推,直到最後一個構造器包含所有參數。
// Telescoping constructor pattern - does not scale well! public class NutritionFacts { private final int servingSize; // (mL) required private final int servings; // (per container) required private final int calories; // optional private final int fat; // (g) optional private final int sodium; // (mg) optional private final int carbohydrate; // (g) optional public NutritionFacts(int servingSize, int servings) { this(servingSize, servings, 0); } public NutritionFacts(int servingSize, int servings, int calories) { this(servingSize, servings, calories, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat) { this(servingSize, servings, calories, fat, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) { this(servingSize, servings, calories, fat, sodium, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) { this.servingSize = servingSize; this.servings = servings; this.calories = calories; this.fat = fat; this.sodium = sodium; this.carbohydrate = carbohydrate; } }
重疊構造器像套圈一樣,對參數進行賦值。
你必須很小心地將值和參數的位置一一對應,隨著參數數量的增加,你肯定不會記得第六個參數是什麽。
並且如果兩個類型相同參數的順序發生了調換,可能編譯期不會提示錯誤,但在運行時會報錯。
重疊構造器模式可行,但是當有許多參數時,客戶端代碼會很難編寫,並且可讀性很差。
JavaBeans模式
另一種解決辦法是JavaBeans模式,這種模式簡單並且靈活,也是我們最經常使用的,通過setter方法來設置參數。
// JavaBeans Pattern - allows inconsistency, mandates mutability public class NutritionFacts { // Parameters initialized to default values (if any) private int servingSize = -1; // Required; no default value private int servings = -1; // " " " " private int calories = 0; private int fat = 0; private int sodium = 0; private int carbohydrate = 0; public NutritionFacts() { } // Setters public void setServingSize(int val) { servingSize = val; } public void setServings(int val) { servings = val; } public void setCalories(int val) { calories = val; } public void setFat(int val) { fat = val; } public void setSodium(int val) { sodium = val; } public void setCarbohydrate(int val) { carbohydrate = val; } }
JavaBeans模式彌補了重疊構造器的不足,有著良好的可實現性和可讀性。
但是其也存在著不足:
JavaBeans是可變的,意思是在被創建之後它們的狀態可以通過setter方法隨之更改。
它們的域不能聲明為final,這也使它們不能成為不可變對象,不能保證線程安全。
Builder模式
Builder模式作為一種更好的方法,既能保證安全性,還有著良好的可讀性。
通過Builder類來返回一個builder對象,然後在客戶端調用Builder中的方法來設置參數,最後調用builder()方法來完成創建一個不可變對象。
Builder類是一個靜態的內部類,其中的方法和setter類似,並且可以實現鏈式調用,易於使用和閱讀。
/** * @ClassName: NutritionFacts * @Description: 構建器 * @author LJH * @date 2017年6月26日 下午8:57:03 */ public class NutritionFacts { // required private final int servingSize; private final int servings; // optional private final int calories; private final int fat; private final int sodium; private final int carbo; public static class Builder { private final int servingSize; private final int servings; private int calories = 0; private int fat = 0; private int sodium = 0; private int carbo = 0; public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } public Builder calories(int val) { calories = val; return this; } public Builder fat(int val) { fat = val; return this; } public Builder sodium(int val) { sodium = val; return this; } public Builder carbo(int val) { carbo = val; return this; } public NutritionFacts build() { return new NutritionFacts(this); } } private NutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbo = builder.carbo; } public int getServingSize() { return servingSize; } public int getServings() { return servings; } public int getCalories() { return calories; } public int getFat() { return fat; } public int getSodium() { return sodium; } public int getCarbo() { return carbo; } public static void main(String[] args) { NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbo(27).build(); System.out.println("The nutritionfacts of cocaCola \nServing Size: " + cocaCola.servingSize + " ml"); System.out.println("Servings: " + cocaCola.servings + " per container"); System.out.println("Calories: " + cocaCola.calories); System.out.println("Fat: " + cocaCola.fat + " g"); System.out.println("Sodium: " + cocaCola.sodium + " mg"); System.out.println("Carbo: " + cocaCola.carbo + " g"); } }View Code
使用Builder模式的好處如下:
- 構建器能通過builder方法和setter方法對其參數強加約束條件並且檢驗,如果不滿足條件可以拋出異常;
- 使用了構建器模式的類可以是不可變的;
- builder可以有多個可變參數(varargs);
- 構建器模式十分靈活,可以利用單個builder構建多個對象,參數可以改變,也可以自動填充;
- 利用帶有泛型的builder可以生成一個抽象工廠。
Builder模式的一個缺點就是,你必須自己編寫代碼創建。
總之,如果類的構造器或者靜態工廠方法中含有多個參數,優先選擇Builder模式。
Item 3. 用私有構造器或者枚舉類型強化Singleton屬性
Singleton既單例模式,作為設計模式中的一種,和其他模式一樣,如果沒有在項目中使用過真的很難理解。
單例模式雖然結構很簡單,但一開始看的時候我就懵了??,不知道為什麽要這麽做。
看了幾篇博客之後,把幹巴巴的代碼和實際應用結合起來後就變得容易理解。
其中一篇博客中舉了一個例子:
假設有這樣一個應用,其中需要讀取配置文件的內容。許多應用都會有自己的配置文件,開發人員可以對應用中的一些參數進行自定義,然後寫入配置文件。
在實際項目中通常會使用xml或者properties格式的文件作為配置文件,現在假設我們通過一個叫Config的類來實現讀取配置文件的功能。客戶端可以通過new一個Config實例來獲得操作配置文件內容的對象。
如果在程序運行時,有很多模塊都需要加載配置文件,那麽每使用一次都需要創建一個Config對象。這樣做肯定會產生問題,在程序運行時會存在多個Config對象,而這些對象中的內容都是相同的,浪費了內存資源。
那麽怎樣能減少這種浪費,每次用到Config類時,都返回同一個對象呢?答案就是單例模式。
實現單例模式的方法有很多種,詳細可以看這裏。
最常用的是餓漢式方法,優點是線程安全,創建簡單。
但是由於沒有實現懶加載,無論有沒有用到對象都會創建,浪費了一定的空間。
public class Singleton { private static Singleton INSTANCE = new Singleton(); private Singleton() { System.out.println("Singleton is created"); } public static Singleton getInstance() { return INSTANCE; } public static void printStr() { System.out.println("Singleton"); } }
java.lang.Runtime使用的就是該方法實現單例模式。
另一種是通過枚舉創建,這種方法是作者推薦的,利用了JDK1.5之後加入的Enum類。
public enum EnumSingleton { INSTANCE; public void printStr() { System.out.println("Singleton"); } public static void main(String[] args) { EnumSingleton.INSTANCE.printStr(); } }
可以看到這個方法非常簡潔明了,而且利用了枚舉類的特性,提供了序列化機制,防止多次實例化。
作者認為單元素的枚舉類型已經成為了實現單例模式的最佳方法。
不過感覺這種方法可讀性不是很好,一般情況還是會選擇餓漢式的創建方法。
Item 4. 通過私有構造器強化不可實例化的能力
我們經常會重復使用一些類,調用它們中的方法,這種情況下我們一般會把它們設計成一個工具類,這個類中包含一些靜態方法,我們可以直接通過類名調用。
例如:java.lang.Math,java.util.Arrays,java.util.Collections。
這些工具類被設計成不可實例化的類,因為實例化對對它們來說沒有意義。
然而在缺少顯示構造器的情況下,編譯期會自動提供一個公共的、無餐的默認構造器。
你可能會試圖通過抽象類的方式來使得這個類不能被實例化,但是抽象類可以被繼承,它的子類仍然可以被實例化。
並且這樣做會讓用戶以為設計成抽象類的目的是為了繼承。
那麽怎樣才能使一個類具有不可實例化的能力?
因為只有提供一個顯示的構造器,編譯期才不會自動生成默認構造器,所以我們只需要將構造器設為私有的(private)即可。
這樣由於外部的類和它的子類不能調用一個私有構造方法,這個類也就不能被實例化。
// Noninstantiable utility class public class UtilityClass { // Suppress default constructor for noninstantiability private UtilityClass() { throw new AssertionError(); } }
為了防止在這個類的內部調用構造器,可以使用一個斷言AssertionError()來避免這種情況的發生。
Item 5. 避免創建不必要的對象
一般來說,最好通過重用對象來代替每次都創建一個功能相同的新對象。
重用方式快速,並且簡單。如果一個對象是不可變的,那麽它就可以一直被重用。
創建字符串時,你可能會選擇這麽做:
String s = new String(“stringette”); // 不要這麽做!
用這種方式代替會更好:
String s = “stringette”;
初次使用字面量創建字符串也會在堆中創建對象,不過之後使用相同字符串時,都會利用字符串常量池中的引用,而不會創建新的對象。
(關於兩種方法創建字符串更詳盡的介紹可以看這兩篇Java字符串常量池和intern()方法、Java中的字符串字面量)
除了重用不可變對象,也可以重用那些已知不會被修改的可變對象。
例如書中的例子,計算一個人是否是在“baby boomer”時期出生的。下面是一個反例:
public class Person { private final Date birthDate; // Other fields, methods, and constructor omitted // DON’T DO THIS! public boolean isBabyBoomer() { // Unnecessary allocation of expensive object 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幾個不會被修改的對象,如果每次調用方法都創建幾個不必要的對象就會造成內存資源的浪費。
替代方法是:
class Person { private final Date birthDate; // Other fields, methods, and constructor omitted /** * The starting and ending dates of the baby boom. */ 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; } }
改進後的Person類只在初始化的時候創建Calendar、TimeZone和Date這個幾個對象,之後再調用isBabyBoomer方法就可以一勞永逸了。
除了上面提到的兩點,盡量做到以下來避免創建不必要的對象:
使用靜態工廠方法。
例如Boolean.valueOf方法,不會重復創建對象。
優先使用基本類型,而不是裝箱基本類型。
不同於基本類型,有時你可能會沒有意識到程序會自動裝箱,裝箱就意味著創建對象。(雖然Character、Byte、Short、Integer和Long實現了常量池技術,但是範圍只有[-127,128])
使用對象池,除非池中的對象是非常重量級的。
例如數據庫連接池,將數據庫連接對象保存在對象池中來重用。
Item 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); } }
在pop方法中,被彈出的元素的引用依然存在於數組中,這個元素實際上已經是一個過期引用——它永遠也不會再被訪問,但Java的垃圾回收無法知道這一點,除非該引用被覆蓋。
即使Stack對象不再需要這個元素,但是數組中的引用仍然可以讓它繼續存在。
在支持垃圾回收的語言中,內存泄漏的存在非常隱蔽。
為了解決這個問題,我們只需要做到:一旦對象的引用已經過期,就清空這些引用。
修改上面的例子:
public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; // Eliminate obsolete reference return result; }
那麽何時清空引用呢?
- 一旦元素被釋放掉,則該元素中包含的任何對象引用都應該被清空。
- 當你把引用放在緩存中,它就可能會被遺忘,導致過了很久之後雖然已經沒用了,但還是殘留在緩存中。這種情況我們應該偶爾去清理沒有用的項。
- 使用監聽器和其他回調時,我們應該顯式地註銷。最好的方法是只保存保存它們的弱引用,然後儲存在WeakHashMap中。
- 使用分析工具(Heap Profiler)來發現內存泄漏。
轉載請註明原文鏈接:http://www.cnblogs.com/justcooooode/p/7956048.html
參考資料
《Effective Java》第二章——創建和銷毀對象
https://jlordiales.me/2012/12/26/static-factory-methods-vs-traditional-constructors/
http://vojtechruzicka.com/avoid-telescoping-constructor-pattern/
https://www.ibm.com/developerworks/cn/java/j-lo-Singleton/index.html
https://medium.com/@biratkirat/learning-effective-java-item-4-4bc457fc5674
【讀書筆記】《Effective Java》——創建和銷毀對象