天天看点

《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()

第八章 序列化