现在的位置: 首页 > 综合 > 正文

读《Effective java 中文版》(1)

2013年12月09日 ⁄ 综合 ⁄ 共 11710字 ⁄ 字号 评论关闭

FROM 竹笋炒肉 http://hedong.3322.org/

读《Effective java 中文版》(1)
  Joshua Bloch著,潘爱民译,机械工业出版社
  我是在过年回家的往返火车上,翻完了这本书。为了帮助java程序员“用习惯和高效的方式”使用Java语言,作者利用9章的篇幅,提出了57条规则。有些规则你可能在别的地方或书中也曾见到,但对这些规则的解释以及谈及的一些java背后的技术内容,更让人觉得有所收获。此书确实不愧为2002年Jolt大奖(
http://www.sdmagazine.com/),如果你曾用Java或相关技术开发过一段时间的软件,我建议你看一下这本书,一方面加深一下对Java语言的理解,一方面检查一下自己的实力。
  回来以后,从网上找到了这书的目录,列在后面供参考。并将我重读此书的一些笔记,陆续放上来。

译者序

前言
第1章 引言 1

第2章 创建和销毁对象 4
第1条:考虑用静态工厂方法代替构造函数 4
第2条:使用私有构造函数强化singleton属性 8
第3条:通过私有构造函数强化不可实例化的能力 10
第4条:避免创建重复的对象 11
第5条:消除过期的对象引用 14
第6条:避免使用终结函数 17

第3章 对于所有对象都通用的方法 21
第7条:在改写equals的时候请遵守通用约定 21
第8条:改写equals时总是要改写hashCode 31
第9条:总是要改写toString 36
第10条:谨慎地改写clone 39
第11条:考虑实现Comparable接口 46

第4章 类和接口 51
第12条:使类和成员的可访问能力最小化 51
第13条:支持非可变性 55
第14条:复合优先于继承 62
第15条:要么专门为继承而设计,并给出文档说明,要么禁止继承 67
第16条:接口优于抽象类 72
第17条:接口只是被用于定义类型 76
第18条:优先考虑静态成员类 78

第5章 C语言结构的替代 82
第19条:用类代替结构 82
第20条:用类层次来代替联合 84
第21条:用类来代替enum结构 88
第22条:用类和接口来代替函数指针 97

第6章 方法 100
第23条:检查参数的有效性 100
第24条:需要时使用保护性拷贝 103
第25条:谨慎设计方法的原型 107
第26条:谨慎地使用重载 109
第27条:返回零长度的数组而不是null 114
第28条:为所有导出的API元素编写文档注释 116

第7章 通用程序设计 120
第29条:将局部变量的作用域最小化 120
第30条:了解和使用库 123
第31条:如果要求精确的答案,请避免使用float和double 127
第32条:如果其他类型更适合,则尽量避免使用字符串 129
第33条:了解字符串连接的性能 131
第34条:通过接口引用对象 132
第35条:接口优先于映像机制 134
第36条:谨慎地使用本地方法 137
第37条:谨慎地进行优化 138
第38条:遵守普遍接受的命名惯例 141

第8章 异常 144
第39条:只针对不正常的条件才使用异常 144
第40条:对于可恢复的条件使用被检查的异常,对于程序错误使用运行时异常 147
第41条:避免不必要地使用被检查的异常 149
第42条:尽量使用标准的异常 151
第43条:抛出的异常要适合于相应的抽象 153
第44条:每个方法抛出的异常都要有文档 155
第45条:在细节消息中包含失败-捕获信息 157
第46条:努力使失败保持原子性 159
第47条:不要忽略异常 161

第9章 线程 162
第48条:对共享可变数据的同步访问 162
第49条:避免过多的同步 168
第50条:永远不要在循环的外面调用wait 173
第51条:不要依赖于线程调度器 175
第52条:线程安全性的文档化 178
第53条:避免使用线程组 181

第10章 序列化 182
第54条:谨慎地实现Serializable 182
第55条:考虑使用自定义的序列化形式 187
第56条:保护性地编写readObject方法 193
第57条:必要时提供一个readResolve方法 199

中英文术语对照 202
参考文献 207
模式和习惯用法索引 212
索引 214

读《Effective java 中文版》(2)
  第1条:考虑用静态工厂方法代替构造函数

静态工厂方法的一个好处是,与构造函数不同,静态工厂方法具有名字。
第二个好处是,与构造函数不同,它们每次被调用的时候,不要求非得创建一个新的对象。
第三个好处是,与构造函数不同,它们可以返回一个原返回类型的子类型的对象。
  静态工厂方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以并不存在,从而可以成为服务提供者框架(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();
}
}
}
静态工厂方法的主要缺点是,类如果不含公有的或者受保护的构造函数,就不能被子类化。
第二个缺点是,它们与其它的静态方法没有任何区别。
  对它们的命名习惯是:

valueOf
该方法返回的实例与它的参数具有相同的值,常用于非常有效的类型转换操作符
getInstance
返回的实例是由方法的参数来描述的,但不能够说与参数具有相同的值。常用于提供者框架中。

读《Effective java 中文版》(3)
  第2条:使用私有构造函数强化Singleton属性
  关于Singleton在《Java Design Patterns A Tutorial》一书(汉译为JAVA设计模式)的第6章也有论述。

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

公有静态成员是一个final域
例如://singleton with final field
public class Elvis{
public static final Elvis INSTANCE = new Elvis()[
private Elvis(){
...
}
}

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

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

 

读《Effective java 中文版》(4)
  第3条:通过私有构造函数强化不可实例化的能力

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

 

读《Effective java 中文版》(5)
第4条:避免创建重复的对象

  从性能的角度考虑,要尽可能地利用已经创建的大对象(创建代价比较高的对象)。
  如果一个对象是非可变的(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条的核心,该条讲的是保护性拷贝的问题)。

 

读《Effective java 中文版》(6)
  第5条:消除过期的对象引用

  下面的例子存在内容泄漏(或者说无意识的对象保持,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.LinkedHashMap的revmoveEldestEntry方法来实现后一方案。
  如果一个持续运行的java应用速度越来越慢,就要考虑是否检查内存泄露问题。
  书中说检查内存泄露的软件统称heap profiler,我检索到了两个
http://sourceforge.net/projects/simpleprofilerhttp://www.manageability.org/blog/stuff/open-source-profilers-for-java,以后有机会可得好好研究一番。不知读到此文的朋友,能否再推荐几个好的工具软件?

 

读《Effective java 中文版》(7)
  第6条:避免使用终结函数

  终结函数(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条。

 

读《Effective java 中文版》(8)
  尽管Object是一个具体类,但它主要是为扩展,它的所有非final方法(equals,hashCode,toString,clone和finalize)都是为了被改写的,每个方法的改写都有明确的通用约定。

  第7条:在改写equals的进修请遵守通用约定

  equals方法实现了等价关系:

自反性(reflexive)

对称性(symmetric)

传递性(transitive)

一致性(consistent)

对于任意的非空引用值x,x.equals(null)一定返回false.

一个失败的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方法的“处方”:

使用==操作符检查“实参是否为指向对象的一个引用”
如果是,返回true;

使用instanceof操作符检查“实参是否为正确的类型”,
如果不是,返回false;

把实参转换到正确的类型

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

当你编写完成了equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的?

  下面是一些告诫:

当你改写equals的时候,总是要改写hasCode方法(参见第8条)

不要企图让equals方法过于聪明
如果只是简单地测试域中的值是否相等,则不难做到遵守equals约定。

不要使equals方法依赖于不可靠的资源
否则难以满足一致性要求。除了少数的例外,equals方法应该针对驻留在内存中的对象执行确定性的计算

不要将equals声明中的Object对象替换为其它的类型
当你的equals不能正常工作时,看看是不是犯了下述例子的错误。
public boolean equals(MyClass o){
...
}

 

读《Effective java 中文版》(9)
第8条:改写equals时总是要改写hashCode
  java.lnag.Object中对hashCode的约定:

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

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

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

 

看个不改写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".
  理想情况下,一个散列函数应该把一个集合中不相等的实例均匀地分布到所有可能的散列值上,下面是接近理想的“处方”:

把某个非零常数值(如17)保存在一个叫result的int类型的变量中;

对于对象中每个关键字域f(指equals方法中考虑的每一个域),完成以下步骤:

为该域计算int类型的散列码c:

如果该域是bloolean类型,则计算(f?0:1)

如果该域是byte,char,short或int类型,则计算(int)f

如果该域是long类型,则计算(int)(f^(>>>32))

如果该域是float类型,则计算Float.floatToIntBits(f)

如果该域是double类型,则计算Double.doubleToLongBits(f)得一long类型值,然后按前述计算此long类型的散列值

如果该域是一个对象引用,则利用此对象的hashCode,如果域的值为null,则返回0

如果该域是一个数组,则对每一个数组元素当作单独的域来处理,然后安下一步的方案来进行合成

 

利用下面的公式将散列码c 组合到result中。result=37*result+c;

 

检查“相等的实例是否具有相等的散列码?”,如果为否,则修正错误。

  依照这个处方,得PhoneNumber的hashCode方法:
public int hashCode(){
int result=17;
result=37*result+areaCode;
result=37*result+exchange;
result=37*result+extension;
return result;
}
  如果计算散列码的代价比较高,可以考虑用内部保存这个码,在创建是生成或迟缓初始化生成它。不要试图从散列码计算中排除掉一个对象的关键部分以提高性能。

 

读《Effective java 中文版》(10)
第9条:总是要改写toString
  对于toString的通用约定是:

返回的字符串应是一个简洁、信息丰富且易于阅读的表达形式
建议所有的子类都改写这个方法(超类的实现是"类名@散列串")

 

  当一个对象被传递给println、字符串连接操作符(+)、assert(java1.4版)时,toString会被自动调用。
  在实际应用中,toString方法应该返回对象中包含的所有令人感兴趣的信息或摘要信息。不管你是否决定指定返回值的格式,都应该在文档中明确地表明你的意图。另外,为toString返回值中包含的所有信息都提供一种编程访问途径是一个好的做法,这样可以让程序直接得到特定的数据,则无需要费力来解析这个字符串来获得。


抱歉!评论已关闭.