Effective Java 筆記

對全部對象都通用的方法

equals和hashCode方法的關係

  1. 重寫equals方法必須也要重寫hashCode方法。
  2. equals用的屬性沒變,則屢次調用hashCode返回值也必須保持不變。
  3. equals比較相等的對象,hashCode也必須相同。反之否則。
  4. 所處相同hash bucket的對象,hashCode可能不一樣,由於在求解bucket位置時會對hashCode進行截斷,根據bucket大小採用後段值。

clone方法的設計原則

  1. Cloneable接口並不提供接口方法clone,clone是Object類實現的基本方法。
  2. 實現Cloneable接口的類應該提供一個public的clone函數覆蓋原來protect的方法。
  3. clone方法首先調用super的clone方法,而後再處理須要深層拷貝的內部屬性。
  4. 道行不深不要使用該接口。

Comparable接口的設計原則

  1. 傳遞(a>b,b>c則a>c),對稱(a>b則b<a)。
  2. 最好compareTo方法和equals方法保持一致。

類和接口

訪問控制

  1. 頂層類(非嵌套)和接口只有兩種訪問級別,包私有和公有的。聲明爲public則類爲公有的,無聲明則默認包私有。
  2. 成員域、方法、嵌套類和嵌套接口有private、包私有(默認)、protected和public四種級別。可訪問性依次加強。
  3. 子類在重寫父類方法時,可訪問性只能保持不變或者加強。由於經過超類的引用也能夠正常的使用子類實例。
  4. 實例域必定不要設置爲public。不然,會失去對該域的控制權。

公有類的暴露公有域

公用類若是暴露了本身的域,則會致使客戶端濫用該域,而且該公用類在之後的升級中沒法靈活的改變屬性的表達方式。java

不可變對象的設計

  1. String、基本類型的包裝類是不可變對象。
  2. 設計不可變類應該遵循如下準則:數組

    1. 不要提供任何修改對象狀態的方法。
    2. 保證類不會被擴展。通常類能夠加final。
    3. 全部的域都是final。
    4. 全部的域都是私有的。
    5. 保證全部可變域沒法被客戶端得到。
  3. 不可變對象線程安全,能夠被自由的共享。不可變類不該該提供clone和拷貝構造器,直接共享最好,可是String仍是提供了拷貝構造器。
  4. 不可變類也有必定的劣勢,由於一個操做可能涉及多個臨時的不可變類,而致使大量對象的建立和銷燬,因此此時應該採用不可變類配套的可變類。如String類對應的StringBuilder。
  5. 應該提供儘量小的可變狀態。

複合優先於繼承

  1. 繼承會破壞封裝性,實現繼承須要對父類的實現進行充分的瞭解。因此父類和子類應該實如今同一個包內,由同一個開發團隊維護。
  2. 一個類在設計時應該明確指明是否是爲了可被繼承而設計的。
  3. 一個類若是沒有考慮本身可能被繼承,有些方法可能會被重寫,則其內部調用這些可被重寫方法的方法就可能會出現意想不到的異常行爲。繼承該方法的子類會調用父類的內部方法,而父類內部方法的更新可能會致使子類的異常。

接口優於抽象類

  1. 現有的類能夠很容易的加入新的接口,由於接口能夠實現多個。
  2. 類只容許有一個父類,若是用抽象類描述共性,則須要該共性的類必須爲該抽象類的後代,即便這些子類並無很顯然的關係。
  3. 接口可讓咱們實現非層次結構的類框架。若是採用抽象類,則屬性組合可能致使子類的組合爆炸。
  4. 接口的缺點是擴展新方法時,全部實現該接口的類都要從新添加該方法,而抽象類能夠提供默認實現。不過,如今Java8提供了default描述默認實現方法,彷佛這種弊端能夠避免。

接口中定義屬性

  1. 接口中定義的屬性默認爲final static。
  2. 最好不要用接口來定義屬性,由於實現該接口的類會引入這些屬性,形成類的命名空間污染。接口應該用來描述類能夠執行的動做。

內部類的設計

  • 靜態成員類:用static修飾的內部類,能夠不依賴於外部實例進行建立。
  • 非靜態成員類:建立時依賴於外部類的實例,而且建立後與外部實例綁定。無靜態屬性。
  • 匿名內部類:做爲參數,只會被實例化一次。和非靜態成員相似。在非靜態環境中,會與外部類的實例綁定。
  • 局部類:聲明局部變量的地方能夠聲明局部類,它有名字,能夠在局部被屢次實例化。在非靜態環境中,會與外部類的實例綁定。

泛型

不要在代碼中使用原生類型

以下代碼使用原生類型:安全

ArrayList a=new ArrayList<String>();
    a.add(new Object());

以上代碼編譯和運行均可以經過,可是埋下了不少隱患。併發

List<String>是List的子類,而不是List<Object>的子類。List這種原生類型逃避了類型檢查。app

List中add任何對象都對。List<?>的變量能夠引用任何參數化(非參數也能夠)的List,可是沒法經過該變量添加非null元素。框架

假設Men extends Person, Boy extends Men:ide

  1. <? extends T>表示上界,<? super T>表示下界。
  2. ArrayList<? extends Men> ml=new ArrayList<Boy>();,等號右邊部分能夠是Men與其子類的參數化ArrayList,或者是ArrayList<>()和ArrayList()。初始化時能夠在new ArrayList<Boy>()中填入Boy對象,此後不能再往ml裏存元素,從ml的視角,ml多是ArrayList<Men>、ArrayList<Boy>等等,存入任何Men或者其子類對象都不合適,爲了安全起見都不容許存。只能取元素,而且取出的元素只能賦值給Men或者其基類的引用,由於其中元素可能存了任何Men的子類,爲了保險起見取出的值用Men或其基類表示。
  3. ArrayList<? super Men> ml=new ArrayList<Person>();,初始化時能夠存Person對象,以後能夠再存入Men(與其子類,這是默認的),可是再存入Person對象是錯誤的。從ml視角,等號右邊能夠是ArrayList<Person>()、ArrayList<Men>()等等,因此最高只能存入Men對象。取出的元素都是Object,由於等號右邊能夠是ArrayList<Object>()。
  4. 總結2和3條,可知 <? extends T><? super T>是對等號右邊實參數化ArrayList的限制,而不是對ArrayList中可存入元素的描述。由於從引用ml中沒法得知其實際指向的是那種參數化的ArrayList實例,因此再往其中添加元素時會採用最謹慎的選擇。

列表和數組的區別

數組是協變的,也就是Fruit[] fs= new Apple[5];是合法的,由於Apple是Fruit的子類,則數組也成父子關係,而列表則不適用於該規則。數組的這種關係容易引起錯誤,如fs[0]= new Banana(),編譯時沒錯,這在運行時報錯。 函數

建立泛型數組是非法的,如new E[]; new List<E>[]; new List<String>[] 。泛型參數在運行時會被擦除,List<String>[]數組可能存入List<Integer>對象,由於運行時二者都是List對象,這顯然是錯誤的,因此泛型不容許用在數組中。 工具

以下代碼在編譯時不會出錯,在運行時出錯java.lang.ClassCastException測試

ArrayList<String> list=new ArrayList<String>();  
        for (int i = 0; i < 10; i++) {  
            list.add(""+i);  
        }
        //該行報錯       
        String[] array= (String[]) list.toArray();  
}

緣由很迷,toArray返回的是Object[]數組,可是不能強制轉化爲String[],明明元素實際類型是String。有的解釋說,在運行時只有List的概念,而沒有List<String>概念。我感受事情沒這麼簡單。

泛型和泛型方法

泛型是在整個類上採用泛型,這樣能夠在類內部方便的使用泛型參數。泛型方法是更精細的利用參數類型,將泛型參數設定在每一個方法上。

比較下面兩個接口,體會其中不一樣:

public interface Comparable<T> {
    public int compareTo(T o);
}

public interface Comparable2 {
    public <T> int compareTo2(T o);
}

public class Apple implements Comparable<Apple>, Comparable2{
    @Override
    public int compareTo(Apple o) {
        return 0;
    }

    @Override
    public <T> int compareTo2(T o) {
        //T 能夠爲任何類,因此Apple能夠和任何類比較
        return 0;
    }
}

遞歸類型限制

有類:

class Apple implements Comparable<Apple>{

}

class RedApple extends Apple{

}

有方法:

public static  <T extends Comparable<T>> T get(T t){
    return t;
}

該方法就採用了遞歸的類型限制,由於泛型T被限制爲 Comparable<T>的子類,而Comparable<T>中又包含了T,這造成一種遞歸的類型限制。Apple類能夠調用該函數,RedApple則會出現錯誤,以下所示。

RedApple ra=new RedApple();
Apple a= get(ra); //正確   
RedApple b=get(ra); //錯誤

緣由是在調用泛型函數時,會自動進行類型推斷,第一個get函數根據左邊參數,推斷T爲Apple,符合條件。在第二個get公式中,推斷T爲RedApple,不符合get函數的泛型限制條件。

一個比較複雜的泛型函數

public static <T extends Comparable<? super T>> T max(List<? extends T> list)

其中<T extends Comparable<? super T>>描述了T實現了Comparable接口或者其基類實現了該接口,經過繼承得到Comparable的狀況比較常見,這增長了該函數的通用性。參數List<? extends T>表示List只要存的是T的子類就能夠,這是顯然合理的,一樣加強了該函數的通用性。

異構容器

一個容器如Set只有1個類型參數,Map只有2個類型參數,可是有時候須要多個類型參數。下面是一個設計巧妙、能夠容納多個類型參數的類。

public static void main(String[] args){
    Favorites f =new Favorites();
    f.putFavorite(String.class, "Java");
    f.putFavorite(Integer.class, 1111);
    f.putFavorite(Class.class, Favorites.class);
    int fi=f.getFavorite(Integer.class);
}

public class Favorites{
    private Map<Class<?>, Object> favorites=new HashMap<Class<?>, Object>();

    public <T> void putFavorite(Class<T> type, T instance){
        if(type==null)
            throw new NullPointerException("Type is null");
        favorites.put(type, instance);
    }

    public <T> T getFavorite(Class<T> type){
        return type.cast(favorites.get(type));
    }
}

String.class爲Class<String>的實例,Integer.class爲Class<Integer>的實例,這二者顯然不是同一個類,可是卻能夠放在同一個Map中。Map中採用了通配符?,按理Map沒法再加入任何元素,可是該通配符並非直接表示Map的類型參數,而是Class<?>。所以Map的鍵值能夠是Class<String>、Class<Integer>等不一樣的類型,所以成爲一個異構容器。

其中Map實例favorites並無限制value必定是key描述的類的實例,而方法putFavorite經過類型參數T,巧妙的限制了二者的關係。

枚舉和註解

枚舉類型舉例

枚舉類型更像是一個不能new的類,只能在定義時就實例化好須要的固定數目的實例。以下所示:

public enum Planet {
    VENUS(2),
    EARTH(3),
    MARS(5);

    int data;

    Planet(int i){
        this.data=i;
    }

    public int getData(){
        return data;
    }
}

其構造函數默認是private,而且沒法修改。在枚舉中還能夠定義抽象方法,以下:

public enum Operation{
    PLUS { double apply(double x, double y){return x+y;}},
    MINUS { double apply(double x, double y){return x-y}};

    abstract double apply(double x, double y);
}

自定義註解的例子

定義一個用來測試方法是否能拋出目標異常的註解。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest{
    Class<? extends Exception>[] value();
}

元註解指明瞭該註解在運行時保留,而且只適用於註解方法。

使用以下:

@ExpectionTest({IndexOutOfBoundsException.class, NullPointerException.class})
public static void doublyBad(){
    List<String> list=new ArrayList<String>();
    //該方法會拋出IndexOutOfBoundsException
    list.addAll(5,null);
}

測試過程實現以下:

public static void main(String[] args) throws Exception{
    int tests=0;
    int passed=0;
    Class testClass=Class.forName(args[0]);
    for(Method m : testClass.getDeclaredMethods()){
        if(m.isAnnotationPresent(ExceptionTest.class)){
            tests++;
            try{
                m.invoke(null);
                System.out.printf("Test failed: no exceptions");
            }catch( Throwable wrappedExc){
                Throwable exc = wrappedExc.getCause();
                Class<? extends Exception>[] excTypes=m.getAnnotation(ExceptionText.class).value();
                int oldPaassed=passed;
                for(Class<? extends Exception> excType:excTypes){
                    if(excType.isInstance(exc)){
                        passed++;
                        break;
                    }
                }

                if(passed==oldPassed)
                    System.out.printf("Test failed");
            }
        }
    }
}

方法

必要時進行保護性拷貝

  1. 構造函數在接受外來參數時,必要時須要拷貝參數對象,而不是直接將參數對象賦值給本身的屬性。由於直接採用外部傳入的對象,外部能夠任意的修改這些對象,從而致使你本身設計的類內部屬性變化。若是不想讓使用你設計的類的客戶有修改其內部屬性的權利,除了設置爲private外,還應該注意採用拷貝的方式使用外部傳入的數據。選擇copy而不是clone,是由於傳入對象多是客戶定製過的參數子類,該子類仍然可能將其暴露在外面。
  2. 須要保護內部屬性不被修改,除了關注構造函數的參數,還須要關注get相似的方法。這些返回內部屬性的方法,應該返回拷貝過的屬性對象。

慎用重載

重載方法是靜態的,在編譯時就已經選擇好,根據參數的表面類型,如Collection<String> c=new ArrayList<String>(),有兩個重載函數,getMax(Collection<?> a)getMax(ArrayList<?> a),在調用getMax(c)時,會選擇getMax(Collection<?> a)方法,該選擇在編譯時就決定好了。當重載方法有多個參數時,狀況會變得更復雜,選擇結果可能出人意料。

而方法的重寫選擇時動態的,在運行時根據調用者的實際類型決定哪一個方法被調用。

慎用可變參數

可變參數可讓用戶靈活的填入不一樣數量的參數,可是該方法本質上是將參數組織成數組,因此每次調用這些方法時都會涉及數組的建立和銷燬,開銷較大。

併發

同步的意義

  1. 保證數據從一個一致狀態轉一到另外一個一致狀態,任什麼時候候讀取該數據都是一致的。
  2. 保證對數據的修改,其它線程當即可見。

讀寫變量是原子性的

除了double和long之外,讀寫變量是原子性的。可是Java沒法保證一個線程的修改對另外一個線程是可見的。

在同步模塊中當心調用其它方法

若是一個同步方法在其中調用了一個不禁本身控制的方法,好比客戶傳入的方法,客戶可能在實現方法時申請同步鎖,或者啓動新線程申請鎖,這可能會致使死鎖。

併發工具優先於wait和notify

java.util.concurrent包提供了執行框架、併發集合和同步器三種工具,應該儘可能使用這些工具來實現併發功能,而不是使用wait、notify。

若是使用wait,notify則應該採用以下模式:

public void waitA(){
        synchronized(a){    //得到鎖
            while(a>10)     //放在while循環中保證知足條件
                try {
                    a.wait(); //釋放鎖、若是被喚醒則須要從新得到鎖
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        }
    }

    //其它線程調用該方法喚醒等待線程
    public void notifyA(){
        a.notifyAll();
    }

notifyAll方法相較於notify方法更安全,它保證喚醒了全部等待a對象的線程,被喚醒不表明會被當即執行,由於還須要得到鎖。

不要對線程調度器有任何指望

Tread yield(讓步)即當前線程將資源歸還給調度器,可是並不能保證當前線程下面必定不會被選中。線程的優先級設置也是不能保證按你預期的進行調度。

相關文章
相關標籤/搜索