译者序
序
前言
第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(); } } } 静态工厂方法的主要缺点是,类如果不含公有的或者受保护的构造函数,就不能被子类化。 第二个缺点是,它们与其它的静态方法没有任何区别。 对它们的命名习惯是: <!--[if !supportLists]-->1. <!--[endif]-->valueOf
该方法返回的实例与它的参数具有相同的值,常用于非常有效的类型转换操作符 <!--[if !supportLists]-->2. <!--[endif]-->getInstance
返回的实例是由方法的参数来描述的,但不能够说与参数具有相同的值。常用于提供者框架中。 读《Effective java 中文版》(3)
第2条:使用私有构造函数强化Singleton属性
关于Singleton在《Java Design Patterns A Tutorial》一书(汉译为JAVA设计模式)的第6章也有论述。 Singleton(只能被实例化一次的类)的实现,要私有的构造函数与公有的静态成员结合起来,根据静态成员的不同,分为两种方法:
<!--[if !supportLists]-->1. <!--[endif]-->公有静态成员是一个final域
例如://singleton with final field public class Elvis{ public static final Elvis INSTANCE = new Elvis()[ private Elvis(){ ... } } <!--[if !supportLists]-->2. <!--[endif]-->公有静态成员是一个工厂方法
例如://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:///projects/simpleprofiler和http://www./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方法实现了等价关系:
一个失败的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方法的“处方”: <!--[if !supportLists]-->1. <!--[endif]-->使用==操作符检查“实参是否为指向对象的一个引用”
如果是,返回true; <!--[if !supportLists]-->2. <!--[endif]-->使用instanceof操作符检查“实参是否为正确的类型”,
如果不是,返回false; <!--[if !supportLists]-->3. <!--[endif]-->把实参转换到正确的类型
<!--[if !supportLists]-->4. <!--[endif]-->对于该类中每一个"关键(significant)"域,检查实参中的域与当前对象中对应的域值是否匹配。
如果所有的测试都成功,则返回true; <!--[if !supportLists]-->5. <!--[endif]-->当你编写完成了equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的?
下面是一些告诫:
读《Effective java 中文版》(9)
第8条:改写equals时总是要改写hashCode
java.lnag.Object中对hashCode的约定: <!--[if !supportLists]-->1. <!--[endif]-->在一个应用程序执行期间,如果一个对象的equals方法做比较所用到的信息没有被修改的话,则对该对象调用hashCode方法多次,它必须始终如一地返回同一个整数。
<!--[if !supportLists]-->2. <!--[endif]-->如果两个对象根据equals(Object o)方法是相等的,则调用这两个对象中任一对象的hashCode方法必须产生相同的整数结果。
<!--[if !supportLists]-->3. <!--[endif]-->如果两个对象根据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". 理想情况下,一个散列函数应该把一个集合中不相等的实例均匀地分布到所有可能的散列值上,下面是接近理想的“处方”: <!--[if !supportLists]-->1. <!--[endif]-->把某个非零常数值(如17)保存在一个叫result的int类型的变量中;
<!--[if !supportLists]-->2. <!--[endif]-->对于对象中每个关键字域f(指equals方法中考虑的每一个域),完成以下步骤:
<!--[if !supportLists]-->1. <!--[endif]-->为该域计算int类型的散列码c:
<!--[if !supportLists]-->1. <!--[endif]-->如果该域是bloolean类型,则计算(f?0:1)
<!--[if !supportLists]-->2. <!--[endif]-->如果该域是byte,char,short或int类型,则计算(int)f
<!--[if !supportLists]-->3. <!--[endif]-->如果该域是long类型,则计算(int)(f^(>>>32))
<!--[if !supportLists]-->4. <!--[endif]-->如果该域是float类型,则计算Float.floatToIntBits(f)
<!--[if !supportLists]-->5. <!--[endif]-->如果该域是double类型,则计算Double.doubleToLongBits(f)得一long类型值,然后按前述计算此long类型的散列值
<!--[if !supportLists]-->6. <!--[endif]-->如果该域是一个对象引用,则利用此对象的hashCode,如果域的值为null,则返回0
<!--[if !supportLists]-->7. <!--[endif]-->如果该域是一个数组,则对每一个数组元素当作单独的域来处理,然后安下一步的方案来进行合成
<!--[if !supportLists]-->2. <!--[endif]-->利用下面的公式将散列码c 组合到result中。result=37*result+c;
<!--[if !supportLists]-->3. <!--[endif]-->检查“相等的实例是否具有相等的散列码?”,如果为否,则修正错误。
依照这个处方,得PhoneNumber的hashCode方法: public int hashCode(){ int result=17; result=37*result+areaCode; result=37*result+exchange; result=37*result+extension; return result; } 如果计算散列码的代价比较高,可以考虑用内部保存这个码,在创建是生成或迟缓初始化生成它。不要试图从散列码计算中排除掉一个对象的关键部分以提高性能。 读《Effective java 中文版》(9)
第9条:总是要改写toString
对于toString的通用约定是:
当一个对象被传递给println、字符串连接操作符(+)、assert(java1.4版)时,toString会被自动调用。
在实际应用中,toString方法应该返回对象中包含的所有令人感兴趣的信息或摘要信息。不管你是否决定指定返回值的格式,都应该在文档中明确地表明你的意图。另外,为toString返回值中包含的所有信息都提供一种编程访问途径是一个好的做法,这样可以让程序直接得到特定的数据,则无需要费力来解析这个字符串来获得。 读《Effective java 中文版》(11)
第10条:谨慎地改写clone
Cloneable接口的目的是作为对象的一个mixin接口,表明这样的对象允许克隆。Cloneable没有包含任何方法,只是决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,则Object的clone方法返回该对象的逐域拷贝,否则抛出一个CloneNotSupportedException异常。
至于clone本身,是一种很有风险的、语言之外的对象创建机制:无须调用构造函数就可以创建一个函数。 clone方法的约定是:创建和返回对象一个拷贝,且
当然,这三个也不是绝对的要求。
如果你改写了一个非final类的clone方法,则应该返回一个通过调用super.clone而得到的对象。如果一个类的所有超类都遵守这条规则,那么一直调用super.clone最终会调用到object的clone方法,从而创建出正确的类的实例。这种机制大致上类似于自动的构造函数链,只不过它不是强制要求的。 实际上,clone方法是另一个构造函数,你必须确保它不会伤害到原始的对象,并且正确地建立起被克隆对象中的约束关系。 Clone结构与指向可变对象的final域的正常用法是不兼容的。非除非在原始对象和克隆对象之间可以安全地共享此可变对象。为了使一个类成为可克隆的,可能有必要从某些域中去掉final修饰符。 实现对象拷贝的好办法,是提供一个拷贝构造函数(其唯一的参数的类型是包含该构造函数的类)。 Cloneable有如此多问题,其它的接口不应该扩展该接口,为了继承而设计的类(参见第15条)也就该实现这个接口。 读《Effective java 中文版》(12)
第11条:考虑实现Comparable接口
compareTo方法在Object中并没有被声明,它是java.lang.Compareable接口中唯一的方法。一个类实现了Compareable接口,就表明它的实例具有内在的排序关系(natural ordering)。如果一个数组中的对象实现了Compareable接口,则对这个数组进行排序非常简单:Arrays.sort();对于存储在集合中的Comareable对象,搜索、计算极值以及自动维护工作都非常简单。一旦你的类实现了Compareable接口,它就可以跟许多泛型算法(generic algorithm)以及依赖于该接口的集合的操作实现进行协作,以小的努力得到强大的功能。 Java平台库中的所有值类(value classes)都实现了Compareable接口。 compareTo的约定是:
将当前这个对象与指定的对象进行顺序比较,当该对象小于、等于或大于指定对象时,分别返回一个负整数、0或正整数,如果无法进行比较,则抛出ClassCastException异常。
与equals不同的是,在跨越不同类的时候,comapreTo可以不做比较。 依赖于比较关系的类包括有序集合类TreeSet和TreeMap,以及工具类Collections和Arrays,它们内部都含有搜索和排序算法。 与equals相似,compareTo也遵循:自反性、对称性、传递性、非空性。并且,没有一种简单地方法可以做到,在扩展一个新的可实例化的类的时候,既增加了新的特征,同时以保持了compareTo约定。 读《Effective java 中文版》(13)
第12条:使类和成员的可访问能力最小化
一个设计良好的模块会隐藏所有的实现细节,把它的API与实现清晰地隔离开来。模块之间只通过它们的API进行通信,一个模块不需要知道其它模块的内部工作情况,此即为信息隐藏(information hiding)或封装(encapsulation)。这样做的理由,是为有效地解除一个系统中各模块之间的耦合关系,其代价是性能的牺牲。 java中一个实体(类、接口、成员等)的可访问性,由该实体声明所在位置,以及该实体声明中所出现的访问修饰符(private protected 和 public)共同决定的。
经验表明,应尽可能地使一个类或成员不被外界访问,即应使用最低可能地、且与该软件正确功能相一致的访问级别。 “使一个不必要的公有类成为包级私有的类”。对于顶层的(非嵌套的)类和接口,只有两种可能的访问级别:包级私有和公有。把一个类或接口做成包级私有,则它成了包的实现的一部分,而不是包导出的API的一部分,从而可在以后自由地修改它;如果做成公有,则有义务永远支持它。 如果一个包级私有的顶层类或接口只在某一个类的内部使用,则应考虑将其做为后者的一个私有嵌套类(或接口)。 对于成员(域、方法、嵌套类和嵌套接口)有四种可能的访问级别,可访问性递增顺序为:
受保护的成员是一类的导出的API的一部分,须永远支持,受保护的成员应尽量少用。 如果一个方法改写了超类中的一个方法,那么子类中该方法的访问级别低于超类中的级别是不允许的。 公有类应尽可能地减少公有的域。包含公有可变域(公有的非final域或公有指向可变对象的final域)的类不是线程安全的。一个例外是,通过公有的静态final域来暴露类的常量是允许的。 具有公有的静态final数组域几乎总是错误的。如: //protential security hole! public static fina Type[] VALUES={...}; 应被替换为一个私有数组,以及一个公有的非可变列表,如: private static Type[] PRIVATE_VALUES={...}; public static final List VALUES= Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES)); 或者被替换为一个私有数组,以及返回数组拷贝的公有方法,如: private static Type[] PRIVATE_VALUES={...}; public static final Types[] values(){ return (Type[] PRIVATE_VALUES.clone()); }; 读《Effective java 中文版》(14)
第13条:支持非可变性
一个非可变类是一个简单的类,它的实例不能被修改。每个实例中包含的所有信息都必须在该实例被创建的时候就提供出来,并在对象的整个生存期内固定不变。如java的String类、原语类型的包装类、BigInteger和BigDecimal等。 非可变类的5条规则:
看一个“复数”的例子: public final class Complex{ private final float re; private final fload im; public Complex(float re,float im){ this.re=re; this.im=im; } public float realPart(){return re;}; public float imaginaryPart(){return im;}; public Complex add(Complex c){ return new Complex(re+c.re,im+c.im); } public Complex subtract(Complex c){ return new Complex(re-c.re,im-c.im); } public Complex multiply(Complex c){ return new Complex(re*c.re-im*c.im,re*c.im+im*c.re); } public Complex divide(Complex c){ float tmp=c.re*c.re+c.im*c.im; return new Complex((re*c.re+im*c.im)/tmp,(im*c.re-re*c.im)/tmp); } public boolean euqals(Object o){ if (o==this) return true; if(!(o instanceof Complex))return false; Complex c=(Complex)o; return (Float.floatToIntBits(re)==Float.floatToIntBits(c.re))&& Float.floatToIntBits(im)==Float.floatToIntBits(c.im)); } public int hashCode(){ int result=17+Float.floatToIntBits(re); result=37*result+Float.floatToIntBits(im); return result; } public String toString(){ return "("+re+"+"+im+"i)"; } }//此Complex定义是一个非工业级的复数实现,一是因为乘、除法的实现可能导致不正确舍入,二是没有处理复数NaN或无穷大的情况。 该例中对,对复数的操作都会返回一个新的实例,而不修原来的实例,这被称之为函数式作法(functional),与之对应的是修改实例状态的过程式方法(precedural)。 非可变对象比较简单,它本质上是线程安全的,它们不要求同步,因而非可变对象可被自由地共享(一个简单地做法,是为提供公有的静态final常量),而不需要进行保护性拷贝,而且也应为非可变类提供clone方法或拷贝构造函数(如String类的拷贝构造函数,应尽量不用)。 不仅可以共享非可变对象,而且可以共享它们的内部信息。例如,BigInteger的negate方法产生一个新实例,其存放数值的数组与原来的实例是同一个数组。 非可变对象为其他对象--无论是可变的还是非可变的--提供了大量的构造(buildig block)。 非可变类真正唯一的缺点是,对于每一个不同的值都要求一个单独的对象。常见的解决思路是提供一个可变的配套类,如String的配置可变类StringBuffer. 一个类绝不允许其子类改写的方法有三:
由于历史的原因,BigInteger或BigDecimal的方法可以被改写,所以你在开发自己的非可变类时,如果不能确保收到这两个类的实例的非可变性,则需要进行保护性拷贝。如: public void foo(BigInteger b){ if(b.getClass() != BigInteger.class) b=new BigInteger(b.toByteArray()); ... } 其实,前面所述的规则过于强,只需满足:没有一个方法能对对象状态产生外部可见(externally visible)的改变。例如: //Cached, lazily initialized function of an immutable object private volatile Foo cachedFooVal = UNLIKELY_FOO_VALUE; public Foo foo(){ Foo result=cachedFooVal; if(result==UNLIKELY_FOO_VALUE) result=cachedFooVal=fooVal(); return result; } //fooVal函数开销较大,所以在前面使用了迟缓初始化的技术。 private Foo fooVal(){...} 除非有很好的理由要让一个类成为可变类,否则就应该是非可变的,缺点是性能问题,如果对性能要求比较高,可以提供公有的可变配套类。 如果一个类不能被做成非可变类,仍可应尽可能地限制它的可变性。构造函数应创建完全初始化的对象,所有约束关系应在这时候建立起来,构造函数不应把“只初始化了一部分的实例”传递给其它的方法。 读《Effective java 中文版》(15)
第14条:复合优先于继承
继承是实现代码重用的有力手段,但不适当地使用继承会导致脆弱的软件。
与方法调用不同的是,继承打破了封装性,一个子类依赖于其超类中特定功能的实现细节。超类的实现的变化,则子类可能会被打破。
有一种办法可以避免上述所有问题:新类,不是扩展一个已有类,而是设置一个私有域,它引用这个已有类的一个实例。这种设计被称为“复合(composition)”。新类中的每个实例方法,都可以调用被包含的已有类实例中对应的方法,并返回它的结果,即为“转发方法(forwarding method)”。这样的类比较稳固,这不依赖于已有类的实现细节。一个类的实例都把另一个类的实现包装起来了,则前者的类叫做包装类(wrapper class)。 看一个例子: //wrapper class - uses composition in place of inheritance public class InstrumentedSet implements Set{ private final Set s; private int addCount=0; public InstrumentedSet(Set s){ this.s=s; } public boolean add(Object o){ addCount++; return s.add(); } public boolean addAll(Collection c){ addCount+=c.size(); return s.addAll(c); } public int getAddCount(){ return addCount; } //forwarding methods public void clear() { s.clear(); } public boolean contains(Object o){return s.contains(o); } public boolean isEmpty() {return s.isEmpty(); } public int size() {return s.size(); } .... public String toString() {return s.toString(); } } 上例中,InstrumentedSet类对Set类进行了修饰,增加了计数特性。有时,复合和转发这两项技术的结合被错误地引用为“委托(delegation)”,从技术的角度而言,这不是委托,除非包装对象把自己传递给一个被包装的对象。 包装类几乎没有什么缺点。需要注意的是,包装类不适合用于回调框架(callback framework)中。在回调框架中,对象把自己的引用传递给其它的对象,以便将来调用回来,当它被包装起来以后,它并不知道外面的包装对象的情况,所以它传递一个指向自己的引用(this)时,会造成回调时绕开外面的包装对象的问题。这被称为SELF问题。 只有当子类真正是超类的“子类型(subtype)”时,继承才是合适的。即两者之存在“is-a”的关系。java平台中,也有违反这条规则的地方:如Stack不是向量,所以不应扩展Vector;属性列表不是散列表,所以Properties不应扩展Hashtable。在决定使用复合还是扩展时,还要看一试图扩展的类的API有没有缺陷,如果你愿意传播这些缺陷到自己的API中,则用继承,否则可用复合来设计一个新的API。 读《Effective java 中文版》(16)
第15条:要么专门为继承而设计,并给出文档说明,要么禁止继承
一个专门为了继承而设计并且具有良好文档说明的类,意味着:
为了继承而设计一个类,要求对这个类有一些实质性的限制。对于那些并非为了安全地进行子类化而设计和编写文档的类(如普通的具体类),禁止子类化。有两种办法可以禁止子类化:
<!--[if !supportLists]-->1. <!--[endif]-->把类声明为final
<!--[if !supportLists]-->2. <!--[endif]-->把所有的构造函数变成私有的或包级私有的,增加一些公有的静态工厂来替代构造函数的位置
如果必须从这样的类来继承,则要确保这类永远不会调用到它的可改写的方法。
读《Effective java 中文版》(17)
第16条:接口优于抽象类
Java语言中的接口与抽象类的一些区别:
看一下接口:
<!--[if !supportLists]-->1. <!--[endif]--> 已有的类可以很容易被更新,以实现新的接口。只要:增加要求的方法,在类的声明上增加implements子句。
<!--[if !supportLists]-->2. <!--[endif]--> 接口是定义mixin(混合类型)的理想选择。一个mixin是指这样的类型:一个类除了实现它的基本类型(primary type)之外,还可以实现这个mixin类型,以表明它提供了某些可选择的行为。接口之所以能定义mixin,因为它允许可选的功能可被混合到一个类型的基本类型中,而抽象类不能用于定义mixin类型,同样的理由是因为它们不能被更新到已有的类中:一个类不可能有一个以上的父类,并且在类层次结构中没有适当的地方来放置mixin。
接口使得我们可以构造出非层次结构的类型结构。看例子: public interface Singer{ AudioClip sing(Song s); } public interface Songwriter{ Song compose(boolean hit); } 为解决歌唱家本人也能做曲的情况,可以很简单地做到: public interface SingerSongwriter extends Singer, Songwriter{ AudioClip strum(); void actSensitive(); } 如果用抽象类来做,会是如何? <!--[if !supportLists]-->3. <!--[endif]--> 接口使得安全地增强一类的功能成为可能,做法是使用第14条介绍的包装类模式。如果用抽象类型来做,则程序员除了使用继承没有别的方法。
<!--[if !supportLists]-->4. <!--[endif]--> 接口不允许包含方法的实现。把接口和抽象类的优点结合起来,对于期望导出的每一个重要接口,都提供一个抽象的骨架实现(skeletal implementaion)类。接口的作用仍是定义类型,骨架实现类负责所有与接口实现相关的工作。按照惯例,骨架实现被称为AbstractInterface(注:此interface是所实现的接口的名字,如AbstractList,AbstractSet)。看一个静态工厂:
//List adapter for int array static List intArrayAsList(final int[] a){ if (a==null) throw new NullPointerException(); return new AbstractList(){ public Object get(int i){ return new Integer(a[i]); } public int size(){ return a.length; } public Object set(int i,Object o){ int oldVal=a[i]; a[i]=((Integer)o).intValue(); return new Integer(oldVal); } } } 这个例子是一个Adapter,它使得一个int数组可以被看作一个Integer实例列表(由于存在int和Integer之间的转换,其性能不会非常好) 骨架实现的优美之外在于,它们为抽象类提供了实现上的帮助,但又没有强加“抽象类被用做类型定义时候”所特有的严格限制。对一地一个接口的大多数实现来讲,扩展骨架实现类是一个很显然的选择,当然它也只是一个选择而已。 实现了这个接口的类可以把对于接口方法的调用,转发到一个内部私有类的实例上,而这个内部私有类扩展了骨架实现类。这项技术被称为模拟多重继承。 编写一个骨架实现类相对比较简单,首先要认真研究接口,并且确实哪些方法是最为基本的(primitive),其他的方法在实现的时候将以它们为基础,这些方法将是骨架实现类中的抽象方法;然后须为接口中的其它方法提供具体的实现。骨架实现类不是为了继承的目的而设计的。(怎么理解?)看例子: //skeletal implementation public abstract class AbstractMapEntry implements Map.Entry{ //primitives public abstract Object getKey(); public abstract Object getValue(); //Entries in modifiable maps must override this method public Object setValue(Object value){ throw new UnsupportedOperationException(); } //Implements the general contract of Map.Entry.equals public boolean equals(Object o){ if (o==this) return true; if (!(o instanceof Map.Entry)) return false; Map.Entry arg=(Map.Entry)o; return eq(getKey(),arg.getKey())&&eq(getValue(),arg.getValue()); } private static boolean eq(Object o1,Object o2){ return (o1==null?02==null:o1.equals(o2)); } //implements the general contract of Map.Entry.hashCode public int hashCode(){ return (getKey()==null?0:getKey.hashCode())^(getValue()==null?0:getValue().hashCode()); } } 使用抽象类来定义允许多个实现的类型,比使用接口有一个明显的优势:抽象类的演化比接口的演化要容易的多。在后续的发行版中,如果希望在抽象类中增加一个方法,只增加一个默认的合理的实现即可,抽象类的所有实现都自动提供了这个新的方法。对于接口,这是行不通的。虽然可以在骨架实现类中增加一方法的实现来解决部分问题,但这不能解决不从骨架实现类继承的接口实现的问题。由此,设计公有的接口要非常谨慎,一旦一个接口被公开且被广泛实现,对它进行修改将是不可能的。 读《Effective java 中文版》(18)
第17条:接口只是被用于定义类型
当一个类实现了一个接口的时候,这个接口被用做一个类型(type),通过此类型可以引用这个类的实例。因此,一个类实现了一个接口,就表明客户可以对这个类的实例实施某些动作。为了任何其它目的而定义接口是不合适的。 常量接口是对接口的不良应用,因它不满足上述条件。常量接口中没有包含任何方法,只包含静态的final域,每个域都导出一个常量。r看下例。如果一个类要使用这些常量,它只要实现这个接口,就可以避免用类名来修饰常量名。但是,实现一个常量接口,会导致把这样的实现细节(指内部使用常量)泄露到该类的导出API中。而且在将来的发行版本中,如果类不再需要这些常量了,但仍要实现这个接口。还有,这样的类的子类,也会为常量所污染。
//Constant interface pattern - do not use! public interface PhysicalConstants{ static final double AVOGADROS_NUMBER =6.02214199e23; static final double BOLTZMANN_CONSTANT =1.3806503e-23; static final double ELECTRON_MASS 9.10938188e-31; } Java平台中有几个常量接口,如:java.io.ObjectStreamConstants,这不值得效仿。
如果要导出常量,有几种方案: <!--[if !supportLists]-->1. <!--[endif]-->如果这常量与某个已有类或接口紧紧联系在一起,则应该把它们加入类或常量中。
<!--[if !supportLists]-->2. <!--[endif]-->如果这些常量最好被看作一个枚举类型的成员,那么应该用一个类型安全枚举类(typesafe enum class),参见第21条。
<!--[if !supportLists]-->3. <!--[endif]-->否则,应用一个不可被实例化的工具类(utility class),见第3条。看对上面例子的修改:
//constant utility class public class PhysicalConstants{ private PhysicalConstants(){};//prevent instantiation public static final double AVOGADROS_NUMBER =6.02214199e23; public static final double BOLTZMANN_CONSTANT =1.3806503e-23; public static final double ELECTRON_MASS 9.10938188e-31; } 可以通过将常量放在局部变量中,来减少键盘的录入工作量。如: private static final double PI=Math.PI; 读《Effective java 中文版》(19)
第18条:优先考虑静态成员类
嵌套类(nested class)是指被定义在另一类的内部的类,它只为它的外围类服务。如果一个嵌套类可能会用于其它的某个环境,那就应为一个顶层类(top-level class)。嵌套类有四种:静态成员类(static member class)、非静态成员类(nonstatic member class)、匿名类(anonymous class)和局部类(local class),其中后三种称为内部类(inner class)。 静态成员类是一种最简单的嵌套类,最后把它看作一个普通的类,碰巧被声明在另一个类的内部而已,它可以访问外围类的所有成员,包括那些声明为私有的成员。静态成员类是外围类的一个静态成员,与其他的静态成员一样,遵守同样的可访问性规则。如果它被声明为私有的,则只能在外围类的内部才可以被访问。静态成员类的一种通常用法是作为公有的辅助类,仅当它与它的外部类一起使用时才有意义。
从语法上,非静态成员类与静态成员类的差别在于后者有一个static。非静态成员类的每一个实例,都隐含着与外围类的一个外围实例紧密联系在一起。在非静态成员类的实例方法内部,调用外围实例上的方法是可能的,或者使用一个经过修饰的this也可以得到个指向外围实例的引用。在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的。非静态成员类的一种通常用法是定义一个Adapter,它允许外部类的一个实例被看作另一个不相关的类的实例。如Map接口的实现往往使用非静态成员类来实现它们的“集合”视图,这些“集合”视图是由Map的keySet、entrySet、Value方法返回。再看Set集合接口实现迭代器的例子: //Typical use of a nonstatic member class public class MySet extends AbstractSet{ // bulk of the class omitted public Iterator interator(){ return new MyIterator(); } private class MyIterator implements Iterator{ ... } } 如果声明的成员类不要求访问外围实例,那么就把static修饰符放到成员类的声明中。这会减少维护成员类实例对外围实例对象的引用的开销。 私有静态成员类的一种通常用法是用来代表外围类对象的组件。例如:Map实例把一些Key和Value关联起来,其内部通常有一个Entry对象对应于每个键值对。每个Entry对象都与一个Map对象关联,但Entry的方法(getKey、getValue、setValue)都不需要外围的Map。此时,用私有的静态成员类是最佳选择。 匿名类没有名字,它不是外围类的一个成员。它并不与其它的成员一起被声明,而在被使用的点上同时声明和被实例化。匿名类可以出现在代码中任何允许表达式出现的地方。匿名类的行为与成员类非常类似,具体取决于它所在环境:如果匿名类出现在一个非静态的环境中,则它有一个外围实例。 匿名类的适用性有一些限制:
匿名类的常见用法: <!--[if !supportLists]-->1. <!--[endif]-->
匿名类的一通常用法是创建一个函数对象(function object),如: //typical use of an anonymous class //Arrays.sort(args,new comparator(){ public int compare(Object o1,Object o2){ return ((String)o1).length()-((String)o2).length(); } }); <!--[if !supportLists]-->2. <!--[endif]-->另一个常见用法是创建一个过程对象(process object),如Thread、Runnable、TimerTasker实例。
<!--[if !supportLists]-->3. <!--[endif]-->在一个静态工厂方法的内部(参见第16条)
<!--[if !supportLists]-->4. <!--[endif]-->用在复杂的类型安全枚举类型(它要求为每个实例提供单独的子类)
例子:
//typical use of a public static member class public class Calculator{ public static abstract class Operation{ private final String name; Operation(String name){ this.name=name;} public String toString(){ return this.name}; //perform arithmetic op represented by this constant abstract double eval(double x,double y); //doubly nested anonymous classes public static final Operation PLUS = new Operation("+"){ double eval(double x,double y){return x+y;} } public static final Operation MINUS= new Operation("-"){ double eval(double x,double y){return x-y;} } public static final Operation TIMES= new Operation("*"){ double eval(double x,double y){return x*y;} } public static final Operation DIVIDE=new Operation("/"){ double eval(double x,double y){return x/y;} } } //Return hte results of the specified calculation public double calculate(double x,Operation op,double y){ return op.eval(x,y); } } 局部类是四种嵌套类中最少使用的类。在任何“可以声明局部变量”的地方,都可以声明局部类。与成员类一样,局部类有名字,可以被重复使用。与匿名类一样,当且仅当局部类被用于非静态环境下的进修,它们才有外围实例。与匿名类一样,它必须非简短,以不会损害外围方法或者初始化器的可读性。
总之,如果一个嵌套类需要在单个方法之外仍是可见的,或者它太长了,不适合于放在一个方法内部,则应该使用成员类。如果成员类的每个实例都需要一个指向其外围的实例的引用,则把成员类做成非静态的;否则做成静态的。假设一个嵌套类属于一方法的内部,如果只需要在一个地方创建它的实例,并且已经有了一个预先存在的类型可以说明这个类的特征,则把它做成匿名类;否则变成局部类。 读《Effective java 中文版》(20)
第19条:用类代替结构
Java语言中让一个类退化到只包含一些公开的数据域,这样的类与C语言的结构类似,如: //degenerate classes like this should not be public class Point{ public float x; public float y; } 这样的类,抛弃了所有数据封装的优点,而你几乎不能对这样的API做什么改动或限制,应该代之以私有域和公有访问方法,如: //Encapsulated structure class class Point{ private float x; private float y; public Point (float x,float y){ this.x=x; this.y=y; } public float getx(){return this.x;} public float gety(){return this.y;} public void setx(float x){this.x=x;} public void sety(float y){this.y=y;} } JAVA平台库中,有几个类违反了"公有类不应该直接暴露数据域“的告诫,如java.awt中的Point和Dimension。 读《Effective java 中文版》(21)
第20条:用类层次来代替联合
C语言中的联合(union)是用来定义可以容纳多种数据类型的数据结构,常见用法是在一个结构中包含一个联合和一个标签(标签通常是某一个枚举(enum)类型),用标签指明联合中存放的是哪种数据类型的数据,这也被称为可区分的联合。Java语言中,union结构被子类型化这种机制代替。 类层次与可区分的联合相比,有N多好处:
<!--[if !supportLists]-->1. <!--[endif]-->类层次提供了类型的安全性,而C语言中如果标签的指示与union存放对象不一致,则会出错。
<!--[if !supportLists]-->2. <!--[endif]-->代码简洁明了。
<!--[if !supportLists]-->3. <!--[endif]-->容易扩展,即使多方在独立的工作。
<!--[if !supportLists]-->4. <!--[endif]-->可以反映出这些类型之间本质上的层次关系,从而允许更强的灵活性,以及更好的编译时类型检查。
当要编写一个类,而其中包含一个显式的标签域的时候,就应该考虑这个标签是否可以被取消而用一个类层次来代替。 C语言中union的另一个用途是,查看一片数据的内部表示。看例: union{ float f; int bits; }sleaze; sleaze.f=6.699e-41;
print("%x\n",sleeze.bits); 在java语言中可以这样做:
system.out.println(Integer.toHexString(Float.floatToIntBits(6.699e-41f))); 读《Effective java 中文版》(22)
第21条:用类来代替enum结构
C语言中,enum结构定义一个枚举类型:它的合法值是由一组固定的常量组成的。但这种类型定义有问题:
不幸的是,java中最常用的针对枚举类型的模式,也有这些缺点,如例: //the int enum pattern - problematic!! public class PlayingCard{ public static final int SUIT_CLUBS =0; public static final int SUIT_DIAMONDS =1; public static final int SUIT_HEARTS =2; public static final int SUIT_SPADES =3; .... } 幸运的是,java程序还有被称为类型安全枚举的模式:定义一类来代表枚举类型的单个元素,并且不提供任何公有的构造函数,相反提供公有的静态final域,使枚举类型的每一个常量都对应一个域。如:
//the typesafe enum pattern public class Suit{ private final String name; private Suit(String name){this.name=name;} public String toString(){return name;} public static final Suit CLUBS =new Suit("Clubs"); public static final Suit DIAMANDS=new Suit("Diamands"); public static final Suit HEARTS =new Suit("Hearts"); public static final Suit SPADES =new Suit("Spades"); } 即使这个类没有声明为final,客户也没法创建这个类的对象,也无法扩展这个类,因而除了通过这些公有的静态final域导出的Suit对象之外,永远不会有其它的对象存在。 好处:
使得一个类型安全枚举类可以扩展,只需要提供一个受保护的构造函数即可。
看一个可扩展的、可序列化的类型安全枚举类: //Serializable,extensible typesafe enum public abstract class Operation implements Serializable{ private final transient String name; protected Operation(String name){this.name=name;} public static Operation PLUS=new Operation("+"){ protected double eval(double x,double y){return x+y;} }; public static Operation MINUS=new Operation("-"){ protected double eval(double x,double y){return x-y;} }; public static Operation TIMES=new Operation("*"){ protected double eval(double x,double y){return x*y;} }; public static Operation DIVIDE=new Operation("/"){ protected double eval(double x,double y){return x/y;} }; protected abstract double eval(double x,double y); public String toString(){return this.name;} public final boolean equals(Object that){ return super.equals(that); } public final int hashCode(){ return super.hashCode(); } //the 4 following lines are necessary fro serialization private static int nextOrdinal =0; private final int ordinal=nextOrdinal++; private static final Operation[] VALUES={PLUS, MINUS, TIMES, DIVIDE}; Object readResolve() throws ObjectStreamException{ return VALUES[ordinal]; } } //subclass of extensible , serializable typesafe enum abstract class ExtendedOperation extends Operation{ ExtendedOperation(String name){super(name);} public static Operation LOG=new Operation("log"){ protected double eval(double x,double y){return Math.log(x)/Math.log(y);} }; public static Operation EXP=new Operation("exp"){ protected double eval(double x,double y){return Math.power(x,y);} }; //the 4 following lines are necessary fro serialization private static int nextOrdinal =0; private final int ordinal=nextOrdinal++; private static final Operation[] VALUES={LOG,EXP}; Object readResolve() throws ObjectStreamException{ return VALUES[ordinal];//canonicalize } } 与int模式相比,类型安全枚举模式的缺点:
读《Effective java 中文版》(23)
第22条:用类和接口来代替函数指针
C语言支持函数指针,其主要用途是实现Strategy(策略)模式,典型的应用如比较器函数,就是策略模式的一个例子。Java省略了函数指针,因为对象引用可以被用于提供同样的功能。如例: class StringLengthComparator{ public int compare(String s1,String s2){ return s1.lenght()-s2.length(); } } java中实现策略模式,声明一个接口来表示该策略,且为每个具体策略声明一个实现了该接口的类。如果一个具体策略只被使用一次,则常用匿名类实现声明和实例化。如果一个具体策略要被导出以重复使用,则常为一个私有的静态成员类,且通过一个公有静态final域被导出,其类型为该策略接口。
作为一个典型的具体策略类,StringLengthComparator类是无状态的,没有域,它作为一个singleton非常合适,如下: class StringLengthComparator{ private StringLengthComparator(){} public static final StringLengthComparator INSTANCE=new StringLengthComparator(); public int compare(String s1,String s2){ return s1.lenght()-s2.length(); } } 在设计具体策略类的时候,还需要定义一个策略接口,如: //Stragetegy interface public interface Comparator{ public int compare(Object o1,Object o2); } 具体的策略类往往使用匿名类声明,如: Arrays.sort(stringArray,new Comparator(){ public int compare(Object o1,Object o2){ String s1=(String)o1; String s2=(String)o2; return s1.length()-s2.length(); } }); 由于策略接口被用做所有具体策略实例的类型,故不必将具体策略类做成公有的。如下: //exporting a concrete stategy class Host{ ... private static class StrlenCmp implements Comparator , Serializable{ public int compare(Object o1,Object o2){ String s1=(String)o1; String s2=(String)o2; return s1.length()-s2.length(); } } //returned comparator is serializable public static final Comparator STRING_LENGTH_COMPARATOR=new StrLenCmp(); } 读《Effective java 中文版》(24)
第23条:检查参数的有效性
当编写一个方法或者构造函数的时候,应该考虑对于它的参数有哪些限制,且应把这些限制写到文档中,然后在这个方法体的起始处,通过的检查来实施这些限制。 对于公有的方法,使用javadoc的@throws标签在文档中写下“一旦针对参数值的限制被违反之后将会抛出的异常”,如IllegalArgumentException、IndexOutOfBoundsException或NullPointerException。
非公有的方法,通常应该使用assertions断言来检查它们的参数。 一些情况下可以不进行参数限制,如:
读《Effective java 中文版》(25)
第24条:需要时使用保护性拷贝
从安全的角度考虑,应该设计面对客户的不良行为时仍能保持健壮性的类,无论系统的其它部分发生什么事情,这些类的约束都可以保持为真。 下面是一个有问题的例子:
//Broken "immutable" time period class public final class Period{ private final Date start; private final Date end; /** *@param start the begining of the period *@param end the end of the period *@throws IllegalArgumentException if start is after end *@throws NullPointerException if start or end is null */ public Period(Date start,Date end){ if (start.compareTo(end)>0) throw new IllegalArgumentException(start+" after "+end); this.start=start; this.end =end; } public Date start(){ return start; } public Date end(){ return end; } ....//remainder omitted } 由于Date本身可变,所以约束条件很容易被打破,如下: //attack the internals of a Period instance Date start=new Date(); Date end=new Date(); Period p=new Period(start,end); end.setYear(78);//modify the internals of P!! 为保护Period实例的内部信息免受攻击,对构造函数的每个参数进行保护性拷贝是必要的。而且,保护性拷贝要在检查参数的有效性之前进行,并且有效性检查是针对拷贝后的对象而不是原始的对象,以防止其它的线程会修改原始值。如下: //repared constructor- make defensive copy of parameters. public Period(Date start,Date end){ this.start=new Date(start.getTime()); this.end=new Date(end.getTime()); if(this.start.compareTo(this.end)>0) throw new IllegalArgumentException(start+" after "+end); } 注意,如果一个类可以被子类化,则不要用clone方法进行参数的保护性拷贝。 到目前为止,虽然对Period类进行了保护,但对它的攻击还是有可能的。如下:
//second attack on the internals of a Period instance. Date start=new Date(); Date end=new Date(); Period p=new Period(start,end); p.end().setYear(78);//modifis the internal of p!! 此时要对相关的方法进行修改,要求它们返回的是内部域的保护性拷贝即可,如下: //repaired accessors-make defensive copies of internal fields public Date start(){ return (Date)start.clone(); } public Date end(){ return (Date)end.clone(); } 至此,“一个周期的起始时间不能落后于结束时间”的约束,才真正做的到。 只要有可能,都使用非可变的对象做为对象内部的组件,这样就不必关心保护性拷贝的问题。
读《Effective java 中文版》(26)
第25条:谨慎地设计方法的原型
<!--[if !supportLists]-->1. <!--[endif]-->谨慎选择方法的名字
<!--[if !supportLists]-->2. <!--[endif]-->不要过于追求提供便利的方法。
只有当一个方法被用的非常频繁的时候,才考虑为它提供一个快捷方法。如果不能确定,还是不考虑为好。 <!--[if !supportLists]-->3. <!--[endif]-->避免长长的参数列表。
三个参数应为实践中的最大值。有两项技术可以缩短方法的参数列表: <!--[if !supportLists]-->o <!--[endif]-->把一个方法分解成多个方法,每个方法只要求这些参数一个子集。
<!--[if !supportLists]-->o <!--[endif]-->创建辅助类,来保存参数的聚集
<!--[if !supportLists]-->4. <!--[endif]-->对于参数类型,优先使用接口而不是类
例如,应该使用Map而不是Hashtable作为参数类型。 <!--[if !supportLists]-->5. <!--[endif]-->谨慎地使用函数对象
读《Effective java 中文版》(27)
第26条:谨慎地使用重载
下面的例子希望能识别出实例的类型分别为Set, List, 和unkown:
//broken - incorrect use of overloading. public class CollectionClassifier{ public static String classify(Set s){ return "Set"; } public static String classify(List l){ return "List"; } public static String classify(Collection c){ return "Unkown collection"; } public static void main(String[] args){ Collection[] tests=new Collection[]{ new HashSet(), new ArrayList(); new HashMap().values()}; for(int i=0;i<TESTS.LENGTH;I++){ System.out.println(classify(tests[i])); } } } 显示结果却是unkown,unkown,unkown,:(,虽然常识是“重载方法的选择是静态的,改写方法的选择是动态的”。 下面是重写的一个例子: class A{ String name(){return "A";} } class B extends A{ String name(){return "B";} } class C extends B{ String name(){return "C";} } public class Overriding{ public static void main(String[] args){ A[] tests=new A[]{new A(), new B(), new C()}; for(int i=0;i<TESTS.LENGTH;I++){SYSTEM.OUT.PRINTLN(TESTS[I].NAME());} } } 打印结果当然是"ABC"了。 因为改写机制是规范,而重载机制是例外,所以改写机制满足了人们对于方法调用的行为的期望,而重载机制很容易混淆这些期望。以避免重载机制的混淆用法的方法有
读《Effective java 中文版》(28)
第27条:返回零长度的数组而不是null
看例子: private List cheeseInStock=...; /** * @return an array containing all of the cheese in the shop, * or null if no cheeses are available for purchase. */ public Cheese[] getCheeses(){ if( cheeseInStock.size==0) return null; ... } 调用这个方法时,需要: Cheese[] cheeses=shop.getCheeses(); if(cheeses!=null && Arrays.asList(shop.getCheeses()).contains(Cheese.STILTON)) System.out.println("Jolly good, just the thing"); 如果改用返回长度为0的数组,则是这样:
private List cheesesInStock=...;
private final static Cheese[] NULL_CHEESE_ARRAY=new Cheese[0]; /** * @return an array containing all of the cheeses in the shop. */ public Cheese[] getCheese(){ return (Cheese[])cheeseInStock.toArray(NULL_CHEESE_ARRAY); } 调用代码改为: if(Arrays.asList(shop.getCheeses()).contains(Cheese.STILTON)) System.out.println("Jolly good, just the thing"); 读《Effective java 中文版》(29)
第28条:为所有导出的API元素编写文档注释
为了正确地编写API文档,必须在每一个被导出的类、接口、构造函数、方法和域声明之前增加一个文档注释。
具体编写时的一些注意事项:
从1.2.2开始,javadoc已经有“自动重用”或“继承”方法注释的能力了。 读《Effective java 中文版》(30)
第29条:将局部变量的作用域最小化
应该打破C语言设计的一个习惯:局部变量须被声明在代码块的开始处。java语言允许在任何可以出现语句的地方声明变量。 使一个局部变量的作用域最小化,最有力的技术是在第一次使用它的地方声明。几乎每一个局部变量的声明都应包含一个初始化表达式,如果没有足够的信息进行初始化,则应该推迟这个声明,try-catch是个例外。
最小化局部变量作用域的另一项技术是方法小而功能单一。 最常见的局部变量作用最小化的例子是for循环,相对于while循环,除了前者可以少一行代码外,前者可以避免“复制-粘贴”类错误。如下例: Iterator i=c.iterator(); while(i.hasNext()){ doSomething(i.next()); } Iterator i2=c2.iterator(); while(i.hasNext()){ //BUG!!! doSomething(i2.next()); } ------------------------------------- for (Iterator i=c.iterator();i.hasNext();){ doSomething(i.next()); } for (Iterator i2=c2.iterator();i.hasNext();){//Compile-time error. doSomething(i2.next()); } 读《Effective java 中文版》(31)
第30条:了解和使用库
不要从头发明轮子。 使用的标准库的益处: <!--[if !supportLists]-->1. <!--[endif]-->通过使用标准库,可以充分利用这些编写标准库的专家的知识,以及其它人的使用经验。
<!--[if !supportLists]-->2. <!--[endif]-->不必浪费时间为那些与工作关系不大的问题提供特别的解决方案。精力应该在应用上,而不是在底层的细节上。
<!--[if !supportLists]-->3. <!--[endif]-->它的性能会随着版本更新不断提高,而你不必须为它做任何工作。
<!--[if !supportLists]-->4. <!--[endif]-->使自己的代码融入主流。
每一个程序员都应该熟悉java.lang和java.util,以及java.io中的内容。如集合框架(Collections Framework),它以6个集合接口Colection, Set, List, Map, SortedSet, SortedMap为基础, 以前的Vector, HashTable被更新以后也加入到这里。另外,java.util.regex(正则式工具), java.util.prefs(配置信息管理工具), java.nio(高性能的io工具), java.util.LinkedHashSet/LinkedHashMap/IdentityHashMap(新的集合实现)也都值得特别关注。
读《Effective java 中文版》(32)
第31条:如果要求精确的答案,请避免使用float和double.
float和double类型的主要设计目标是为了科学计算和工程计算,采用二进制浮点运算,不适于要求精确结果的场合,float与dobule尤其不合适于货币计算。 对于货币计算的解决方案是: <!--[if !supportLists]-->1. <!--[endif]-->采用int类型,自己处理小数位,数值不超过9位
<!--[if !supportLists]-->2. <!--[endif]-->采用long类型,自己处理小数位,数值不超过18位
<!--[if !supportLists]-->3. <!--[endif]-->采用BigDecimal类型,不是很方便且运算慢,但可以从8种舍入方式中进行选择以灵活控制
读《Effective java 中文版》(33)
第32条:如果其它类型晚适合,则尽量避免使用字符串
字符器不适合代替其它的值类型:
读《Effective java 中文版》(34)
第33条:了解字符串连接的性能
为连接n个字符串而重复地使用字符串连接操作符,将需要n的平方级的时间。这是由于字符器是非可变的(见第13条),当两个字符串被连接的时候,它们的内容都要被拷贝。为了获得可接受的性能,使用StringBuffer替代String. 下面的例子被优化后,性能是优化前的数十倍。
public String statement(){ String s=""; for (int i=0;i<NUMITEMS();I++){ s += lineForItem(i); return s; } 优化后如下: public String statement(){ StringBuffer s=new StringBuffer(numItems()*LINE_WIDTH); for (int i=0;i<NUMITEMS();I++) s.append(lineForItem(i)); return s.toString(); } 读《Effective java 中文版》(35)
第34条:通过接口引用对象
第25条中建议过:应使用接口而不是类作为参数的类型。更进一步,应该优先使用接口而不是类来引用对象。只有当创建某个对象的时候,才需要引用这个对象的类。 这一条好象有点偏激,hehe. 例子:
//Good -uses interface as type List subscribers=new Vector(); //Bad - use class as type! Vector subscribers=new Vector(); 这么做的最大好处,就是使程序更加灵活。接着上面的例子,如果你的程序中使用的Vector的方法,ArrayList方法都有,且你现在由于某种原因更喜欢使用ArrayList,则只需要改动一行就可以了。即: Vecotr subscribers=new ArrayList(); 当然,也有一些情况,适合于用类来引用对象:
总之,如果有接口,则用接口来引用对象,如果没有则使用类层次结构中提供了所需功能的最高层的类。
读《Effective java 中文版》(36)
第35条:接口优先于映像机制
映像设施(reflection facility)java.lang.reflect,提供了通过程序来访问善于已装载的类的信息。映像机制允许一个类使用另一个类,即使当前者被编译时后者还根本不存在。映像设施最初是为了基于组件的应用创建工具而设计的。这样的工具通常要根据需要装载类,并且用映偈功能找出它们支持哪些方法和构造函数。映像功能是在设计时刻被使用的:通常,普通应用在运行时刻不应该以映像方式访问对象,因为使用映像是有代价的:
一些复杂的应用程序需要使用映像机制,如浏览器、对象监视器、代码分析工具、内嵌的解释器、RPC系统等。
如果某个类在程序编译时不可用,但其接口或超类可用,则可以用映像方式创建实例,然后通过它们的接口或超类,以正常的方式访问这些实例。如果存在构造函数没有参数,则只需要Class.newInstance()而无需reflection来创建实例。看例子: //Reflective instantiation with interface access public static void main(String[] args){ Class cl=null; try{ c1=Class.forName(args[0]); }catch(ClassNotFoundException e){ System.err.println("CLass not found"); System.exit(1); } Set s=null; try{ s=(Set)c1.newInstance(); }catch(IllegalAccessException e){ System.err.println("Class not Accessible"); System.exit(1); }catch(InstantiationException e){ System.err.println("Class not instantiable"); System.exit(1); } s.addAll(Arrays.asList(args)).subList(1,args.length-1)); System.out.println(s); } 如果第一个参数为java.util.HashSet则输出是随机顺序,如果是java.util.TreeSet则按字母排序输出。例子演示的技术,可以用来实现服务提供者框架(service provider framewrok)。在这个例子中,三个异常都是在不使用映像时的编译时错误,而且代码比较冗长。 读《Effective java 中文版》(37)
第36条:谨慎地使用本地方法
Java Native Interface允许java应用可以调用本地方法(用C/C++等本地程序语言来编写的特殊方法),用途有三:
使用本地方法也有一些缺点:
.
随着java的发展和优化,其三种用途大都有相应的替代方案。所以谨慎使用本地方法。 读《Effective java 中文版》(38)
第37条:谨慎地进行优化
不要费力地去编写快速的程序--应该努力编写好的程序,速度自然会随之而来。在设计系统的时候,特别是在设计API、线路层协议和永久数据格式的时候,要考虑性能的因素。在每次试图做优化之前和之后,要借助性能分析工具对性能进行分析。 考虑API设计决定的性能后果。如使一个公有的类型成为可变的,则可能会导致大量的保护性拷贝(参见第24条);该用复合模式时使用了类继承,人为地将子类和超类绑在了一起(参见第14条);在API中使用实现类型则不是接口,会把应用束缚在一个具体的实现上(参见第34条)等。一般而言,好的API设计也伴随着好的性能。 读《Effective java 中文版》(39)
第38条:遵守普遍接受的命名惯例
java的命名惯例分为两大类:字面的和语法的。 字面命名惯例涉及包、类、接口、方法和域。
语法命名惯例比字面惯例更灵活。
读《Effective java 中文版》(40)
第39条:只针对不正常的条件才使用异常
异常只应该被用于不正常的条件,它们永远不应被用于正常的控制流。 下面是一个用异常作遍历结束条件的滥用异常的例子:
//horrible abuse of exceptions. Don‘t ever do this! try{ int i=0; while(true)a[i++].f(); }catch(ArrayIndexOutOfBoundsException e){ } 其错有三: <!--[if !supportLists]-->1. <!--[endif]-->创建、抛出和捕获异常的开销是很昂贵的。因为它的初衷是用于不正常的情形,少有jvm会它进行性能优化。
<!--[if !supportLists]-->2. <!--[endif]-->把代码放在try-catch中会阻止jvm实现本来可能要执行的某些特定的优化。
<!--[if !supportLists]-->3. <!--[endif]-->有些现代的jvm对循环进行优化,不会出现冗余的检查。
这条原则也适用于API设计。一个设计良好的API不应该强迫它的客户为了正常的控制流而使用异常。如果类中有一个”状态相关”的方法,即只有特定的条件下可被调用的方法,则这个类也应有一个单独的“状态测试”方法,以为调用这个状态相关方法前的检查。如Collection类的next方法和hasNext方法。 Posted by Hilton at March 7, 2004 05:58 PM | TrackBack
Comments
我倒是想和大家讨论一下,如何处理函数返回值的问题。 如果一个函数执行错误了,一种方法可以通过int返回各种错误。 二可以通过异常,将错误丢出。
想知道大家都是怎么处理的,书中显然是不推崇第二种做法,但我却觉得这么做可以简化程序。
读《Effective java 中文版》(41)
第40条:三种可抛出结构的使用
对于可恢复的条件,使用被检查的异常;对于程序错误,使用运行时异常;错误往往被JVM保留用于指示资源不足、约束失败、或其它使程序无法继续执行的条件。 对于一个方法声明要抛出的每一个被检查的异常,它是对API用户的一种潜在指示:与异常相关联的条件是调用这次个方法的一种可能结果。
两种未被检查的可抛出结构:运行时异常和错误,在行为上相同的,它们都不需要、也不应该被捕获的抛出物。你所实现的所有未被检查的抛出结构都应是RuntimeException的子类。定义一个非Exception、RuntimeException或Error子类的抛出物是可行的,但从行为意义上它等同于被普通的被检查异常(即Exception子类而非RuntimeException子类). 异常是个完全意义上的对象,在其上可以定义任意的方法。因被检查的异常往往指示了可恢复的条件,所以可通过定义方法,使调用者可获得一些有助于恢复的信息。 读《Effective java 中文版》(42)
第41条:避免不必要地使用被检查的异常
与返回代码不同,被检查的异常强迫程序处理例外的情况,从而大大地提高了程序的可靠性。而过分地使用被检查的异常,则增加了不可忽视的负担。如果正确地使用API并不能阻止这种异常条件的产生,并且一旦产生了异常,使用API的程序可以采取有用的动作,那么这种负担被认为是正当的。 }catch(TheCheckedException e){
e.printStackTree(); System.exit(1); } 如果使用API的程序员无法做得比这更好,那么未被检查的异常可能更为合适。在实践中,catch几乎总有断言失败的特征。 “把被检查的异常变成未被检查的异常”的一种技术是,把这个要抛出异常的方法分成两个方法,第一个方法返回一个boolean以指明是否要抛出异常,另一个执行真正的功能,如果条件不满足就抛异常。如下: //Invocation with checked exception try{ obj.action(args); }catch(TheCheckedException e){ //Handle exception condition } 转换为: //Invocation with state-testing method and unchecked exception if(obj.actionPermitted(args)){ obj.action(args)); }else{ //handle exception condition } 当然这种转换并不总是合适的,例如一对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,那么这种转换将是不合适的。 读《Effective java 中文版》(43)
第42条:尽量使用标准的异常
java平台库中讫今为止最常被重用的异常如下:
其它的异常也可以使用,只要确保抛出异常的条件与文档要求一致即可。 读《Effective java 中文版》(44)
第43条:抛出的异常要适合于相应的抽象
高层的实现,应该捕获低层的异常,同时抛出一个可以按照高层抽象进行解释的异常,这种做法叫做异常转译(exception translation)。即如: //exception translation! try{ //use lowlevel abstraction to do our bidding ... }catch(LowerLevelException e){ throw new HigherLevelException(...); } 低层的异常被高层的异常保存起来,且高层的异常提供一个公有的访问方法来获得低层的异常,这种做叫做异常链接(exception chaining)。 //Exception chaining. try{ //use lower-level abstraction to do our bindding ... }catch(LowerLevelException e){ throw new HigherLevelException(e); } 异常链的实现非常简单,在1.4及以后版本中,可以通过Throwable来获得支持。
//Exception chaining in release 1.4 or later HigherLevelException(Throwable t){ super(t); } 如果是在早期java版本中,则需要先将其保存: //Exception chaining prior to release 1.4 private Throwable cause; HigherLevelException(Throwable t){ cause=t; } public Throwable getCause(){ return cause; } 处理来自低层的异常, <!--[if !supportLists]-->1. <!--[endif]-->最好的做法是,在调用低层方法之前通过一些检查等手段来确保它们会成功执行;
<!--[if !supportLists]-->2. <!--[endif]-->其次的做法是,让高层处理这些异常,从而将高层方法的调用者与低层的问题隔离开;
<!--[if !supportLists]-->3. <!--[endif]-->一般的做法是使用异常转译;
<!--[if !supportLists]-->4. <!--[endif]-->如果低层方法的异常对高层也是合适的,则将其从低层传到高层。
读《Effective java 中文版》(48)
第47条:不要忽略异常
异常的目的是强迫你处理不正常的条件,空的catch块会使异常达不到应有的目的,至少catch块中也应包含一条说明,用来解释为什么忽略掉这个异常。这对被检查的异常和未被检查的异常都适用。 简单地将一个未被检查的异常传播到外界至少会使程序迅速地失败,从而保留了有助于调试该失败条件信息,比异常被忽略后的一个不可预测的时刻程序失败这种情况要强。 读《Effective java 中文版》(49)
第48条:对共享可变数据的同步访问
同步,不仅可以阻止一个线程看到对象处于不一致的状态中,它还可以保证通过一系列看似顺序执行的状态转变序列,对象从一种一致的状态变迁到另一种一致的状态。 synchronized关键字可以保证在同一时刻,只有一个线程在执行一条语句,或者一段代码块。java语言保证读或写一个变量是原子的,除非这个变量的类型是long或double. java的内存模型决定,为了在线程之间可靠地通信,以及为了互斥访问,对原子数据的读写进行同步是需要的。看一个可怕的例子:
//Broken - require synchronization! private static int nextSerialNumber=0; public static int generateSerialNumber(){ return nextSerialNumber++; } 对其改进,只需要在generateSerialNumber的声明中增加synchronized修饰符即可。 为了终止一个线程,一种推荐的做法是让线程轮询某个域,该域的值如果发生变化,就表明此线程就应该终止自己。下面的例子就是这个思路,但在同步出了问题。 //Broken - requires synchronization public class StoppableThread extends Thread{ private boolean stopRequested=false; public void run(){ boolean done=false; while(!stopRequested && !done){ ...//do what needs to be done in the thread } } public void requestStop(){ stopRequested=true; } } 对其改进如下: //Properly synchronized cooperative thread temination public class StoppableThread extends Thread{ private boolean stopRequested=false; public void run(){ boolean done=false; while(!stopRequested() && !done){ ...//do what needs to be done in the thread } } public synchronized void requestStop(){ stopRequested=true; } private synchronized boolean stopRequested(){ return stopRequested; } } 另一种改进是,将stopRequested声明为volatile,则同步可以省略。 再来看迟缓初始化(lazy initialization)问题,双重访问模式并不一定都能正常工作,除非被共享的变量包含一个原语值。看例子:
//The double-check idion fro lazy initialization - broken! private static Foo foo=null; public static Foo getFoo(){ if (foo==null){ synchronized(Foo.class){ if(foo==null)foo=new Foo(); } } return foo; } 最容易的修改是省去迟缓初始化: //normal static initialization (not lazy) private static finall Foo foo=new Foo(); public static Foo getFoo(){ return foo; } 或者使用正确的同步方法,但可能增加少许的同步开销: //properly synchronized lazy initialization private static Foo foo=null; public static synchronized Foo getFoo(){ if(foo==null)foo=new Foo(); return foo; } 按需初始化容器模式也不错,但是它只能用于静态域,不能用于实例域。 //The initialize-on-demand holder class idiom private static class FooHolder(){ static final Foo foo=new Foo(); } public static Foo getFoo(){ return FooHolder.foo;} 简而言之,无论何时当多个线程共享可变数据的时候,每个读或写数据的线程必须获得一把锁。如果没有同步,则一个线程所做的修改就无法保证被另一个线程所观察到。 读《Effective java 中文版》(52)
第51条:不要依赖于线程调度器
不能让应用程序的正确性依赖于线程调度器。否则,结果得到的应用程序既不健壮也不具有可移植性。作为一个推论,不要依赖Thread.yield或者线程优先级。这些设施都只是影响到调度器,它们可以被用来提高一个已经能够正常工作的系统的服务质量,但永远不应用来“修正”一个原本并不能工作的程序。 编写健壮的、响应良好的、可移植的多线程应用程序的最好办法是,尽可能确保在任何给定时刻只有少量的可运行线程。这种办法采用的主要技术是,让每个线程做少量的工作,然后使用Object.Wait等待某个条件发生,或者使用Thread.sleep睡眠一段时间。 读《Effective java 中文版》(53)
第52条:线程安全性的文档化
每个类都应该清楚地在文档中说明它的线程安全属性。在一个方法的声明中出现synchronized修饰符,这是一个实现细节,并不是导出的API文档的一部分。 一个类为了可被多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全性级别。
对于有条件的线程安全类,在文档中指明“为了允许方法调用序列以原子方式执行,哪一个对象应被锁住”. 读《Effective java 中文版》(54)
第53条:避免使用线程组
除了线程、锁和监视器之外,线程系统还提供了一个基本的抽象,即线程组(thread-group)。然而线程组并没有提供太多有用的功能。 一个例外是,当线程组中的一个线程抛出一个未被捕获的异常时,ThreadGroup.uncaughtException方法会被自动调用。“执行环境”使用这个方法,以便用适当的方式来响应未被捕获的异常。 读《Effective java 中文版》(57)
第56条:保护性地编写readObject方法
编写一个类的readObject方法,相当于编写一个公有的构造函数,无论给它传递一个什么样的字节流,它都必须产生一个有效的实例。下面是缩写健壮的readObject方法的指导原则:
readResolve方法有可能取被用来替代保护性的readObject方法。 不严格地说,readObject是一个“用字节流作为唯一参数”的构造函数。当面对一个人工伪造的字节流的时候,readObject产生的对象会违反它所属的类的约束条件。初步的方法,是在readObject方法进行约束性检查,如下例:
private void readObject(OjbectInputStream s) throws IOException, ClassNotFoundException{ s.defaultReadObject(); //Check that our invariants are satisfied
if(start.compareTo(end)>0) throw new InvalidObjectException(start+" after "+ end); } 对上述的防范仍可进行攻击:伪造一个字节流,这个字节流以一个有效的Period实例所产生的字节流作为开始,然后附加上两个额外的引用,指向Period实例中的两个内部私有Date域,攻击者通过引用攻击内部域。所以,当一个对象被反序列化的时候,对于客户不应该拥有的对象引用,如果哪个域包含了这样的对象引用,则必须要做保护性拷贝,这是非常重要的。如下例: private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException{ s.defaultReadObject(); start=new Date(start.getTime()); end=new Date(end.getTime()); if(start.compareTo(end)>0) throw new InvlaidObjectException(start+" after "+end); } 读《Effective java 中文版》(58)
第57条:必要时提供一个readResolve方法
无论是singleton,或是其他实例受控(instance-controlled)的类,必须使用readResolve方法来保护“实例-控制的约束”。从本质上来讲,readResovle方法把一个readObject方法从一个事实上的公有构造函数变成一个事实上的公有静态工厂。对于那些禁止包外继承的类而言,readResolve方法作为保护性的readObject方法的一种替代,也是非常有用的。 如下sigleton类:
public class Elvis{ public static final Elvis INSTANCE = new Elvis(); private Elvis(){ ... } ...//remainder omitted } 如果Elvis实例序列化接口,则下面的readResolve方法足以保证它的sigleton属性。 private Object readResolve() throws ObjectStreamException{ //return the one true elvis and let the GC take care of the Elvis impersonator return INSTANCE; } 不仅仅对于singleton对象是必要的,readResolve方法对于所有其它的实例受控类(如类型安全枚举类型)也是必需的。 readResolve方法的第二个用法是,就像在第56条建议的那样,作为保护性的readObject方法的一种保守的替代选择。此时,第56条中的readObject方法可以下例的例子替代: //the defensive readResolve idiom private Object readResolve() throws ObjectStreamException(){ return new Period(start,end); } 对于那些允许继承的类,readResolve方法可能无法替代保护性的readObject方法。如果超类的readResolve方法是final的,则使得子类实例无法被正常地反序列化。如果超类的readResolve方法是可改写的,则恶意的子类可能会用一个方法改写它,该方法返回一个受损的实例。 |
|
来自: bluecrystal > 《java》