顯示具有 java 標籤的文章。 顯示所有文章
顯示具有 java 標籤的文章。 顯示所有文章

2014年1月27日 星期一

C++ 下 thread-safe 的 lazy initialization

《clang 避免 non-local static 物件初始化順序的方法》提到可用 static local variable, 然後用 method 傳回的方式避免產生 global static variable (藉此避免不同編譯單元的初始化問題)。以下是一個例子:

const struct Point* center()
{
  static Point* s_center = CreateCenterPoint();
  return s_center;
}

但是, 在 multi-thread 的環境下, s_center 可能被初始化兩次。假設 CreateCenterPoint() 會傳回不同的值, 或是外界可以改變取得的 Point, 有兩份 Point 會造成問題。

好消息是:

所以情況比想像中的安全。

不過, 其它 lazy initialization 的實作方式有可能出錯。更一般化的 lazy initialization 會用 Double-Checked Lock Pattern, 但是這個作法有不易察覺的漏洞

直覺的作法如下:

Singleton* Singleton::instance()
{
  if (pInstance == 0) { // 1st test
    Lock lock;
    if (pInstance == 0) { // 2nd test
      pInstance = new Singleton;
    }
  }
  return pInstance;
}

注意初始化 singleton 是三個步驟組成的:

  1. 配置一塊新記憶體
  2. 初始化新記憶體
  3. 將新記憶體的位置指向目標指標 (即 pInstance)

compiler 有可能更動三者的順序。最壞的情況下, thread A 執行了 1, 3, 還沒執行 2, 這時 thread B 發覺 pInstance 不是 0, 於是回傳尚未初始化的 pInstance。解法是要使用 memory barrier。或是在 C++11 後更可用跨平台的解法。詳情見 Double-Checked Locking Is Fixed In C++11

另外, Java 1.4 以前 Double-Checked Locking 也有問題。Java 1.5 後多了 volatile 表示「取出最新的值」, 才有辦法修正此問題。Effective Java 2/e Item 66 "Synchronize access to shared mutable data" 和 Item 71 "Use lazy initialization judiciously" 有詳細的討論。

2012年12月15日 星期六

Some tips about Android JNI

和 JNI 奮鬥了一陣子, 隨手備忘查到的東西

用法和一般 Java JNI 差不多, 所以可以先參考一般 JNI 的用法, 再來看 Android JNI Tips:

C 和 C++ 的寫法只有一點小差異, 見 Wikipedia 的範例, 這樣看別人的範例時, 可以一併參考 C 或 C++ 的 code

JNI 的用法和 reflection 的概念差不多, 要先取得 class、再取得 method、再呼叫 method。所以比較需要查的是型別參數:

  • 這裡的 Type Signatures
  • constructor 的名稱是 <init>
  • inner class 的型別用 $ 分隔, 比方 package com.mypkg 的 class A 內有 inner class B, 型別名稱為 Lcom.mypkg.A$B;。注意 inner class 為 non-static 時, 要先有 A 才能 new 出 B

2011年12月3日 星期六

CPython 的 garbage collection

來消化之前有讀沒時間寫的東西。

長期使用 python 後, 會很納悶為啥記憶體一直漲, 明明已沒用到了, 卻沒有減少。這篇提到 CPython 的 gc 機制採用 reference counting, 另有備案的 mark and sweep, 用來解決 circular reference。btw, 強烈推薦 Back to basic: Series on dynamic memory management, 淺顯地介紹各種 gc 運作的方式。

Scott 的說法, reference counting 算是苦工的半自動化管理記憶體, 因為實作細節交給實作者處理, 用 Python API 寫 CPython 的 extension 時, 開發者得自己管理物件的 reference count, 是件很辛苦的事。好處是在 reference count 為 0 的時候, 會立即回收記憶體。但缺點是, 若 a、b 兩物件互相參照到對方, 卻沒被任何人用到的話, a 和 b 都無法被回收, 因為 reference count 永遠會是 1。

為解決這個問題, 只好多提供 gc.collect 喚起 mark and sweep 的演算法清掉 a、b。但 mark and sweep 也有自己的問題, 主要有兩點:

  • 不像 reference counting 會在沒用到的第一時間點立即回收, 而是 mark and sweep 執行後才回收。
  • 執行 mark and sweep 的時候為確保沒有算錯 reference graph, 必須暫停目前所有執行中的 python process/thread, 結束後才可繼續執行。可想而知, 記憶體用量大後, 這個暫停時間也會太久, 不適合需要不斷有回應的程式 (如 GUI、Web)。

所以又有了 generational garbage collection, 關鍵的想法是: 觀察到大部份物件都很早死 (英年早逝啊~), 所以只要回收年輕的物件即可回收大部份的記憶體。於是將使用到的物件分不同「年代」, 預設只回收最年輕的一代, 沒回收到的物件就放到下一代。下回要再回收的時候, 就不動這個舊一代的物件, 減少要建立的 reference graph。

回想平時寫程式的結構, 可以想見, 像頻繁使用的 local variable 仍會被這個方法回收, 偶而用到的 global variable 只有第一次會算到, 之後被當作舊一代的物件後, 就不會再檢查。這樣只要有個方式確保不會漏查舊一代指向新一代的物件, 就可確保不會誤刪仍用到的物件。至於明明可回收卻沒回收到的物件, 相較於省下的時間來說, 算是可接受的取捨。generational garbage collection 值得一看, 有提到一些巧思, 取得計算時間和空間的平衡。

可想而知, CPython 用的是 generational gc, 而 JVM 也是。看來好東西大家都會一起用。

Btw, 即使了解了這些仍無法解釋為啥 CPython 有一堆沒有歸還一堆沒在用的記憶體, 實際的情況比想像中還複雜, CPython 有一些「絕對不會歸還」的記憶體, 像是 small integer pool, 和重覆回收使用的 PyIntObject, 或是有 circular reference 且這之中有物件實作 __del__。

2011-12-04 Update

看到 Thinker 針對本篇的補充, 貼過來備忘: CPython 的 GC 二、三事

2011年11月21日 星期一

Effective C++ item 23: 以 non-member、non-friend 取代 member function

看到這條款有種水到渠成的快感, 來寫下心得。

在我大學寫 Java 四年的時光裡, 我寫了一堆 over design 假 OOP 的東西。於是, 在我出社會工作的三年裡, 我改用 Python 盡可能的都寫成 function, 覺得很卡時 (像是一堆參數重覆傳來傳去), 才會包成 class。想體會看看兩種極端的寫法, 藉此看看能否從中悟出什麼道理。

這七年的時光合起來, 我沒悟出什麼大道理, 到是覺得後來這種寫法偶而是有點不便, 不過大部份時候還挺順的, 不會包得太笨重。直到看到 Effective C++ item 23, 才有種恍然大悟的感覺, 明白背後的原因。

作者提出一個觀點: 愈少程式能動到資料, 封裝的效果愈好。所以, 若這個操作可以寫成 non-member、non-friend function, 可保證它不能存取到 private data。再者, 也比較有機會讓不同 class 來重用這個 function。像是 std 裡面的 sort(), 就比放到 vector 裡成為 member function 來得好。既可提高 vector 的封裝效果, 也方便重用 sort (有提供 index 和 comparable 即可)。這篇兼顧「減少相依性」和「提高重用性」切入說明這則條款的精髓, 值得一讀。

初看這個觀點有些反直覺, 還是會覺得放到 class 裡比較像「封裝」。要找函式時也比較方便, 看 class 的 header 就可知道全部操作。但 item 23 接著強調: 若要方便找函式, 放在 namespace 下也有類似的效果, 如同 Java 寫成 utility class 的 static member function 一樣。

回想 Java 的 Arrays.sort() 和 Collections.sort() 也是同樣的設計, 只是 Java 沒 namespace, 這類函式改放到某個 class 下。當然, Python 的 sorted() 也是如此, 不過是放在 __builtin__ 這個 package 裡。其它類型的還有 max()、min() 等函式。Python 和 Java 都有提供方便的 max()、min() 來操作 container, 不知 C++ 有沒有類似的東西, 只看到 algorithm 有比較兩個數字的實作。

2011-11-22 Update

Aethanyc 提醒, STL 裡有 max_element()

2011年11月19日 星期六

C++ 建構式和解構式使用 virtual 的注意事項

原本打算快速掃過一遍《Effective C++ 3/e》, 有個大略印象, 之後再視需求仔細看相關守則, 但看到一半發現忘了一堆前面看過的東西。還是來寫寫筆記好了。

  • item 07: declare destructors virtual in polymorphic base classes。這樣 delete base pointer 時, 才會呼叫到真正的 destructor, 然後正確地依序往上呼叫各類別的 destructor。
  • item 09: never call virtual functions during construction or destruction。理由是 C++ 在 constructor / destructor 裡沒有多型的效果, 因為 C++ 認為在建構或解構時呼叫子類別的函式有危險, 有可能用到未定義的資料, 乾脆定成在 constructor / destructor 裡沒有多型的效果
  • 續上則, 補充說明原因。建構式的呼叫順序是由上往下, 而解構式是由下往上。所以若在建構式呼叫子類別的函式, 該函式可能用到子類別特有的欄位, 這時自然還沒初始化, 導致未定義行為。反之, 若在解構式呼叫子類別的函式, 子類別的解構式已清掉它的欄位, 這時再使用到也會導致未定義行為。
  • 續上則, 值得注意的是, 在 Java 裡沒有禁止這麼做。Java 類別欄位的 (class field) 的所有型別都有預設的初始值 (像是 0、false、null 等)。Java 任何時候呼叫任何函式, 都會有多型效果。在建構式或解構式時這麼做, 會呼叫到子類別的函式, 至於這類的行為是否符合需求, 就自行判斷啦。下面附上 Java 的測試碼, 順便測一下欄位和方法的行為, 注意兩者行為不同, 方法有多型效果。
class A {
 String m = "A.m";
 
 void p() { System.out.println("A.p(): " + m); }
 
 A() { System.out.println("A()"); p(); }
}

class B extends A {
 String m = "B.m";
 
 B() { System.out.println("B()"); p(); }
 
 void p() { System.out.println("B.p(): " + m); }
 
}

public class Test {
 public static void main(String[] args) {
  A a = new B();
  System.out.println("--- After new ---");
  a.p();
  System.out.println(a.m);
  System.out.println(((B)a).m);
 }

}
/*
執行結果:
A()
B.p(): null
B()
B.p(): B.m
--- After new ---
B.p(): B.m
A.m
B.m
*/

2011年10月30日 星期日

小心 C++ 的 non-local static 物件的初始順序

Java 有保證在第一次使用 class 內任何東西時, 會執行 class initialization 的程式。而 C++ 沒有保證不同編譯單元的 non-local static 物件的初始順序。

  • 編譯單元是指同一 object file 的原始碼和其引入的檔案
  • static 物件指不在 heap 也不在 stack 上的東西, 包含全域變數、namespace 上的變數、class 內 static member 等

這會導致一個可怕的事實: 使用另一個 object file 的 namespace 下的物件時 (這是滿常見的需求), 會導致未定義的行為。《Effective C++ 3/e》item 4 的建議解法是用函式存 local static 物件, 然後用函式取得該物件 (類似 singleton 的概念)。

用程式來說, 就是:

// Bad! Lead to an undefined behavior if clients access tfs.
FileSystem tfs;

// Good!
FileSystem& tfs() {
    static FileSystem fs;
    return fs;
}

附帶一提, 不同語言的規範差真多, 之前看 ARM 的組語, 才發覺之前只知道 x86 的東西, 而對 CPU 有些誤解。現在確信只學單一工具會有風險, 思維會受限於該工具, 誤以為那就是世界。人生有許多事也是如此, 想到莊子所說的話:

井蛙不可以語於海者,拘於虛也;﹔夏蟲不可以語於冰者,篤於時也;曲士不可以語於道者,束於教也。

體悟漸多後, 漸漸也不知要寫什麼心得才好。總之就是多看看, 時時提醒自己所知很窄, 別錯將一種方法當作只有這種方法。

2011年8月29日 星期一

Synchronization and the Java Memory Model 筆記

昨天看 Effective Java 提到 synchronization 同時提供 exclusive execution 和 communication, 但多數人會忽略後面這點。原本不懂這是什麼意思, 看了《Synchronization and the Java Memory Model》才知道是怎麼回事, Java Memory Model 真的和直觀的想法很不一樣。記錄一下筆記。

final class SetCheck {
  private int  a = 0;
  private long b = 0;

  void set() {
    a =  1;
    b = -1;
  }

  boolean check() {
    return ((b ==  0) ||
            (b == -1 && a == 1)); 
  }
}

這段程式有可能 return false, 因為 set() 裡的 a、b 設值可能會順序相反, set 和 check 可能同時被交錯執行。Java 只有保證單一 thread 自己執行的時候, 看起來像按順序由上而下、由左而右執行 (文中用 as-if-serial 表示, 這詞還滿妙的)。

在 multi-thread 時, 情況不同, 各個 thread 有自己的 memory, Java 沒有保證什麼時候 thread 之間才會看到更新後的情況, 這點滿可怕的。但可以用 synchronization lock 或宣告 volatile 強迫 thread 之間同步資料。我想這就是書上說 communication 的意思。

Java 有保證除 long 和 double 外的欄位, 更新值的時候是 atomic 的 (也就是包含 reference), 而 volatile 宣告的欄位也都是 atomic, 包含 volatile long 和 double。這裡要注意幾點:

  • i++ 包含 i + 1 和設值兩個操作, 沒有 atomic。所以設 volatile 也會有危險, 有需要的話還是要用 synchronized, 或照 Effective Java 的建議, 看能不能用 java.util.concurrent.atomic 的類別滿足需求。
  • long、double 外的欄位有 atomic, 但沒保證更新後其它 thread 會讀到新值。若要其它thread 不會讀到 stale value, 要宣告 volatile, 或在讀取前加上 synchronized。
  • array 有 volatile 不表示裡面的元素有 volatile (這還算直覺)。

後面說明各種特殊情況, 還有強調因為 CPU 管理 cache 方式, 有些時候其實 JVM 沒有保證 thread 之間的資料有更新, 但執行起來卻像有這一回事, 讓這種 bug 更難重製。

Visibility 那段值得細讀, 說明什麼情況下可確保 thread 之間有取到或寫入最新的值。像 start Thread 後會確保該 thread 讀到最新的值, 所以, 盡量在 start() 前建立物件, 還有別在 constructor 內 start thread, 避免 thread 執行後取得不完整的狀態。

2011-09-06 更新: 備忘簡短易懂的入門文件: Java Concurrency / Multithreading - Tutorial

2011年8月27日 星期六

Effective Java 讀書筆記: Item 66 - 用 synchronized 確保 concurrency

這節提供一個不直覺的錯誤

package trial;

import java.util.concurrent.TimeUnit;

public class Trial {
    private static boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (!stopRequested) {
                    i++;
                    System.out.println(i);
                }
            }
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

作者說在沒用 synchronization 機制的情況下, Java 沒有保證 thread 什麼時候看到彼此之間的修改。上面的例子, 在作者的機器上不會停。不過我自己試的結果是一秒後結束。作者說原因是 JVM 有可能最佳化 while 那行, 變成只檢查一次 stopRequested, 結果就不會停了。這個 VM 最佳化技巧叫做 hoisting (文中的例子則是改用 stack 上的變數取代 heap 上的)。

解法是宣告 stopRequested 時加上 volatile。不過作者後來接著強調 volatile 很難寫對, 叔叔有練過, 小朋友別亂用 volatile, 乖乖用 java.util.concurrent.atomic 下的類別, 像是 AtomicLong。這個 package 下的東西用了 machine-level 的方法減少 lock overhead, 比用 synchronized 有效率。

當然, 保險的話, 讀寫時都用 synchronized 最安全, 不過就會損失一些效率了。

2011年7月14日 星期四

Effective Java 讀書筆記: Item 50 - 考慮用更適合的型別替代 String

原本不覺得這則有什麼特別的, 翻 Head First - OOAD 後, 看到裡面第一個例子就是將一堆屬性的型別從 String 盡量換成 enum, 程式瞬間變清楚許多, 而且也可避免大小寫或拼錯字的問題, 字串比對有點囉唆, 而且 enum 的比對效率也比 String 快 (因為 enum 的每一元素都是 singlton, 可用 == 直接比對)。才想到之前也看過類似的小問題, 只是量不大, 只覺得程式並不清楚, 到沒有沒特別嚴重, 結果沒想到要去重構它們。

附上 Head First - OOAD 的例子, 看了比較有感覺。修改之前:

Guitar:
----------------------------
serialNumber: String
price: String
builder String
model: String
type: String
backWood: String
topWood: String

修改之後:

Guitar:
----------------------------
serialNumber: String
price: double
builder Builder
model: String
type: Type
backWood: Wood
topWood: Wood

上面的 Builder、Type、Wood 是 enum。這樣在提供搜尋吉它的介面時, 可以省掉一些前處理 (像是 toLower()、trim() ) 等操作。

2011年7月13日 星期三

Effective Java 讀書筆記: Item 52 - 用 interface 宣告變數

這是一則順手做不花力氣, 但不做也無傷大雅的建議。

作者建議這麼寫:

List<String> names = new ArrayList<String>();

而非這麼寫:

ArrayList<String> names = new ArrayList<String>();

這樣若要替換實作方式時, 只需改一行程式即可。比方說從 single-thread 要改為 multi-thread, 只要改變產生 names 的程式:

List<String> names = new Vector<String>();

剩下用到 names 的程式都不用改。

以前我覺得這是理所當然該做的事, 甚至用這做為「是否具備 Java 常識」的指標之一。後來發覺實在是太多人不知道這個準則, 而開始重新思考究竟沒做到這件事會有什麼後果?

結論是: 其實不太嚴重, 一來這鮮少發生。二來若真有那麼一天需要換實作, 配合 IDE 也可以很快地改完 (static typing 萬歲!!)。雖說不遵守這個準則也無傷大雅, 我仍覺得照著做較好。如同之前在《養成寫程式的好習慣》所言, 做好許多這類小細節, 長久下來會少掉很多問題, 這類的成本是難以追蹤的。

2011年7月12日 星期二

Effective Java 讀書筆記: Item 38 - 注意串接字串的效率

最近發覺不少人沒注意到這件事, 寫一下心得。

Joel 有寫一篇很長的描述, 說明串接字串的效率問題, 有興趣的話值得一讀。結論是, 若一直用 "+=" 的寫法 (或是 C 的 strcat ) 串接 N 個字串, 效率是 O(N^2)。Java 有提供 class StringBuffer 和 StringBuilder 提供 O(N) 的實作。做法是每次要 reallocate array 時就要兩倍的空間, 這樣 amortized time 就會是 O(N)。並且最多只浪費一半的空間。

StringBuffer 和 StringBuilder 都是繼承 AbstractStringBuilder, 將主要運算轉包給父類別。兩者的差別是 StringBuffer 的方法有加上 synchronized, 也就是說, StringBuffer 是 thread-safe, 而 StringBuilder 不是。所以, 在使用單一 thread 的情況下 (通常如此), 應該使用 StringBuilder 以獲得較快的時間。

附帶一提, 若目的是 join 的話, 用 Apache Commons LangStringUtils.join() 會比自己用 StringBuilder 刻來得省事。

2011年7月6日 星期三

Effective Java 讀書筆記: Item 38 - 檢查傳入參數

記錄讀書心得, 內容不一定和書上一致, 有些是我自己的看法。

早期發現, 早期治療。傳入參數時就先檢查, 這樣有錯時比較清楚原因為何。

針對方法的存取級別, 行為有所不同:

  • exposed API: 有錯就丟 exception。記得寫清楚註解, 說明會丟那些 exception
  • 內部用的: 用 assert 即可。錯了就讓它掛, 馬上修。外部使用時也可透過 java interpreter ( -ea / -da) 參數決定是否要執行 assert 的程式。不執行的話, 可以提昇速度

Effective Java 讀書筆記: Item 37 - 用 marker interface 定義型別

記錄讀書心得, 內容不一定和書上一致, 有些是我自己的看法。

marker interface 是指沒任何 method 的 interface, 充其量是用來表示一種型別, 有填入 meta data 的意味。但 marker interface 不如 annotations 彈性, 擴充 interface 的 method 意味著強迫所有 client code 要改寫, 而 annotations 無此困擾, 可以再擴充屬性。

有了 annotations 後, marker interface 還是有一點好處: 它可以保證在編譯時找出不當的參數傳遞。比方說若 ObjectOutputStream.writeObject() 的參數定義成 Serializable 而不是 Object 的話, 就能保證不會有人用錯 writeObject(), 傳入的物件一定支援 serialization。相對來說, annotations 得在執行時才能偵測到錯誤, 不如編譯時偵錯來得方便。

仔細想想, 之所以會有 annotations vs. maker interface 的 trade-off, 原因是 Java 為了避免多重繼承衍生的問題, 不支援多重繼承, 改用多重實作代替。但不能為 interface 的 method 提供預設行為, 大幅提高擴充 interface 的成本 (必須改寫所有用到的 class)

參考《侵入,无侵入? Annotation vs Interface》的一些例子, 比較明白有些框架會用 marker interface 或 annotations 表示 class 的屬性, 從而影響框架處理物件的方式。Joshua Bloch 強調: 若在使用 annotations 時用到 ElementType.TYPE (表示這個 annotation 只能用在 class、interface、enum), 多想一想是否用 marker interface 更合適。自己沒遇過實例, 還無法體會, 總之就先備忘吧。

Effective Java 讀書筆記: Item 36 - 善用 @Override

記錄讀書心得, 內容不一定和書上一致, 有些是我自己的看法。

在 method 加 @Override 可確保自己不會手殘沒覆寫到該覆寫的 method。經典的犯錯例子是覆寫 class T 的 equals, 但參數沒有用 Object 而不小心寫成 T, 像是這樣:

public equals(T other) // WRONG

有加 @Override 的話, 會造成 compilation error。盡可能用 @Override, 沒任何害處。

另一個我常用的內建 annotations 是 @Deprecated, 配合 IDE, 可先避免繼續使用不適用的方法 (會有 warning), 再來逐步換掉 caller, 最後再移掉 deprecated method。

2011年7月4日 星期一

Effective Java 讀書筆記: Item 35 - 使用 annotations 替代 naming patterns

記錄讀書心得, 內容不一定和書上一致, 有些是我自己的看法。

1.5 後有了 annotations, 方便將資訊加入 constructor、method、field、local variable 等處。官網有一個簡單的例子, 說明 annotations 的用處; 書上也是用 @Test 為例, 但寫得更清楚, 講比較多例子。

了解 annotations 強大之處最快的方法就是看 JUnit 4 的用法 (只要六十秒!!), 簡單易懂又沒有繼承class 的負擔, 也不用擔心打錯字時 compiler 抓不出錯誤 (像是 test 寫成 tset)。JCommander 也是不錯的例子, 很清楚地表示命令列參數, 滿好用的命令列框架。

btw, 初學 annotations 時, 我不太能抓到它的意思。搞半天才明白它只能填入 meta data, 還要另寫程式使用 reflection 讀出 annotations 才有用處。也就是定好規格, 用 annotations 填入規格的細節資訊, 再用另外的工具讀出 annotations 的內容做對應處理。

乍看之下, Python 的 decorator 語法和 annotations 很像, 但彈性多了: decorator 本身就是操作方式, 而不是 meta data, 方便使用; 再加上 Python 的 function 是 first-class object, 不需使用「reflection」即可操作 function, 方便實作。

2011年7月3日 星期日

Effective Java 讀書筆記: Item 32 - 使用 EnumSet 替代 bit fields

記錄讀書心得, 內容不一定和書上一致, 有些是我自己的看法。

這節說明有 java.util.EnumSet 這種好東西, 不用擔心使用 enum 後就沒有位元運算可用。以往用 int 表示 constant 時, 常會技巧性地將常數設成 2 的次方, 方便之後用位元運算表示常數的聯集、找出交集等。而 EnumSet 針對 enum 實作了相關操作, 底層也是使用 long 或 long[] 表示, 不用擔心時間或空間成本。

有興趣學習位元運算的技巧的話, 看實際實作 EnumSet 的 RegularEnumSet 和 JumboEnumSet 的原始碼可以學到不少東西, 像是用 population count 速算集合裡的個素。

Effective Java 讀書筆記: Item 31 - 透過 instance field 傳入數值, 別用 ordinal 代替

記錄讀書心得, 內容不一定和書上一致, 有些是我自己的看法。

這則和後面幾則的重點之一是別亂用 ordinal 做些隱諱的事, 像是「巧妙地」用 ordinal() 表示用到的數字, 比方說 enum { NONE, SINGLE, COUPLE }, 這樣 ordinal() 剛好可表示人數。相信同意「explicit is better than implicit」的人不會對此有異議。

那 ordinal 能用來幹嘛呢? 作者建議盡可能別用它, 它主要的目的是給 EnumSet 和 EnumMap 這類資料結構使用。

Effective Java 讀書筆記: Item 30 - 善用 enum 表示常數和相關操作

記錄讀書心得, 內容不一定和書上一致, 有些是我自己的看法。

參照官網介紹的例子 enum Planet, 可以清楚地明白使用 enum 的好處。書上列舉的好處有:

  • 安全、易讀、執行速度不輸 int 的實作版
  • 天生 immutable
  • 已實作 equals()、hashCode()、Serializable、Comparable

Making the Most of Java 5.0: Enum Tricks 這篇進一步說明使用 enum 的技巧:

  • reverse lookup。key 可以是 code 或字串。enum 會自動生成 valueOf(String) 提供字串的反查, 但有覆寫 toString() 的話, valueOf() 就失效了。這時記得提供自己寫的 fromtString() (不能覆寫 valueOf(), 因為 enum 的方法都是 final)
  • template method。書上稱這個為「constant-specific method implementations」, 重點是藉由 abstract method 保證之後新增的欄位不會漏實作必要的 method。

這則後半都在討論如何寫出好程式, 讓後續維護的人很難犯錯, 這也是作者不斷強調的精神, 一連串的討論值得細細思考。

當 enum 的行為只差在數值不同時, 一個 method 只有一種行為, 用一個 method 即可; 但若要提供多種不同行為時, 實作上有許多選擇:

  1. 只用一個 method 使用 switch 來決定行為
  2. 用 abstract method + constant-specific method implementations 各別實作對應的行為
  3. 類似上面的作法, 但將運算轉包給 nested enum, 使用 composition 的技巧 (見後文說明)。作者稱這個為「the strategy enum pattern」

第一個作法最簡單, 缺點是增加新的常數時, 不小心漏寫對應的程式時, 不會 compile error, 也不會有 runtime error。第二個作法透過 abstract method, 漏寫會導致 compilation error。

但第二個作法也有個小問題: 不同方法之間無法重用類似的程式, 而重覆的程式容易造成 bug。於是有第三個作法來避免這個問題, 不過也提昇實作複雜度, 程式變得沒那麼易懂。

書上舉的範例是: 定義 enum PayrollDay 來表示星期幾, 並提供方法依當天的工作時數來計算薪資。書上的範例 code 如下:

enum PayrollDay {
    MONDAY(PayType.WEEKDAY),
    TUESDAY(PayType.WEEKDAY),
    WEDNSDAY(PayType.WEEKDAY),
    THURSDAY(PayType.WEEKDAY),
    FRIDAY(PayType.WEEKDAY),
    SATURDAY(PayType.WEEKEND),
    SUNDAY(PayType.WEEKEND);

    private final PayType payType;
    PayrollDay(PayType payType) { this.payType = payType; }

    double pay(double hoursWorked, double payRate) {
        return this.payType.pay(hoursWorked, payRate);
    }

    private enum PayType {
        WEEKDAY {
            double overtimePay(double hours, double payRate) {
                return hours <= HOURS_PER_SHIFT ? 0 :
                    (hours - HOURS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            double overtimePay(double hours, double payRate) {
                return hours * payRate / 2;
            }
        };
        private static final int HOURS_PER_SHIFT = 8;

        abstract double overtimePay(double hours, double payRate);

        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            return basePay + overtimePay(hoursWorked, payRate);
        }
    }
}

這個寫法有兩個特色:

  • 將實際的計算外包給 PayType, 藉此共用算法
  • 透過 constructor 強迫選擇 PayType。若有新的情況, 比方新年假期要算兩倍薪資, 開發者會記得寫新的 PayType。

依作者的思維, 讓維護的人不易犯錯比較重要。在這前提下, 讓程式沒那麼直接, 是可以容忍的 trade-off。我個人也認為如此, 開發者的功力會持續進步, 就愈來愈能看懂這類「進階技巧」, 而不會覺得這類寫法比較難懂。長遠來看, 是比較好的選擇。

2011年7月1日 星期五

Effective Java 讀書筆記: Item 10 - 總是要覆寫 toString

記錄讀書心得, 內容不一定和書上一致, 有些是我自己的看法。

toString 方便顯示訊息給使用者看和除錯, 要實作它應該沒什麼爭議。作者一些額外的考量:
  • 是否要在文件 (註解) 裡說明詳細的格式, 並傳回包含所有資訊的字串。附帶好處是可用來和物件轉換, 方便和外界輸入輸出, 或寫入硬碟作為永久資料。可以是XML、JSON 或自己訂的特殊格式
  • 提供一個 static factory method 轉換字串會更方便
  • 缺點是, 一但明確在文件中訂了格式, 日後就不方便更改。感覺得出來作者一直都很強調「contract 」, 非常小心看待 public API 之類的事
  • 字串包含的任何資訊都要有對應的取得方法, 不然會讓程式設計師 parse toString() 的結果, 成為易錯且難以擺脫的 de factor API
btw, 我發覺寫讀書心得的難處之一是, 得想辦法將英文的概念轉成中文, 有些說法在英文裡很直覺, 但直譯成中文會很怪, 也可能是我不習慣這些詞的中文用法吧。

Effective Java 讀書筆記: Item 9 - 覆寫 equals 時, 一定要覆寫 hashCode

記錄讀書心得, 內容不一定和書上一致, 有些是我自己的看法。

覆寫 equals 卻沒覆寫 hashCode 的話, 使用 HashSet / HashMap / Hashtable 或任何用到 hashCode 的 class 就會出包: 使得兩個邏輯上相等的物件, 在 hash 的階段就被視作不同物件, 而無法找回來。

實作一個好的 hashCode 是一門學問, 作者列了很多注意事項, 看描述不如看 code, 引用 String 的 hashCode:
@Override
public int hashCode() {
    int h = hash;
    if (h == 0) {
        int off = offset;
        char val[] = value;
        int len = count;

        for (int i = 0; i < len; i++) {
            h = 31*h + val[off++];
        }
        hash = h;
    }
    return h;
}
value、offset、count 是 member field。不同 String 之間有共享 value, 用來避免在呼叫 substring 後增加重覆內容, 因此浪費記憶體。value[offset] ... value[offset + count -1] 是這個 String 實際用到的內容, 所以計算 hash code 時只看這些字元。

上面的程式有幾個重點
  • result = 31 * result + c 的數學形式
  • 選用質數當乘數較好 (如31), 別選 2 的倍數, 乘到溢位就都變零了
  • 31 的另一好處是近代 JVM 會做最佳化, 31*h 會自動轉成 (h<<5) - h
  • 用乘法可確保相同字元在不同順序的情況下, 會有不同的 hash code
其它相關要點
  • 各種 primitive type 都有轉為 int 的方式, 像 long n 可用 (int)(n ^ (n>>>32))、float f 可用 Float.floatToIntBits(f)
  • 寫 unit test 確保邏輯上相同的物件有相同的 hash code
  • hashCode 計算量太大的話, 可考慮用 lazy evaluation + cache
  • 別為了省計算時間而簡化產生 hash code 的方法, 這會反映在 hash table 的 collision 上, 資料量大時可能更不划算

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...