1. 程式人生 > 程式設計 >Effective Java理解(二)

Effective Java理解(二)

使用限定萬用字元來增加靈活性

  1. PECS ( producer-extends,consumer-super),如果一個引數化型別代表一個 T 生產者,使用 <? extends T>;如果它代表 T 消費者,則使用 <? super T>。 在Stack 示例中,pushAll 方法的 src 引數生成棧使用的 E 例項,因此 src 的合適型別為 Iterable<? extends E>;popAll 方法的 dst 引數消費 Stack 中的 E 例項,因此 dst 的合適型別是 Collection <? super E>。 PECS 助記符抓住了使用萬用字元型別的基本原則。 Naftalin 和 Wadler 稱之為獲取和放置原則。
  2. 如果編寫一個將被廣泛使用的類庫,正確使用萬用字元型別應該是強制性的。

泛型結合可變引數

  1. 可變引數和泛型不能很好地互動,因為可變引數機制是在陣列上面構建的脆弱的抽象,並且陣列具有與泛型不同的型別規則。 雖然泛型可變引數不是型別安全的,但它們是合法的。 如果選擇使用泛型(或引數化)可變引數編寫方法,請首先確保該方法是型別安全的,然後使用 @SafeVarargs 註解對其進行標註
  2. 比如List<? extends T>... lists 因為這裡規定了必須為T型別的子類,所以這裡是型別安全的
  3. 在 Java 8 中,註解僅在靜態方法和 final 例項方法上合法; 在 Java 9 中,它在私有例項方法中也變為合法。

列舉型別

許多列舉不需要顯式構造方法或成員,但其他人則可以通過將資料與每個常量關聯並提供行為受此資料影響的方法而受益。 使用單一方法關聯多個行為可以減少列舉。 在這種相對罕見的情況下,更喜歡使用常量特定的方法來列舉自己的值。 如果一些(但不是全部)列舉常量共享共同行為,請考慮策略列舉模式。

EnumMap

  1. 有一組植物代表一個花園,想要列出這些由生命週期組織的植物 (一年生,多年生,或雙年生)。為此,需要構建三個集合,每個生命週期作為一個,並遍歷整個花園,將每個植物放置在適當的集合中。
// Using an EnumMap to associate data with an enum
Map<Plant.LifeCycle,Set<Plant>>  plantsByLifeCycle =
    new
EnumMap<>(Plant.LifeCycle.class); for (Plant.LifeCycle lc : Plant.LifeCycle.values()) plantsByLifeCycle.put(lc,new HashSet<>()); for (Plant p : garden) plantsByLifeCycle.get(p.lifeCycle).add(p); System.out.println(plantsByLifeCycle); //lamba 使用 Collectors.groupingBy 的三個引數形式的方法,它允許呼叫者使用 mapFactory 引數指定 map 的實現 //在大量使用 Map 的程式中可能是至關重要的效能可以得到提升 System.out.println(Arrays.stream(garden) .collect(groupingBy(p -> p.lifeCycle,() -> new EnumMap<>(LifeCycle.class),toSet()))); //除此之外,如果所代表的關係是多維的,使用 EnumMap <...,EnumMap <... >> 複製程式碼

標記註解與標記介面

  1. 如果標記是應用於除類或介面以外的任何程式元素,則必須使用註解,因為只能使用類和介面來實現或擴充套件介面。如果標記僅適用於類和介面,那麼問自己問題:「可能我想編寫一個或多個只接受具有此標記的物件的方法呢?」如果是這樣,則應該優先使用標記介面而不是註解。這將使你可以將介面用作所討論方法的引數型別,這將帶來編譯時型別檢查的好處。
  2. 如果標記是大量使用註解的框架的一部分,則標記註解是明確的選擇。
  3. 如果你想定義一個沒有任何關聯的新方法的型別,一個標記介面是一種可行的方法。 如果要標記除類和介面以外的程式元素,或者將標記符合到已經大量使用註解型別的框架中,那麼標記註解是正確的選擇。 如果發現自己正在編寫目標為 ElementType.TYPE 的標記註解型別,那麼請花時間弄清楚究竟應該用註解型別,還是標記介面更合適。

lamba -- 除非必須建立非函式式介面型別的例項,否則不要使用匿名類作為函式物件

  1. lambda 沒有名稱和檔案; 如果計算不是自解釋的,或者超過幾行,則不要將其放入 lambda 表示式中。 一行程式碼對於 lambda 說是理想的,三行程式碼是合理的最大值。 如果違反這一規定,可能會嚴重損害程式的可讀性。 如果一個 lambda 很長或很難閱讀,要麼找到一種方法來簡化它或重構你的程式來消除它。
  2. Lambda 僅限於函式式介面。 如果你想建立一個抽象類的例項,你可以使用匿名類來實現,但不能使用 lambda。
  3. lambda 不能獲得對自身的引用。 在 lambda 中,this 關鍵字引用封閉例項,在匿名類中,this 關鍵字引用匿名類例項。 如果你需要從其內部訪問函式物件,則必須使用匿名類。

明智審慎地使用 Stream

  1. 過度使用流使程式難於閱讀和維護,使用流而不過度使用它們。其結果是一個比原來更短更清晰的程式

優先考慮流中無副作用的函式

  1. forEach 操作除了表示由一個流執行的計算結果外,什麼都不做,它應僅用於報告流計算的結果,而不是用於執行計算。

謹慎使用流並行

  1. 通常,並行性帶來的效能收益在 ArrayList、HashMap、HashSet 和 ConcurrentHashMap 例項、陣列、int 型別範圍和 long 型別的範圍的流上最好。這些資料結構的共同之處在於,它們都可以精確而廉價地分割成任意大小的子程式,這使得在並行執行緒之間劃分工作變得很容易。
  2. 不要嘗試並行化流管道,除非你有充分的理由相信它將保持計算的正確性並提高其速度。

給引數新增必要的限制

  1. 每次編寫方法或構造方法時,都應該考慮對其引數存在哪些限制。 應該記在這些限制,並在方法體的開頭使用顯式檢查來強制執行這些限制。 養成這樣做的習慣很重要。 在第一次有效性檢查失敗時,它所需要的少量工作將會得到對應的回報。
  2. 在日常開發介面的時候,會有很多插入更新等操作,這個時候使用 validation 來對引數做出限制。

必要的時候進行防禦性拷貝

  1. 如果一個類有從它的客戶端獲取或返回的可變元件,那麼這個類必須防禦性地拷貝這些元件。如果拷貝的成本太高,並且類信任它的客戶端不會不適當地修改元件,則可以用檔案替換防禦性拷貝,該檔案概述了客戶端不得修改受影響元件的責任。
  2. 其實就是深克隆和錢克隆的問題

慎用過載

  1. 過載(overloaded)方法之間的選擇是靜態的,如果在子類中過載例項方法並且在子類的例項上呼叫,它在編譯時就要選擇要呼叫哪個過載方法,執行時型別在每次迭代中都不同,但這不會影響對過載方法的選擇,都會執行父類的過載方法
  2. 重寫(overridden)方法之間的選擇是動態的,如果在子類中重寫例項方法並且在子類的例項上呼叫,則無論子類例項的編譯時型別如何,都會執行子類的重寫方法
//全列印  --  Unknown Collection
public class CollectionClassifier {

    public static String classify(Set<?> s) {
        return "Set";
    }

    public static String classify(List<?> lst) {
        return "List";
    }

    public static String classify(Collection<?> c) {
        return "Unknown Collection";
    }

    public static void main(String[] args) {
        Collection<?>[] collections = {
            new HashSet<String>(),new ArrayList<BigInteger>(),new HashMap<String,String>().values()
        };

        for (Collection<?> c : collections)
            System.out.println(classify(c));
    }
}

//列印各個子類
class Wine {
    String name() { return "wine"; }
}

class SparklingWine extends Wine {
    @Override String name() { return "sparkling wine"; }
}

class Champagne extends SparklingWine {
    @Override String name() { return "champagne"; }
}

public class Overriding {
    public static void main(String[] args) {
        List<Wine> wineList = List.of(
            new Wine(),new SparklingWine(),new Champagne());

        for (Wine wine : wineList)
            System.out.println(wine.name());
    }
}
複製程式碼
  1. 安全和保守的策略是永遠不要匯出兩個具有相同引數數量的過載。 => 可以為方法賦予不同的名稱,而不是過載它們。ObjectOutputStream類。對於每個基本型別和幾個引用型別,它都有其write方法的變體。這些變體都有不同的名稱,例如writeBoolean(boolean)、writeInt(int)和writeLong(long),而不是過載write方法。與過載相比,這種命名模式的另一個好處是,可以為read方法提供相應的名稱,例如readBoolean()、readInt()和readLong()。ObjectInputStream類實際上提供了這樣的讀取方法。
參考:
  1. 《effective java》3rd -- Joshua Bloch