天天看點

《Effective in java》 讀書筆記

第一章  建立和銷毀對象

    第一條  考慮使用靜态工廠方法(static factory method)代替公有的構造方法

    客戶需要得到一個類的執行個體方法有二。提供一個公有的構造函數,或者是使用靜态工廠方法。靜态工廠方法是一個靜态的方法它傳回的是一個類的執行個體。

 好處:1.容易閱讀,靜态工廠方法有名字,eg,GeometryFactory.getInstance();

           2.不要求重新建立一個新對象。保證了單态(singleton),使非可變類可以保證不會有兩個相同的執行個體存在,即當且僅當a==b的時候才有a.equals(b)為true。eg:

public class Foo{
       private Foo instance = null;
       private Foo(){}//私有的構造方法
       public static Synchronizeed  Foo getInstance(){
             if(instance == null){
                instance  = new Foo();
             }
             return instance;
       }
  }      

 //調用: Foo foo = Foo.getInstance();

           3.可以傳回原傳回類型的子類型的對象。比如一個API可以傳回一個對象,同時又希望該對象的類成為公有的。這種方法把具體的實作類隐藏起來。可以得到一個簡潔的API,該技術非常适合于基于接口的架構,eg,不可修飾和同步的集合都是通過java.util.Collections靜态工廠方法而被導出的。

eg:用戶端:

List<String> list = java.util.Collections. EMPTY_LIST ;      
public static final List EMPTY_LIST = new EmptyList();
   private static class EmptyList
        extends AbstractList<Object>
        implements RandomAccess, Serializable {
        // use serialVersionUID from JDK 1.2.2 for interoperability
        private static final long serialVersionUID = 8842843931221139166L;

        public int size() {return 0;}

        public boolean contains(Object obj) {return false;}

        public Object get(int index) {
            throw new IndexOutOfBoundsException("Index: "+index);
        }

        // Preserves singleton property
        private Object readResolve() {
            return EMPTY_LIST ;
        }
    }static final List EMPTY_LIST = new EmptyList();
   private static class EmptyList
        extends AbstractList<Object>
        implements RandomAccess, Serializable {
        // use serialVersionUID from JDK 1.2.2 for interoperability
        private static final long serialVersionUID = 8842843931221139166L;

        public int size() {return 0;}

        public boolean contains(Object obj) {return false;}

        public Object get(int index) {
            throw new IndexOutOfBoundsException("Index: "+index);
        }

        // Preserves singleton property
        private Object readResolve() {
            return EMPTY_LIST ;
        }
    }      

    缺點:1.如果不含有公有的或者受保護的公有構造函數就不能被子類化。既不能被繼承。因為子類的構造函數需要調用父類的構造函數,但是父類的構造函數是私有的不能被外部方法調用,隻能被本類方法調用。推薦使用複合結構。

   第二條 使用私有構造函數強化Singleton屬性

         Singleton 應該是這樣的類,隻能被執行個體化一次。通常用來代表那些具有唯一性的元件。

   第三條 通過私有構造函數強化不可執行個體的能力

       試圖将一個類做成抽象類來強制該類不可被執行個體化,這是行不通的。

 在工具類中(包含靜态方法和靜态域的類)往往不希望被執行個體化,并且其子類也不可執行個體化,一個好的辦法是顯式的構造函數設定為私有的,這樣該類的外部這個構造函數是不可被通路的,如果該類不被類本身從内部調用,則該累是永遠不會被執行個體化的。

eg:java.util.Collections

public class Collections {
    // Suppresses default constructor, ensuring non-instantiability.
    private Collections () {
}      

    第四 避免建立重複對象

      通過靜态工廠方法和構造函數私有化都可以做到避免建立重複對象。

     1)重用非可變對象。在使用非可變類(如:String) 時,應該做到重複使用同一對象,String str="Kill";而避免使用String str=new String("Kill");

     2)對可變類的使用也可以重複使用其對象

public Date isbirthday(){
          Calendar c=new Calendar();
          Date beginDate=c.....
          Date endDate=c....
         return beginDate.compareTo(endDate);
} 
改進 使用靜态化的初始化器
      public static final Date BEGINDATE;
      public static final Date ENDDATE;
      static {
            Calendar c=new Calendar();
            beginDate=c.....
           endDate=c....
}
public Date isbirthday(){
     return beginDate.compareTo(endDate);
}      

這樣 日期對象c,beginDate endDate在任何時刻隻是執行個體化了一次,而不是每次在調用方法isbirthday()時被執行個體化一次。

第五點 消除過期對象引用

 記憶體洩漏問題:如果一個對象的引用被無意識的保留起來,那麼垃圾回收機制是不會去處理這個對象,而且也不會去處理被這個對象引用的其它對象。

比如堆棧的彈出棧元素方法。

public synchronized E pop() {
       E      obj;
        int    len = size();

       obj = peek();
       removeElementAt (len - 1);

        return obj;
    }

  public synchronized void removeElementAt (int index) {
       modCount++;
        if (index >= elementCount) {
           throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                              elementCount);
       }
        else if (index < 0) {
           throw new ArrayIndexOutOfBoundsException(index);
       }
        int j = elementCount - index - 1;
        if (j > 0) {
           System.arraycopy(elementData, index + 1, elementData, index, j);
       }
       elementCount--;
    }      

如果棧是先增長後收縮,那麼從堆棧中彈出的對象不會被當作垃圾回收。

pop()方法的修訂版本

public synchronized void removeElementAt(int index) {
       modCount++;
        if (index >= elementCount) {
           throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                              elementCount);
       }
        else if (index < 0) {
           throw new ArrayIndexOutOfBoundsException(index);
       }
        int j = elementCount - index - 1;
        if (j > 0) {
           System.arraycopy(elementData, index + 1, elementData, index, j);
       }
       elementCount--;
       elementData[elementCount] = null; /* to let gc do its work */
    }      

方法:重新使用這個已經指向一個對象的引用,或結束其生命周期。

一般而言當一個類自己管理起記憶體,很容易引起記憶體洩漏。比如stack 。

避免使用終結函數finalize(),首先說一下finalize()和system.gc()的差別,前者是回收棧中的引用,而後者是回收堆中的對象。JVM 調用垃圾回收器(system.gc())釋放被heap中沒用的對象占有的資源。如果當垃圾回收器确定不存在對該對象的更多引用時,由對象的垃圾回收器調用此方法。由于終結函數的執行是在對象在釋放之前被調用,也就是說在這個對象被認為沒用到釋放是有時間段的。有可能還沒來得及執行終結函數,程式就已經當掉了。是以時間關鍵的任務不應該由終結函數來完成。

那麼一個類封裝了的資源如線程,檔案确實要回收,我們應該怎麼辦才能不需要編寫終結函數呢。提供顯式的終結方法,如流中,檔案中的close(),線程中的cancel()。顯式的終止方法常與try-finally結構結合起來使用,以確定其及時終止。

第二章 對所有對象都通用的方法

equals(),hashCode(),toString(),clone(),finalize()

(1) equals方法一般用于“值類”的情形,比如Integer,Date目的是為了比較兩個指向值對象的引用的時候,希望

它們的邏輯是否相等而不是它們是否指向同一個對象。(和equals本意不符啊。)

    約定

    a 自反性 對任意的對象必須和它自身相等。對值引用x x.equals(x) 一定傳回true

    b 對稱性 對任意的值引用x,y,如果x.equals(y) 一定有y.equals(x)

    典型的例子

public Point{
     private int x;
     private int y;
     public boolean equals(Object o){
      if(o==this){
       return true;
      }
      if(!(o instanceof Point)){
       return false;
      }
      Point p=(Point)o;
      return p.x==x&&p.y==y;
     })
    }
    
    public ColorPoint extends Point{
     private Color color;
     public ColorPoint(int x,int y,Color color){
      super(x,y);
      this.color=color;
     }
     
     public boolean equals(Object o){
      if(this==o)return true;
      if(!(o instanceof ColorPoint)) return false;
      ColorPoint cp=(ColorPoint)o
      return super.equals(o)&&cp.color==color;
     }
    }      

         存在問題 

         如果 Point p=new Point(1,2);

         ColorPoint cp1=new ColorPoint(1,2,Color.RED);

         p.equals(cp1) //雖然忽視了Color屬性但還是傳回true。調用的是Point 的equals方法。

         cp1.equals(p)//實參類型 p instanceof ColorPoint不一緻傳回false;

改進的方式是加入混合比較。  

public boolean equals(Object o) {
    if (this == o)
      return true;
    if (!(o instanceof Point))
      return false;
    //隻是坐标點比較,忽略顔色
    if (!(o instanceof ColorPoint))
      return o.equals(this);
    //帶顔色比較
    ColorPoint cp = (ColorPoint) o;
    return super.equals(o) && cp.color == color;
  }      

     Point p=new Point(1,2);

     ColorPoint cp1=new ColorPoint(1,2,Color.RED);

     p.equals(cp1) 傳回true;

     cp1.equals(p) 傳回true;

     存在問題

      cp1.equals(p) 傳回true

    p.equals(cp2) 傳回true

    但是 cp1.equals(cp2) 傳回false; 違反了傳遞性。

    c 傳遞性 對任意的值引用x,y,z,如果x.equals(y),y.equals(z) 一定有x.equals(z)  

    結論:要想在擴充一個可執行個體化的類的同時,即要保證增加新的特性,又要保證equals約定,

    建議複合優于繼承原則。重新設計ColorPoint類

    若類和類是 a kind of 關系則用繼承

    若類和類是 a part of 關系則用組合(複合)

public class ColorPoint{
     private Point point;
     private Color color;
     boolean equals(Object o){
      ......
      ColorPoint cp=(ColorPoint)o;
      return cp.point.equals(point)&&cp.color.equals(color);
     }
    }      

 一個完整的equals()方法

public class A{
  private property1;
  private property2;
  public boolean equals(Object o){
    if(o==this) 
     return true;
    if(!(o instanceof A))
     return false;
    A a=new A();
    return a.property1==property&&a.property2==property2;
  }
 }      

(2) hashCode()

相等的對象必須要有相等的散列碼,如果違反這個約定可能導緻這個類無法與某些散列值得集合結合在一起使用

是以在改寫了equals方法的同時一定要重寫hashCode方法以保證一緻性。

重寫hashCode()規則:

1.把某個非零常數值(如17)儲存在一個叫result的int類型的變量中;

2.對于對象中每個關鍵字域f(指equals方法中考慮的每一個域),完成以下步驟:

  為該域計算int類型的散列碼c:

  1).如果該域是bloolean類型,則計算(f?0:1)

  2).如果該域是byte,char,short或int類型,則計算(int)f

  3).如果該域是long類型,則計算(int)(f^(f>>>32))

  4).如果該域是float類型,則計算Float.floatToIntBits(f)

  5).如果該域是double類型,則計算Double.doubleToLongBits(f)得一long類型值,然後按前述計算此long類型的散列值

  6).如果該域是一個對象引用,則利用此對象的hashCode,如果域的值為null,則傳回0

  7).如果該域是一個數組,則對每一個數組元素當作單獨的域來處理,然後安下一步的方案來進行合成

3.利用下面的公式将散列碼c 組合到result中。

result=37*result+c;

比如一個Employee 對象的兩個域,id,name在equals中都可慮了

public int hashCode(){

   int result = 17;

   result = 37*result + id;

   result = 37*result + name.hashCode(); 

}

(3) toString() 重寫toString以包含有價值的資訊

(4) Comparable 接口

第三章 類和接口

資訊隐藏(封裝)可以有效的解除一個系統中各個子產品之間的耦合關系。

1)使得各個子產品可以獨立的開發、測試、優化、修改、測試

2)子產品可以并行的開發,加快開發速度

(1)使類和成員的可通路能力盡量的小。

(2)支援非可變性

2.1 非可變類

一個非可變類是一個簡單的類,它的執行個體不能被修改。每個執行個體中包含的所有資訊都必須在該執行個體被建立的時候提供出來,并且在該類的整個生命周期内(lifetime)内固定不變,如:String BigDecimal.非可變類運算傳回的是一個新的執行個體,而不是對原來執行個體的修改,這種做法稱為函數的做法,因為這些方法傳回了一個函數的結果。其對應的是過程的做法,方法内部有一個過程的作用在它們的操作數上使得它的狀态發生了改變。要成為非可變類的原則是:a,不提供修改該對象的方法。

b,保證沒有可被子類修改的方法。

C,所有的域都是private final類型的。

D,所有的域都是私有的。防止客戶直接修改域中的資訊。

非可變對象本質上是線程安全的,他們不要求同步。

非可變對象可以被自由共享。

壞處:對每一個不同的值,需要一個單獨的對象。

(2)複合優于繼承

(3)接口優于抽象

(4)常量接口,在接口的設計中有一種接口,隻有靜态常量我們稱為常量接口。

     缺點:如果實作這個常量接口的實作類改變了,而不需要這個接口裡的值。但這為了維護一緻性還得修改這個接口。

     如果要導出常量方法有三

     a,把常量添加到和它緊密相關的類或接口中。

     b,定義一個安全枚舉類

     c,定義一個工具類

public class Physical{
          private Physical(){}
          private static final double MAX_VALUE=10.0;
          private static final double MIN_VALUE=12.0;
         }      

第四章 方法

(1) 檢查參數的有效性

    方法的參數有某些限制,如,索引必須是非負數,對象引用必須不能為null。對公有方法通過@throws 記錄被抛出的異常

     eg:

@param
    @return
    @throws IllegalArgumentException if x<0
    public BigDecimal(int x){
     if(x<0){
      throw IllegalArgumentException("參數錯誤");
     }
    }      

但是,并不是所有的都在計算之前檢查參數,比如有些是在計算過程中被隐含的完成。

    比如:Collections.sort(list).清單中的所有對象必須是可以互相比較的,在排序清單的過程中

    每個清單中的每個對象與其它對象進行比較,如果對象是不可比較的,則抛出ClassCastExcrption.是以提前檢查清單中的元素是否可比較是沒有什麼意義的。

    有些方法的參數,方法本身沒有使用它。而是存儲起來以後使用,這些參數檢測尤為重要。比如構造方法。

    原則:如果一個方法對接受所有的參數值都能順利的完成工作,那麼對參數的限制越少越好。

(2) 需要使用保護性拷貝

在對象本身不提供幫助的情況下,另一個類要想修改這個對象的内部狀态是不可能的。但又些情況下提供這種幫助又非常容易的。

eg:

import java.util.Date;
import java.lang.IllegalArgumentException;
public class Period{
 private final Date start;
 private final Date end;

//(1)
 public Period(Date start,Date end){
  if(start.compareTo(end)>0){
    throw new IllegalArgumentException("參數錯誤");
    }
   this.start=start;
   this.end=end;
  }

  //(2)對構造方法的可變參數進行保護性克隆
 public Period(Date start,Date end){
    this.start=new Date(start.getTime());//是用新對象
    this.end=new Date(end.getTime());
   if(start.compareTo(end)>0){
    throw new IllegalArgumentException("參數錯誤");
          
    }
    
  public static void main(String []args){
    Date start=new Date();
    Date end=new Date();
    Period p=new Period(start,end);
    System.out.println(p.start+""+p.end);
    end.setYear(78);//由于Date對象是可變的,這是最關鍵的,是以導緻Period對象改變了
    System.out.println(p.start+""+p.end);
   }
}      

使用第一個構造方法如果調用end.setYear(78).則,對象p的end域被改變了,并且限制條件也違反了。

使用第二個構造方法,對構造方法的可變參數進行保護性克隆。p對象的end域指向的是拷貝的日期對象,而不是原來的日期對象。

(2)、方法設計的一些原則

 a,避免長的參數清單,尤其是參數相同的參數清單。

 b,對參數類型使用接口,而不是接口的實作類。

 c,謹慎使用重載。

 d,傳回0程度的數組而不是null.

import java.util.Set;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Collection;

public class CollectionsClassifier{
 public static String classify(Set s){
  return "Set"; 
 } 
 public static String classify(List List){
  return "List";
 }
 public static String classify(Collection c){
  return "collections"; 
 }
 public static void  main(String []args){
  Collection colles[]=new Collection[]{new HashSet(),new ArrayList(),new HashMap().values()};
  for(int i=0;i<colles.length;i++){
   System.out.println(classify(colles[i])); 
  } 
 }
}      

                  collections

                  collections

程式的意圖:期望編譯器根據參數的運作時類型自動将調用分發給适當的重載方法。顯然方法重載是不能滿足客戶

要求。

改寫classify()方法

public static String classify(Collection c){
 if(c instanceof Set) return "Set";
 if(c instanceof List) return "List";
 if(c instanceof Collection) return "Collection";
}      

注意:對for循環的三次疊代,參數的編譯類型都是相同的:Collection。是以每次疊代調用都是classify(Collection)方法

而每次運作時的類型是不一樣的。

補充:重載(overloaded method) 選擇的是靜态的。選擇的依據是參數類型

         重寫(oveeridden method) 選擇的依據是被調用方法所在對象的運作時的類型。

class A{
  String getName(){return "A";}
 } 
 class B extends A{
  String getName(){return "B";}
 }
 class C extends A{
  String getName(){return "C";}
 }
 public class Overriden{
 public static void main(String []args){
  A ArrayTest[]=new A[]{new A(),new B(),new C()};
  for(int i=0;i<ArrayTest.length;i++){
   System.out.println(ArrayTest[i].getName());
  } 
 }
}      

運作結果:A B C。雖然編譯的類型都是A但調用方法是改對象的類型是不一樣的。

原則:對于多個具有相同參數數目的方法來說,盡量避免重載方法。

第五章 通用設計方法

 (1) 将局部變量的作用域最小化

        a.在第一次使用這個變量的地方聲明它。以防止帶來可讀性差和作用域的問題。

        b.幾乎每個作用域的聲明都應該包含一個初始化表達式。

在循環變中,經常要使用最小化變量作用域這一規則。如果在循環終止以後循環變量的内容不再被使用的話,則for循環優于while。

for(Iterator iterator = c.iterator;iterator.hasNext();) {
     doSomething(iterator.next());
}

Iterator iterator = c.iterator;
while(iterator.hasNext()) {
      doSomething(iterator.next());
}

Iterator iterator1 = c.iterator;
while(iterator.hasNext()) {       //BUG。 一不小心,用了上一個變量iterator。
      doSomething(iterator1.next());   
}      

 (2) 如果要得到精确結果,最好是用BigDecimal 而不使用fload或double

 (3) 對數量大的字元串連接配接使用StringBuffer而不是String前者速度快。

第六章 異常

 (1) 異常隻應用于不正常的條件,它們永遠不能應用于正常的控制流程。如果一個類有一個“狀态相關的方法”

 即隻有在特定的不可預知的條件下才可以使用的方法,那麼這個類也往往有一個單獨的“狀态測試方法”。如Iterator

 有一個狀态相關方法next(),那麼對應就有一個狀态測試方法 hasNext().

 (2) 異常分類 

 非檢查型異常  (run-time Exception,Error) NullPointException IndexOutOfBoundsException ArrayOutOfBoundsException IllegalArgumentExcepton ClassCastException

 檢查型異常 IOException FileNotFoundException ClassNotFoundException NoSuchMethodException SQLException 必需要有try-catch捕獲或者傳播到外面(抛出)

使用原則:如果期望使用者能夠恢複,則使用檢查性異常,通過catch捕獲後處理(如IO流沒關閉,找不到檔案,則可以在捕獲後關閉流和建立檔案).

如果一個程式捕獲到一個非檢查型異常或者一個錯誤則往往是不可恢複的情況,繼續執行下去有害無益。

保持異常的原子性:當一個對象抛出一個異常的時候,我們總是希望這個對象仍然能夠保持在一種良好的可用狀态之中。

 eg:

public Object pop(){
      Object result=element[--size];
      element[size]=null;//垃圾回收;
      return result;
     }      

 當初始化的檢查被除掉的話。當這個方法企圖從一個空堆棧中彈出時,它仍然會抛出一個異常,然而會是size保持在(-1)中,進而使将來對該對象的任何調用都會失敗。

 解決方法:對計算處理順序的調整,使得任何可能失敗的計算部分都發生在對象狀态被修改之前。

public Object pop(){
      if(size==0){
       throw new EmptyStackException();
      }
      Object result=element[--size];
      element[size]=null;//垃圾回收;
      return result;
     }      

 第七章 線程

 在JAVA中建立線程并不困難,所需要的三件事:執行的代碼、代碼所操作的資料和執行代碼的虛拟CPU。

 (1)兩個簡單執行個體

eg:Thread
  public class ThreadDemo extends Thread{
   private String str="";              //1.代碼操作的資料
   public ThreadDemo(String str){
    this.str=str;
   }
   public void run(){
    System.out.println("str="str);     //2.執行代碼
   }
  public static void main(String []args){
    ThreadDemo td=new ThreadDemo("線程1");//3.cpu
   td.start();
  }
 }
  eg:Runnable
  public class RunnableDemo implements Runnable{
  private String str="";
  public RunnableDemo(String str){
   this.str=str;
  }
  public void run(){
   System.out.println("pp"+str);
  }
  public static void main(String []args){
   RunnableDemo rd=new RunnableDemo("hello");
   Thread t=new Thread(rd);
   t.start();
  }
 }      

  請注意,當使用 runnable 接口時,您不能直接建立所需類的對象并運作它;必須從 Thread 類的一個執行個體内部運作它。

  (2)synchronized 關鍵字可以保證在同一時刻,隻有一個線程在執行一條語句,或者一段代碼.

   Object.wait()

   Object.notify()

   Object.notifyAll()

   Thread.yield()

   Thread.sleep()

第八章 序列化