Effective Java英文第三版讀書筆記(1) -- 科學地創建和銷毀對象

寫在前面

《Effective Java》原書國內的翻譯只出版到第二版,書籍的編寫日期距今已有十年之久。這期間,Java已經更新換代好幾次,有些實踐經驗已經不再適用。去年底,作者結合Java7、8、9的最新特性,編著了第三版(參考https://blog.csdn.net/u014717036/article/details/80588806)。當前只有英文版本,可以在互聯網搜索到PDF原書。本讀書筆記都是基于原書的理解。


以下是正文部分

如何科學地創建和銷毀對象(Creating and Destroying Objects)

實踐1 拋棄構造函數,使用靜態工廠方法

什么是靜態工廠方法(static factory method)

簡單講,它就是一個返回當前對象實例的靜態方法。示例如下:

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

1.1 優點

  1. 構造函數都以類名命名,區分度不高,而靜態工廠方法可以個性化,對用戶更加友好。
  2. 靜態工廠方法不是必須重新創建一個對象,例如上面 Boolean 的代碼中,返回的是早前已經創建好的對象。這類似于設計模式中的享元模式(Flyweight pattern),典型的,相同內容的String以及Enum就用了該模式。
  3. 靜態工廠方法可以返回子類。Java8中,取消了接口不能包含static方法的限制,因此在接口上實現這種靜態工廠方法類,簡化了文檔,用戶也只需關注主類。
  4. 靜態工廠方法可以根據參數而返回不同的內容。如下示例,根據參數返回不同的類,屏蔽了一些內部細節。用戶只需要知道返回的是EnumSet或其子類即可,哪怕以后EnumSet進一步細分,代碼也幾乎不需要重構。
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);
}
  1. 靜態工廠方法返回的對象類型,甚至可以在當前位置不存在。 這在SPI中用處較多。例如JDBC服務里面,java.sql.Driver接口是對外公開的一個加載驅動接口,但Jdk中并沒有相關實現,實際是由各sql廠商拿到接口后做的實現。

1.2 不足

  1. 沒有public或者protected構造函數的類是無法被繼承的。
  2. 在接口中,靜態工廠方法不如構造函數顯眼,使用者難以發現。

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 當構造函數包含過多參數時,使用builder

在實際的業務開發中,某些類可能包含豐富多樣的屬性。例如一個網站用戶可能包含用戶名、密碼、昵稱、頭像、手機、證件、郵箱、ID、公司名等等信息,有些是必選參數,有些是可選參數。當前端送來一個用戶注冊請求時,則需要創建一個用戶對象。這樣,可能根據參數和用戶類型,需要N個復雜的構造函數。下面列出幾種解決方案:

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 模式

把對象初始化拆分成了幾條語句,代碼層面上更加清晰,閱讀順暢。但是相應的缺點是這種操作是非原子性的,在并發編程中,需要專門為此做保護。

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 模式

結合了前兩種方式的優點,同時安全性和可繼承性得以保證。但是這種方式也有缺陷。首先,創建真正的對象前需要創建Builder對象,增大了系統開銷。在參數量少時,不宜過度使用該模式。

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);
    }
  }
}
//調用方式
Account account = new Account.Builder("Amy", "123456").phone("15199998888").email("a@163.com").build();

實踐3 使用私有構造函數或枚舉類型來強制單例

無狀態的對象通常采用單例模式。通常,有兩種方式來實現單例。這兩種方式都是通過私有構造函數+公有的靜態實例成員實現的。

3.1 單例實現A

沒有公有構造函數確保了只有 INSTANCE 在初始化時調用私有構造函數一次,之后,不能再創建該對象的任何實例。該方式的一個缺點是可能遭受反射攻擊 ,參考:AccessibleObject.setAccessible

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

3.2 單例實現B

使用了靜態工廠方法,通過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實現單例

雖然看起來不太自然,但常常是實現單例的最佳方式。

// Enum singleton - the preferred approach 
public enum Elvis { 
  INSTANCE;
  public void leaveTheBuilding() { ... } 
}

實踐4 使用私有構造函數來限制實例化

有時,我們編寫的對象只包含一組靜態的方法和變量,這樣的對象是無需實例化的。但是在Java中,編譯器始終會采用默認構造函數的策略。為了避免這種情況有以下2種方法:

4.1 抽象類

引入abstract關鍵字,使得類類型為抽象類是無法實例化的。但是這種方式有個缺點是如果有其他類繼承該抽象類,則繼承類是可以實例化的。并且抽象類容易迷惑用戶,用戶會認為需要繼承這個類,而不是直接使用。

4.2 添加私有構造函數

編譯器只在沒有顯式構造函數時為類添加默認構造函數。只要我們在類中顯式添加一個私有構造函數,則該類就沒法實例化了。示例如下,AssertionError并不是必須的,它只是確保沒有在類內部誤調用。
該方式也有缺點:無法繼承。由于子類的構造函數總會(隱式或顯式地)調用父類的構造函數,當父類構造函數為private時,將無法完成該動作。

// Noninstantiable utility class
public class UtilityClass { 
  // Suppress default constructor for noninstantiability 
  private UtilityClass() { throw new AssertionError(); } 
  ... // Remainder omitted 
}

實踐5 使用依賴注入代替硬編碼資源

類與類之間通常都存在依賴關系。下面是兩種如何添加這種依賴關系的反面示例。這兩種方式下,對于多線程,多實例及參數化資源都沒法很好支持。

// 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) { ... }
 }

一種較好的解決方案是,在類的構造函數中傳入相關資源。這就是依賴注入:在對象創建時注入。該方式在靜態工廠方法,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 不要創建不必要的實例

對于不可修改的對象,采用共享模式而不是每次新建。有助于提升程序的性能。

6.1 示例1

String對象的新建,此處字符串值是固定不變的。

// 每次調用都將創建一個新的對象,浪費
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})$是固定不變的,但上述實現中,每次都會用它新建一個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方法從未被調用到,那么ROMAN的初始化是浪費的。原書作者認為:雖然可以通過懶加載的方式來進一步避免該問題,但是增加了代碼的復雜性,且性能實際提升價值不大。

6.3 基礎類型的使用

對于基礎類型,應盡量使用 int, long 而不是 Integer, Long。后者可能觸發不必要的大量對象創建。

private static long sum() {
  // 創建大量對象
  Long sum = 0L; 
  // 創建1個對象
  long sum = 0L;
  for (long i = 0; i <= Integer.MAX_VALUE; i++) sum += i;
  return sum; 
}

實踐7 解決過時引用問題

Java的自動垃圾回收機制,使得程序員可能產生幻覺:不需要進行內存管理。然而事實并非如此。如下就是一個內存管理不當的示例,在這個場景下,隨著棧的增長,elements 可能擴張到很大,但是元素pop()size減小,但是elements 并未聯動減小,那些沒有被垃圾回收掉的比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 解決方案

相對來說,具有垃圾回收機制的編程語言的內存管理問題潛伏的更深,不易察覺,最終影響程序的性能。修復示例程序中這種類型的問題很簡單:將引用設置為null。這樣不僅使得垃圾能夠盡快回收掉,并且使得隨后的引用變得更安全,錯誤引用將觸發NullPointerException

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

需要注意的是,不要過度使用這個方法,它會使編程變得復雜繁瑣。通常只有在程序員自行管理內存的時候,才需要這個手段。對于其他情況,在Java中,對象的生命周期通常是在一定范圍內,例如 {}中,跳出范圍,則自動銷毀。因此,將對象定義在最小化使用范圍中是一種較好的編程習慣。

7.2 高發場景:

  • 緩存,原書作者建議使用WeakHashMap來解決。
  • 監聽與回調,如果向API注冊了回調函數而忘記去注冊會有問題。同樣,原書作者建議使用WeakHashMap來解決。

實踐8 避免使用finalizercleaner

(實際開發中未使用,指導意義不大,暫未閱讀)

實踐9 try-with-resources優于try-finally

對于資源,在Java程序中使用完之后需要進行關閉動作。例如文件流、socket連接等等。如果我們忽視了,則可能給程序帶來不良后果,盡管這些資源有finalizer來收尾。

9.1 try-finally方式

一種常見的方式是try-finally來確保資源能夠在正常/異常情況下也正確關閉。但是當一段try-finally要使用多個資源時,嵌套后的代碼看起來會非常復雜。另外,該方式還有一個問題時,如果IO設備故障,那么 read 以及 close操作都會拋出異常,但是因為close在后,所以之前的異常被沖掉,調試時只看到最后一個異常,增大調試難度。

// 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,規定了資源對象必須實現AutoCloseable接口,這個接口中僅包含了一個方法:void close()

public class MyFile implements AutoCloseable{
  @Override
  public void close() throws Exception {...}
}

try-with-resources的調用語法如下。在這種語法下,當 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);
  }
}

(完)

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容