Effective Java 筆記

NO IMAGE
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

對所有物件都通用的方法

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。否則,會失去對該域的控制權。

公有類的暴露公有域

公用類如果暴露了自己的域,則會導致客戶端濫用該域,並且該公用類在以後的升級中無法靈活的改變屬性的表達方式。

不可變物件的設計

  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這種原生型別逃避了型別檢查。

List中add任何物件都對。List<?>的變數可以引用任何引數化(非引數也可以)的List,但是無法通過該變數新增非null元素。

假設Men extends Person, Boy extends Men:

  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(讓步)即當前執行緒將資源歸還給排程器,但是並不能保證當前執行緒下面一定不會被選中。執行緒的優先順序設定也是不能保證按你預期的進行排程。

相關文章

開發語言 最新文章