寫在前面
《Effective Java》原書國內(nèi)的翻譯只出版到第二版采桃,書籍的編寫日期距今已有十年之久。這期間昼弟,Java已經(jīng)更新?lián)Q代好幾次啤它,有些實踐經(jīng)驗已經(jīng)不再適用。去年底,作者結(jié)合Java7变骡、8离赫、9的最新特性,編著了第三版(參考https://blog.csdn.net/u014717036/article/details/80588806)塌碌。當前只有英文版本渊胸,可以在互聯(lián)網(wǎng)搜索到PDF原書。本讀書筆記都是基于原書的理解台妆。
以下是正文部分
如何科學(xué)地創(chuàng)建和銷毀對象(Creating and Destroying Objects)
實踐1 拋棄構(gòu)造函數(shù)翎猛,使用靜態(tài)工廠方法
什么是靜態(tài)工廠方法(static factory method)
簡單講,它就是一個返回當前對象實例的靜態(tài)方法接剩。示例如下:
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
1.1 優(yōu)點
- 構(gòu)造函數(shù)都以類名命名切厘,區(qū)分度不高,而靜態(tài)工廠方法可以個性化搂漠,對用戶更加友好迂卢。
- 靜態(tài)工廠方法不是必須重新創(chuàng)建一個對象,例如上面
Boolean
的代碼中桐汤,返回的是早前已經(jīng)創(chuàng)建好的對象而克。這類似于設(shè)計模式中的享元模式(Flyweight pattern),典型的怔毛,相同內(nèi)容的String
以及Enum
就用了該模式员萍。 - 靜態(tài)工廠方法可以返回子類。Java8中拣度,取消了接口不能包含static方法的限制碎绎,因此在接口上實現(xiàn)這種靜態(tài)工廠方法類,簡化了文檔抗果,用戶也只需關(guān)注主類筋帖。
- 靜態(tài)工廠方法可以根據(jù)參數(shù)而返回不同的內(nèi)容。如下示例冤馏,根據(jù)參數(shù)返回不同的類日麸,屏蔽了一些內(nèi)部細節(jié)。用戶只需要知道返回的是EnumSet或其子類即可逮光,哪怕以后EnumSet進一步細分代箭,代碼也幾乎不需要重構(gòu)。
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);
}
- 靜態(tài)工廠方法返回的對象類型涕刚,甚至可以在當前位置不存在嗡综。 這在SPI中用處較多。例如JDBC服務(wù)里面杜漠,java.sql.Driver接口是對外公開的一個加載驅(qū)動接口极景,但Jdk中并沒有相關(guān)實現(xiàn)察净,實際是由各sql廠商拿到接口后做的實現(xiàn)。
1.2 不足
- 沒有
public
或者protected
構(gòu)造函數(shù)的類是無法被繼承的戴陡。 - 在接口中塞绿,靜態(tài)工廠方法不如構(gòu)造函數(shù)顯眼,使用者難以發(fā)現(xiàn)恤批。
1.3 最佳實踐
Date d = Date.from(instant);
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
StackWalker luke = StackWalker.getInstance(options);
Object newArray = Array.newInstance(classObject, arrayLen);
FileStore fs = Files.getFileStore(path);
BufferedReader br = Files.newBufferedReader(path);
List<Complaint> litany = Collections.list(legacyLitany);
實踐2 當構(gòu)造函數(shù)包含過多參數(shù)時,使用builder
在實際的業(yè)務(wù)開發(fā)中裹赴,某些類可能包含豐富多樣的屬性喜庞。例如一個網(wǎng)站用戶可能包含用戶名、密碼棋返、昵稱延都、頭像、手機睛竣、證件晰房、郵箱、ID射沟、公司名等等信息殊者,有些是必選參數(shù),有些是可選參數(shù)验夯。當前端送來一個用戶注冊請求時猖吴,則需要創(chuàng)建一個用戶對象。這樣挥转,可能根據(jù)參數(shù)和用戶類型海蔽,需要N個復(fù)雜的構(gòu)造函數(shù)。下面列出幾種解決方案:
2.1 Telescoping Constructor 模式
難讀绑谣、難用党窜。
public class Account {
private final String name;
private final String password;
private final String phone;
private final String email;
public Account(String name, String password) {
this(name, password, null);
}
public Account(String name, String password, String phone) {
this(name, password, phone, null);
}
public Account(String name, String password, String phone, String email) {
this.name = name;
this.password = password;
this.phone = phone;
this.email = email;
}
}
2.2 JavaBean 模式
把對象初始化拆分成了幾條語句,代碼層面上更加清晰借宵,閱讀順暢幌衣。但是相應(yīng)的缺點是這種操作是非原子性的,在并發(fā)編程中暇务,需要專門為此做保護泼掠。
public class Account {
private String name;
private String password;
private String phone;
private String email;
public Account() {}
public String getName() { return name; }
public String getPassword() { return password; }
public String getPhone() { return phone; }
public String getEmail() { return email; }
public void setName(String name) { this.name = name; }
public void setPassword(String password) { this.password = password; }
public void setPhone(String phone) { this.phone = phone; }
public void setEmail(String email) { this.email = email;
}
2.3 Builder 模式
結(jié)合了前兩種方式的優(yōu)點,同時安全性和可繼承性得以保證垦细。但是這種方式也有缺陷择镇。首先,創(chuàng)建真正的對象前需要創(chuàng)建Builder對象括改,增大了系統(tǒng)開銷腻豌。在參數(shù)量少時,不宜過度使用該模式。
public class Account {
private String name;
private String password;
private String phone;
private String email;
private Account(Builder builder) {
this.name = builder.name;
this.password = builder.password;
this.phone = builder.phone;
this.email = builder.email;
}
public static class Builder {
private String name;
private String password;
private String phone = null;
private String email = null;
public Builder(String val1, String val2) {
name = val1;
password = val2;
}
public Builder phone(String val) {
phone = val;
return this;
}
public Builder email(String val) {
email = val;
return this;
}
public Account build() {
return new Account(this);
}
}
}
//調(diào)用方式
Account account = new Account.Builder("Amy", "123456").phone("15199998888").email("a@163.com").build();
實踐3 使用私有構(gòu)造函數(shù)或枚舉類型來強制單例
無狀態(tài)的對象通常采用單例模式吝梅。通常虱疏,有兩種方式來實現(xiàn)單例。這兩種方式都是通過私有構(gòu)造函數(shù)+公有的靜態(tài)實例成員實現(xiàn)的苏携。
3.1 單例實現(xiàn)A
沒有公有構(gòu)造函數(shù)確保了只有 INSTANCE
在初始化時調(diào)用私有構(gòu)造函數(shù)一次做瞪,之后,不能再創(chuàng)建該對象的任何實例右冻。該方式的一個缺點是可能遭受反射攻擊 装蓬,參考:AccessibleObject.setAccessible
。
// Singleton with public final field
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
3.2 單例實現(xiàn)B
使用了靜態(tài)工廠方法纱扭,通過getInstance去獲取實例牍帚。相對來說,該方式更加明晰乳蛾。并且暗赶,如果以后需要改造為非單例,對于用戶代碼沒有影響肃叶。
// Singleton with public final field
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public static Elvis getInstance() { return INSTANCE; }
public void leaveTheBuilding() { ... }
}
3.3 Enum實現(xiàn)單例
雖然看起來不太自然蹂随,但常常是實現(xiàn)單例的最佳方式。
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
實踐4 使用私有構(gòu)造函數(shù)來限制實例化
有時被环,我們編寫的對象只包含一組靜態(tài)的方法和變量糙及,這樣的對象是無需實例化的。但是在Java中筛欢,編譯器始終會采用默認構(gòu)造函數(shù)的策略浸锨。為了避免這種情況有以下2種方法:
4.1 抽象類
引入abstract
關(guān)鍵字,使得類類型為抽象類是無法實例化的版姑。但是這種方式有個缺點是如果有其他類繼承該抽象類柱搜,則繼承類是可以實例化的。并且抽象類容易迷惑用戶剥险,用戶會認為需要繼承這個類聪蘸,而不是直接使用。
4.2 添加私有構(gòu)造函數(shù)
編譯器只在沒有顯式構(gòu)造函數(shù)時為類添加默認構(gòu)造函數(shù)。只要我們在類中顯式添加一個私有構(gòu)造函數(shù),則該類就沒法實例化了砂竖。示例如下,AssertionError
并不是必須的娜遵,它只是確保沒有在類內(nèi)部誤調(diào)用。
該方式也有缺點:無法繼承壤短。由于子類的構(gòu)造函數(shù)總會(隱式或顯式地)調(diào)用父類的構(gòu)造函數(shù)设拟,當父類構(gòu)造函數(shù)為private
時慨仿,將無法完成該動作。
// Noninstantiable utility class
public class UtilityClass {
// Suppress default constructor for noninstantiability
private UtilityClass() { throw new AssertionError(); }
... // Remainder omitted
}
實踐5 使用依賴注入代替硬編碼資源
類與類之間通常都存在依賴關(guān)系纳胧。下面是兩種如何添加這種依賴關(guān)系的反面示例镰吆。這兩種方式下,對于多線程跑慕,多實例及參數(shù)化資源都沒法很好支持万皿。
// Inappropriate use of static utility - inflexible & untestable!
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {} // Noninstantiable
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
// Inappropriate use of singleton - inflexible & untestable!
public class SpellChecker {
private final Lexicon dictionary = ...;
private SpellChecker(...) {}
public static INSTANCE = new SpellChecker(...);
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
一種較好的解決方案是,在類的構(gòu)造函數(shù)中傳入相關(guān)資源核行。這就是依賴注入:在對象創(chuàng)建時注入相寇。該方式在靜態(tài)工廠方法,Builder模式同樣適用钮科。當工程過大時,某個類可能依賴成百上千資源婆赠,這時就需要注入框架來幫忙了绵脯,例如Dagger
, Guice
, Spring
等。
// Dependency injection provides flexibility and testability
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
實踐6 不要創(chuàng)建不必要的實例
對于不可修改的對象休里,采用共享模式而不是每次新建蛆挫。有助于提升程序的性能。
6.1 示例1
String對象的新建妙黍,此處字符串值是固定不變的悴侵。
// 每次調(diào)用都將創(chuàng)建一個新的對象,浪費
String s = new String("bikini");
// 享元模式
String s = "bikini";
6.2 示例2
一個正則匹配模式的例子拭嫁。
static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
此處正則表達式^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$
是固定不變的可免,但上述實現(xiàn)中,每次都會用它新建一個Pattern
對象做粤,因此可以把 Pattern 抽取出來浇借。原書作者實測性能提升6倍多。
On my machine, the original versiontakes 1.1 μs on an 8-character input string, while the improved version takes 0.17 μs, which is 6.5 times faster.
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
方法從未被調(diào)用到妇垢,那么ROMAN
的初始化是浪費的。原書作者認為:雖然可以通過懶加載的方式來進一步避免該問題肉康,但是增加了代碼的復(fù)雜性闯估,且性能實際提升價值不大。
6.3 基礎(chǔ)類型的使用
對于基礎(chǔ)類型吼和,應(yīng)盡量使用 int
, long
而不是 Integer
, Long
涨薪。后者可能觸發(fā)不必要的大量對象創(chuàng)建。
private static long sum() {
// 創(chuàng)建大量對象
Long sum = 0L;
// 創(chuàng)建1個對象
long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) sum += i;
return sum;
}
實踐7 解決過時引用問題
Java的自動垃圾回收機制纹安,使得程序員可能產(chǎn)生幻覺:不需要進行內(nèi)存管理尤辱。然而事實并非如此砂豌。如下就是一個內(nèi)存管理不當?shù)氖纠谶@個場景下光督,隨著棧的增長阳距,elements
可能擴張到很大,但是元素pop()
后size
減小结借,但是elements
并未聯(lián)動減小筐摘,那些沒有被垃圾回收掉的比size
標號大的對象成為了過時引用(obsolete reference,意思是再也不會用到的引用)船老。
// Can you spot the "memory leak"?
public class Stack {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private Object[] elements;
private int size = 0;
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];
}
/**
* * Ensure space for at least one more element, roughly * doubling the capacity each time the
* array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
7.1 解決方案
相對來說咖熟,具有垃圾回收機制的編程語言的內(nèi)存管理問題潛伏的更深,不易察覺柳畔,最終影響程序的性能馍管。修復(fù)示例程序中這種類型的問題很簡單:將引用設(shè)置為null
。這樣不僅使得垃圾能夠盡快回收掉薪韩,并且使得隨后的引用變得更安全确沸,錯誤引用將觸發(fā)NullPointerException
。
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
需要注意的是俘陷,不要過度使用這個方法罗捎,它會使編程變得復(fù)雜繁瑣。通常只有在程序員自行管理內(nèi)存的時候拉盾,才需要這個手段桨菜。對于其他情況,在Java中捉偏,對象的生命周期通常是在一定范圍內(nèi)倒得,例如 {}
中,跳出范圍告私,則自動銷毀屎暇。因此,將對象定義在最小化使用范圍中是一種較好的編程習(xí)慣驻粟。
7.2 高發(fā)場景:
- 緩存根悼,原書作者建議使用
WeakHashMap
來解決。 - 監(jiān)聽與回調(diào)蜀撑,如果向API注冊了回調(diào)函數(shù)而忘記去注冊會有問題挤巡。同樣,原書作者建議使用
WeakHashMap
來解決酷麦。
實踐8 避免使用finalizer
和cleaner
(實際開發(fā)中未使用矿卑,指導(dǎo)意義不大,暫未閱讀)
實踐9 try-with-resources
優(yōu)于try-finally
對于資源沃饶,在Java程序中使用完之后需要進行關(guān)閉動作母廷。例如文件流轻黑、socket連接等等。如果我們忽視了琴昆,則可能給程序帶來不良后果氓鄙,盡管這些資源有finalizer
來收尾。
9.1 try-finally
方式
一種常見的方式是try-finally
來確保資源能夠在正常/異常情況下也正確關(guān)閉业舍。但是當一段try-finally
要使用多個資源時抖拦,嵌套后的代碼看起來會非常復(fù)雜。另外舷暮,該方式還有一個問題時态罪,如果IO設(shè)備故障,那么 read
以及 close
操作都會拋出異常下面,但是因為close
在后复颈,所以之前的異常被沖掉,調(diào)試時只看到最后一個異常沥割,增大調(diào)試難度券膀。
// try-finally is ugly when used with more than one resource!
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0) out.write(buf, 0, n);
} finally {
out.close();
}
} finally {
in.close();
}
}
9.1 try-with-resources
方式
Java7引入的try-with-resources
,規(guī)定了資源對象必須實現(xiàn)AutoCloseable
接口驯遇,這個接口中僅包含了一個方法:void close()
。
public class MyFile implements AutoCloseable{
@Override
public void close() throws Exception {...}
}
try-with-resources
的調(diào)用語法如下蓄髓。在這種語法下叉庐,當 read
以及 close
操作都拋出異常時,close
的異常被抑制掉会喝,以確保程序員看到想要的那個異常陡叠。并且,整個代碼也更加簡潔肢执。
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0) out.write(buf, 0, n);
}
}
(完)