2011年7月6日 星期三

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...