`
jeafyezheng
  • 浏览: 99962 次
  • 性别: Icon_minigender_1
  • 来自: 北京
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论

读《Effective java 中文版》

阅读更多

读《Effective java 中文版》(1<o:p></o:p>

  Joshua Bloch著,潘爱民译,机械工业出版社<o:p></o:p>

  我是在过年回家的往返火车上,翻完了这本书。为了帮助java程序员“用习惯和高效的方式”使用Java语言,作者利用9章的篇幅,提出了57条规则。有些规则你可能在别的地方或书中也曾见到,但对这些规则的解释以及谈及的一些java背后的技术内容,更让人觉得有所收获。此书确实不愧为2002Jolt大奖(http://www.sdmagazine.com/),如果你曾用Java或相关技术开发过一段时间的软件,我建议你看一下这本书,一方面加深一下对Java语言的理解,一方面检查一下自己的实力。<o:p></o:p>

  回来以后,从网上找到了这书的目录,列在后面供参考。并将我重读此书的一些笔记,陆续放上来。<o:p></o:p>

译者序<o:p></o:p>

<o:p></o:p>

前言<o:p></o:p>

1 引言 1<o:p></o:p>

<o:p> </o:p>

2 创建和销毁对象 4<o:p></o:p>

1条:考虑用静态工厂方法代替构造函数 4<o:p></o:p>

2条:使用私有构造函数强化singleton属性 8<o:p></o:p>

3条:通过私有构造函数强化不可实例化的能力 10<o:p></o:p>

4条:避免创建重复的对象 11<o:p></o:p>

5条:消除过期的对象引用 14<o:p></o:p>

6条:避免使用终结函数 17<o:p></o:p>

<o:p> </o:p>

3 对于所有对象都通用的方法 21<o:p></o:p>

7条:在改写equals的时候请遵守通用约定 21<o:p></o:p>

8条:改写equals时总是要改写hashCode 31<o:p></o:p>

9条:总是要改写toString 36<o:p></o:p>

10条:谨慎地改写clone 39<o:p></o:p>

11条:考虑实现Comparable接口 46<o:p></o:p>

<o:p> </o:p>

4 类和接口 51<o:p></o:p>

12条:使类和成员的可访问能力最小化 51<o:p></o:p>

13条:支持非可变性 55<o:p></o:p>

14条:复合优先于继承 62<o:p></o:p>

15条:要么专门为继承而设计,并给出文档说明,要么禁止继承 67<o:p></o:p>

16条:接口优于抽象类 72<o:p></o:p>

17条:接口只是被用于定义类型 76<o:p></o:p>

18条:优先考虑静态成员类 78<o:p></o:p>

<o:p> </o:p>

5 C语言结构的替代 82<o:p></o:p>

19条:用类代替结构 82<o:p></o:p>

20条:用类层次来代替联合 84<o:p></o:p>

21条:用类来代替enum结构 88<o:p></o:p>

22条:用类和接口来代替函数指针 97<o:p></o:p>

<o:p> </o:p>

6 方法 100<o:p></o:p>

23条:检查参数的有效性 100<o:p></o:p>

24条:需要时使用保护性拷贝 103<o:p></o:p>

25条:谨慎设计方法的原型 107<o:p></o:p>

26条:谨慎地使用重载 109<o:p></o:p>

27条:返回零长度的数组而不是null 114<o:p></o:p>

28条:为所有导出的API元素编写文档注释 116<o:p></o:p>

<o:p> </o:p>

7 通用程序设计 120<o:p></o:p>

29条:将局部变量的作用域最小化 120<o:p></o:p>

30条:了解和使用库 123<o:p></o:p>

31条:如果要求精确的答案,请避免使用floatdouble 127<o:p></o:p>

32条:如果其他类型更适合,则尽量避免使用字符串 129<o:p></o:p>

33条:了解字符串连接的性能 131<o:p></o:p>

34条:通过接口引用对象 132<o:p></o:p>

35条:接口优先于映像机制 134<o:p></o:p>

36条:谨慎地使用本地方法 137<o:p></o:p>

37条:谨慎地进行优化 138<o:p></o:p>

38条:遵守普遍接受的命名惯例 141<o:p></o:p>

<o:p> </o:p>

8 异常 144<o:p></o:p>

39条:只针对不正常的条件才使用异常 144<o:p></o:p>

40条:对于可恢复的条件使用被检查的异常,对于程序错误使用运行时异常 147<o:p></o:p>

41条:避免不必要地使用被检查的异常 149<o:p></o:p>

42条:尽量使用标准的异常 151<o:p></o:p>

43条:抛出的异常要适合于相应的抽象 153<o:p></o:p>

44条:每个方法抛出的异常都要有文档 155<o:p></o:p>

45条:在细节消息中包含失败-捕获信息 157<o:p></o:p>

46条:努力使失败保持原子性 159<o:p></o:p>

47条:不要忽略异常 161<o:p></o:p>

<o:p> </o:p>

9 线程 162<o:p></o:p>

48条:对共享可变数据的同步访问 162<o:p></o:p>

49条:避免过多的同步 168<o:p></o:p>

50条:永远不要在循环的外面调用wait 173<o:p></o:p>

51条:不要依赖于线程调度器 175<o:p></o:p>

52条:线程安全性的文档化 178<o:p></o:p>

53条:避免使用线程组 181<o:p></o:p>

<o:p> </o:p>

10 序列化 182<o:p></o:p>

54条:谨慎地实现Serializable 182<o:p></o:p>

55条:考虑使用自定义的序列化形式 187<o:p></o:p>

56条:保护性地编写readObject方法 193<o:p></o:p>

57条:必要时提供一个readResolve方法 199<o:p></o:p>

<o:p> </o:p>

中英文术语对照 202<o:p></o:p>

参考文献 207<o:p></o:p>

模式和习惯用法索引 212<o:p></o:p>

索引 214<o:p></o:p>

<o:p> </o:p>

<o:p> </o:p>

<o:p> </o:p>

读《Effective java 中文版》(2<o:p></o:p>

  第1条:考虑用静态工厂方法代替构造函数<o:p></o:p>

静态工厂方法的一个好处是,与构造函数不同,静态工厂方法具有名字。
第二个好处是,与构造函数不同,它们每次被调用的时候,不要求非得创建一个新的对象。
第三个好处是,与构造函数不同,它们可以返回一个原返回类型的子类型的对象。
  静态工厂方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以并不存在,从而可以成为服务提供者框架(service provider framework,指这样一个系统:提供者为框架的用户提供了多个API实现,框架必须提供一种机制来注册这些实现,以便用户能够使用它们,框架的客户直接使用API而无需关心使用的是哪个实现)的基础。
例子:JCE
//Provider framework sketch
public abstract class Foo{
//map string key to corresponding class object
private static Map implementations=null;

//initializes implementations map the the first time it's called
private static syncronized void initMapIfNecessary(){
if (implementations==null){
implementations=new HashMap();
//load implementation class name and keys from properties file,
//translate names into class objects using Class.forName and store mappings.
....
}
}
public static Foo getInstance(String key){
initMapIfNecessary();
Class c=(CLass)implementations.get(key);
if(c==null){
return new DefaultFoo();
}
try{
return (Foo)c.newInstance();
}catch(Exception e){
return new DefaultFoo();
}
}
}
静态工厂方法的主要缺点是,类如果不含公有的或者受保护的构造函数,就不能被子类化。
第二个缺点是,它们与其它的静态方法没有任何区别。
  对它们的命名习惯是: <o:p></o:p>

<!---->1.      <!---->valueOf
该方法返回的实例与它的参数具有相同的值,常用于非常有效的类型转换操作符 <o:p></o:p>

<!---->2.      <!---->getInstance
返回的实例是由方法的参数来描述的,但不能够说与参数具有相同的值。常用于提供者框架中。<o:p></o:p>

<o:p> </o:p>

读《Effective java 中文版》(3<o:p></o:p>

  第2条:使用私有构造函数强化Singleton属性
  关于Singleton在《Java Design Patterns A Tutorial》一书(汉译为JAVA设计模式)的第6章也有论述。<o:p></o:p>

  Singleton(只能被实例化一次的类)的实现,要私有的构造函数与公有的静态成员结合起来,根据静态成员的不同,分为两种方法:<o:p></o:p>

<!---->1.      <!---->公有静态成员是一个final
例如://singleton with final field
public class Elvis{
public static final Elvis INSTANCE = new Elvis()[
private Elvis(){
...
}
}<o:p></o:p>

<!---->2.      <!---->公有静态成员是一个工厂方法
例如://singleton with static factory method
public class Elvis{
private static final Elvis INSTANCE = new Elvis()[
private Elvis(){
...
}
public static Elvis getInstance(){
return INSTANCE;
}
}<o:p></o:p>


  前者的好处在于成员的声明即可表明类的singleton特性,且效率可能高一些。
  后者的好处在于提供了灵活性。<o:p></o:p>

<o:p> </o:p>

<o:p> </o:p>

读《Effective java 中文版》(4<o:p></o:p>

  第3条:通过私有构造函数强化不可实例化的能力<o:p></o:p>

  如果一个类缺少显式的构造函数,编译器会自动提供一个公有的、无参数的默认构造函数(default constructor)。
  我们只要让这个类包含单个显式的私有构造函数,则它就不可被实例化了,而企图通过将一个类做成抽象类来强制该类不可被实例化是行不通的。
  这种做法的一个副作用,是它使得这个类不能被子类化,因为子类将找不到一个可访问的超类的构造函数。
例如://noninstantiable utility class
public class UtilityClass{
//suppress default constructor for noninstantiability
private UtilityClass(){
//this constructor will never be invoked
}
...
}
说明:工具类(UtilityCLass)指只包含静态方法和静态域的类,它不希望被实例化因为对它进行实例化没有任何意义。<o:p></o:p>

<o:p> </o:p>

<o:p> </o:p>

读《Effective java 中文版》(5<o:p></o:p>

4条:避免创建重复的对象<o:p></o:p>

  从性能的角度考虑,要尽可能地利用已经创建的大对象(创建代价比较高的对象)。
  如果一个对象是非可变的(immutable),则它总是可以被重用。
  对于同时提供了静态工厂方法和构造函数的非可变类,通常可以用静态工厂方法而不是构造函数来避免创建重复的对象。
两个优化例子:
1
、在一个循环中,要将
String s=new String("silly");//Don't do this!
改为
String s="No Longer Silly";
2
、对于那些已知不会被修改的可变对象,也可以重用它们。
public class Person{
private final Date birthDate;
public Person(Date birthDate){
this.birthDate=birthDate;
}
//Don't Do this!
public boolean isBabyBoomer(){
Calendar gmtCal=Calendar.getinstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946,Calendar.JANUARY,1,0,0,0);
Date boomStart=gmtCal.getTime();
gmtCal.set(1965,Calendar.JANUARY,1,0,0,0);
Date boomEnd=gmtCal.getTime();
return birthDate.compareTo(boomStart)>=0 && birthDate.compareTo(boomEnd)<0;
}
}
优化为:
public class Person{
private final Date birthDate;
public Person(Date birthDate){
this.birthDate=birthDate;
}
/**
*The starting and ending dates of the baby boom
*/
private static final Date BOOM_START;
private static final Date BOOM_END;

static{
Calendar gmtCal=Calendar.getinstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946,Calendar.JANUARY,1,0,0,0);
Date boomStart=gmtCal.getTime();
gmtCal.set(1965,Calendar.JANUARY,1,0,0,0);
Date boomEnd=gmtCal.getTime();
return birthDate.compareTo(boomStart)>=0 && birthDate.compareTo(boomEnd)<0;
}
public boolean isBabyBoomer(){
return birthDate.compareTo(boomStart)>=0 && birthDate.compareTo(boomEnd)<0;
}
}
isBabyBoomer从没被调用过时,优化的方案反而不如没优化的。:)
  来看一句象费话一样的话。
  当应该重用一个已有对象的时候,请不要创建新的对象(本条目的核心),当应该创建一个新的对象的时候,请不要重用一个已有对象(第24条的核心,该条讲的是保护性拷贝的问题)。<o:p></o:p>

<o:p> </o:p>

<o:p> </o:p>

<o:p> </o:p>

读《Effective java 中文版》(6<o:p></o:p>

  第5条:消除过期的对象引用<o:p></o:p>

  下面的例子存在内容泄漏(或者说无意识的对象保持,unintentional object retention)。
//Can u spot the "memory leak"?
public class Stack{
private Ojbect[] elements;
private int size=0;
public Stack(int initialCapacity){
this.elements=new Object[initialCapacity];
}
public void push(Object e){
ensureCapacity();
elements[size++]=e;
}
public Object pop(){
if(size==0) throw new EmptyStackException();
return elements[--size];
}
/**
*Ensure space for at least one more element, roughly doubling
*the capacity each time the array needs to grow.
*/
private void ensureCapacity(){
if(elements.length==size){
Object[] oldElements=elements;
elements=new Object[2*elements.length+1];
System.arrayCopy(oldElements,0,elements,0,size);
}
}
}
  消除内容泄露,只需要改动pop()方法。
public Object pop(){
if(size==0) throw new EmptyStackException();
Object result= elements[--size];
elements[size]=null;
return result;
}
  只要一个类自己管理它的内存,程序员就要警惕内存泄露问题。一旦一个元素被释放掉,则该元素中包含的任何对象引用应该要被清空。
  内存泄露的另一个常见来源是缓存,因此这时要用一个线程定期清缓存或在加入时清最少使用的缓存对象。在1.4发行版中,可以用java.util.LinkedHashMaprevmoveEldestEntry方法来实现后一方案。
  如果一个持续运行的java应用速度越来越慢,就要考虑是否检查内存泄露问题。
  书中说检查内存泄露的软件统称heap profiler,我检索到了两个http://sourceforge.net/projects/simpleprofilerhttp://www.manageability.org/blog/stuff/open-source-profilers-for-java,以后有机会可得好好研究一番。不知读到此文的朋友,能否再推荐几个好的工具软件?<o:p></o:p>

<o:p> </o:p>

<o:p> </o:p>

<o:p> </o:p>

读《Effective java 中文版》(7<o:p></o:p>

  第6条:避免使用终结函数<o:p></o:p>

  终结函数(finalizer)通常是不可预测的,常常也是很危险的,一般情况下是不必要的。使用终结函数会导致不稳定的行为、更差的性能,以及带来移植问题。
  JLS不仅不保证终结函数被及时地执行,而且根本不保证它们会被执行。因此,时间关键(time-critical)的任务不应该由终结函数完成(如文件资源的归还),我们也不应该依赖一个终结函数来更新关键性的永久状态(如共享资源的永久锁)。另外,当终结函数的执行时抛出异常时,问题会更严重。
  如果确实有资源要回收则不想使用终结函数,办法是提供一个显式的终止方法。显式的终止方法通常与try-finally结构配合使用,以确保及时终止。
  当然,终结函数并非一无是处:第一种用途是当一个对象的所有者忘记了调用建议的显式终止方法时,终结函数可以充当安全网(safety net)”。第二种用途与对象的本地对等体(native peer)有关。本地对等体是一个本地对象,普通对象通过本地方法(native method)委托给一个本地对象。因为本地对等体不是一个普通对象,所以垃圾回收器不会知道它,当它的普通对等体被回收的时候,它不会被回收。在本地对等体不拥有关键资源的前提下,终结函数是执行这项任务的最合适的工具。
  使用终结函数时,终结函数链(finalizer chain)”并不会被自动执行,因而子类的终结函数必须手工调用超类的终结函数。如:
//manual finalizer chaining
protected void finalize() throws Trowable{
try{
//Finalize subclass state
...
}finally{
super.finalize();
}
}
  可以通过使用匿名类来实现终结函数守卫者(finalizer guardian)”,以确保子类和超类的终结函数都被调用。参见第18条。<o:p></o:p>

<o:p> </o:p>

<o:p> </o:p>

<o:p> </o:p>

读《Effective java 中文版》(8<o:p></o:p>

  尽管Object是一个具体类,但它主要是为扩展,它的所有非final方法(equals,hashCode,toString,clonefinalize)都是为了被改写的,每个方法的改写都有明确的通用约定。<o:p></o:p>

  第7条:在改写equals的进修请遵守通用约定<o:p></o:p>

  equals方法实现了等价关系:<o:p></o:p>


一个失败的equals改写:
public final class CaseInsensitiveString{
private String s;
public CaseInsensitiveString(String s){
if(s==null) throw new NullPointerException();
this.s=s;
}
//Broken-violates symmetry!
public boolean equals(Object o){
if (o instanceof CaseInsensitiveString) return s.equalsIgnoreCase((CaseInsensitiveString)o).s);
if (o instanceof String) return s.equalsIgnoreCase((String)o);
return false;
}
....
}
另一个有问题的equals改写:
public class Point{
private final int x;
private final int y;
public Point(int x,int y){
this.x=x;this.y=y;
}
public boolean equals(Object o){
if(!(o instanceof Point)) return false;
Point p=(Point)o;
return p.x==x && p.y==y;
}
...
}
public class ColorPoint extends Point{
private Color color;
public ColorPoint(int x,int y,Color color){
super(x,y);
this.color=color;
}
//Broken - violates transitivity!
public boolean equals(Object o){
if(!(o instanceof Point))return false;
//if O is a normal Point ,do a color-blind comparison
if(!(o instanceof ColorPoint))return o.equals(this);
//o is a ColorPoint,do a full comparison
ColorPoint cp=(ColorPoint)o;
return super.equals(o)) && cp.color==color;
}
}
  要想在扩展一个可以实例化的类的同时,既要增加新的特征,同时还要保留equals约定,没有一个简单的办法可以做到这一点。根据复合优先于继承的建议,可以如下改动:
public class ColorPoint{
private Point point;
private Color color;
public ColorPoint(int x,int y,Color color){
point=new Point(x,y);
this.color=color;
}
public Point asPoint(){
return point;
}
public boolean equals(Object o){
if(!(o instanceof ColorPoint)) return false;
ColorPoint cp=(ColorPoint)o;
return cp.point.equals(point) && cp.color.equals(color);
}
...
}
  实现高质量equals方法的处方<o:p></o:p>

<!---->1.      <!---->使用==操作符检查实参是否为指向对象的一个引用
如果是,返回true;<o:p></o:p>

<!---->2.      <!---->使用instanceof操作符检查实参是否为正确的类型
如果不是,返回false;<o:p></o:p>

<!---->3.      <!---->把实参转换到正确的类型<o:p></o:p>

<!---->4.      <!---->对于该类中每一个"关键(significant)"域,检查实参中的域与当前对象中对应的域值是否匹配。
如果所有的测试都成功,则返回true;<o:p></o:p>

<!---->5.      <!---->当你编写完成了equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的?<o:p></o:p>


  下面是一些告诫:<o:p></o:p>

<o:p> </o:p>

<o:p> </o:p>

读《Effective java 中文版》(9<o:p></o:p>

8条:改写equals时总是要改写hashCode
  java.lnag.Object中对hashCode的约定:<o:p></o:p>

<!---->1.      <!---->在一个应用程序执行期间,如果一个对象的equals方法做比较所用到的信息没有被修改的话,则对该对象调用hashCode方法多次,它必须始终如一地返回同一个整数。<o:p></o:p>

<!---->2.      <!---->如果两个对象根据equals(Object o)方法是相等的,则调用这两个对象中任一对象的hashCode方法必须产生相同的整数结果。<o:p></o:p>

<!---->3.      <!---->如果两个对象根据equals(Object o)方法是不相等的,则调用这两个对象中任一个对象的hashCode方法,不要求产生不同的整数结果。但如果能不同,则可能提高散列表的性能。<o:p></o:p>

<o:p> </o:p>

看个不改写hashCode导致使用hashMap不能出现预期结果的例子:
public final class PhoneNumber{
private final short areaCode;
private final short exchange;
private final short extension;

public PhoneNumber(int areaCode,int exchage,int extension){
rangeCheck(areaCode,999,"area code");
rangeCheck(exchange,999,"exchange");
rangeCheck(extension,9999,"extension");
this.areaCode=(short) areaCode;
this.exchange=(short) exchange;
this.extension=(short)extension;
}
private static void rangeCheck(int arg,int max, String name){
if(arg<0 || arg>max) throw new IllegalArgumentException(name+":"+arg);
}
public boolean equals(Object o){
if (o == this) reutrn true;
if (!(o instanceof PhoneNumber)) return false;
PhoneNumber pn=(PhoneNumber)o;
return pn.extension==extension && pn.exchange=exchange && pn.areaCode=areaCode;
}
//No hashCode method
...
}
现在有以下几行程序:
Map m=new HashMap();
m.put(new PhoneNumber(1,2,3),"Jenny");
m.get(new PhoneNumber(1,2,3))的返回值什么?
  虽然这个实例据equals是相等的,但由于没改写hashCode而致两个实例的散列码并不同(即违反第二条要求),因则返回的结果是null而不是"Jenny".
  理想情况下,一个散列函数应该把一个集合中不相等的实例均匀地分布到所有可能的散列值上,下面是接近理想的处方<o:p></o:p>

<!---->1.      <!---->把某个非零常数值(如17)保存在一个叫resultint类型的变量中;<o:p></o:p>

<!---->2.      <!---->对于对象中每个关键字域f(equals方法中考虑的每一个域),完成以下步骤:<o:p></o:p>

<!---->1.      <!---->为该域计算int类型的散列码c:<o:p></o:p>

<!---->1.

评论

相关推荐

Global site tag (gtag.js) - Google Analytics