一、检查参数的有效性

极大多数方法和构造函数都会对于传递给它们的参数值有某些限制。

对于公有的方法,使用Javadoc @throws标签(tag)可以使文档中记录下“一旦针对参数值的限制被违反之后将会被抛出的异常”。典型情况下, 这样的异常为IllegalArgumentException、IndexOutOfBoundException或者NullPointException。看一个例子:

/**
* @param m the modulus,which must be positive.
* @return this mod m.
* @throws ArithmeticException if m<=0.
*/
public BigInteger mod(BigInteger m){
if(m.signum()<=0)
throw new ArithmeticException("Modulus not positive"); ...//Do the computation
}

二、需要时使用保护性拷贝

Java程序设计语言用起来如此愉悦的一个原因是,它是一门安全的语言(safe language)。这意味着无需专门手段,它对应缓冲区溢出、数组越界、非法指针以及其他的内存破坏错误自动免疫,而这些错误却困扰着诸如C和C++这样的不安全语言。

例如,下面是表达一段不可变的时间周期:

//Broken "immutable" time period class
public final class Period{
private final Date start;
private final Date end; /**
* @param start the beginning of the period.
* @param end the end of the period;must not precede start.
* @throws IllegalArgumentException if start is after end.
* @throws NullPointException if start or end is null.
*/
public Period(Date start, Date end){
if(start.compareTo(end) > )
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); //Modifies internals of p!

为了保护Period实例的内部信息避免受到这种攻击,对于构造函数的每个可变参数进行保护性拷贝(defensive copy)是必要的,并且使用拷贝之后的对象作为Period实例的组件,而不使用原始的对象。代码改写如下:

//Repaired constructor = makes defensive copies 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);
}

注意,保护性拷贝动作时在检查参数的有效性之前进行的,并且有效性检查时针对拷贝之后的对象,而不是原始的对象。虽然这样看起来有点不太自然,但这是必要的。这样做可以避免“脆弱性窗口”中另外一个线程会改变原始的参数对象,这里脆弱性窗口是指从参数检查开始,一直到参数对象被拷贝之间的一段时间窗。

//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);//modifies internals of p!

为了防御第二种攻击,只需修改这两个访问方法,使它返回可变内部域的保护性拷贝即可:

//Repaired accessors - make defensive copies of internal fields
public Date start(){
return (Date)start.clone();
}
public Date end(){
return (Date)end.clone();
}

采用了新的构造函数和新的访问方法之后,Period成为真正的非可变类。

三、谨慎设计方法的原型

谨慎选择方法的名字。方法的名字应该总是遵循标准的命名习惯。
不要过于追求提供便利的方法。
避免长长的参数列表。
通常,三个参数应该被看做实践中最大值,而且参数越少越好。类型相同的长参数序列尤其有害。当弄错了参数顺序的时候,他们的程序仍然可以编译和运行。
有两项技术可以缩短太长的参数列表。
a、把一个方法分解成多个方法,每一个方法只要求这些参数的一个子集。
b、缩短长参数列表的技术是创建辅助类(helper class),用来保存参数的聚集(aggregate),这些辅助类往往是静态成员类。
对于参数类型,优先使用接口而不是类。无论什么时候,只要存在可用来定义参数的适当接口,就优先使用这个接口,而不是实现该接口的类。
例如,没有理由在编写一个方法时,使用Hashtable作为输入,相反,应该使用Map。这使得你可以传入一个Hashtable、HashMap、TreeMap、TreeMap的子映射表(submap),或者任何有待于将来编写的Map实现。如果使用的是一个类而不是一个接口,则限制了只能传入一个特定的实现,如果碰巧输入的数据时以其他形式存在的话,则会导致不必要的、可能非常昂贵的拷贝操作。
谨慎的使用函数对象。 创建函数对象最容易的方法莫过于使用匿名类,但是这样会带来语法上的混乱。

四、谨慎地使用重载

下面的一个意图良好的集合分类器,根据一个集合(collection)是Set、List,或是其他的集合类型,对它进行分类:

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 "Unknown Collection";
}
public static void main(String args[]){
Collection[] tests = new Collection[]{
new HashSet(), //A set
new ArrayList(), //A arraylist
new HashMap().values() //neither set or list
};
for(int i=0;i<tests.length;i++){
System.out.println(classify(tests[i]));
}
}
}

结果:
Unknown Collection
Unknown Collection
Unknown Collection

结果为什么不是“Set”,“List”以及“Unknown Collection”呢?是因为classify方法被重载(overloading)了,而到底调用哪个重载(overloading)方法时编译时刻作出决定的。由于上面例子的for循环的全部三次迭代,参数编译时类型都是Collection,每次迭代的运行时类型是不同的,但这并不影响对重载方法的选择。因为该参数的编译时类型为Collection,所以,唯一合适的重载方法是第三个:classify(Collection),在循环的每次迭代中,都会调用这个重载方法。

这个程序的行为是违反了直觉的,因为对于重载方法(overloaded method)的选择是静态的,而对于被改写的方法(overridden method)的选择是动态的。对于被改写的方法,选择正确的版本是在运行时刻进行的,选择的依据是被调用方法所在对象的运行时类型。重写的方法是发生在子类继承时,当子类申明的方法与其父类具有相同的原型时。如下面的例子:

public class A {
String name()
{
return "A";
}
}
public class B extends A{
String name(){
return "B";
}
}
public class C extends A {
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());
}
}
}

结果:
A
B
C

一个安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法。

“你能够重载方法”并不意味着“你应该重载方法”。一般地,对于多个相同参数数目的方法来说,你应该尽量避免重载方法。在某些情况下,特别是涉及到构造函数的时候,遵循这条建议也许是不可能的。但至少应该避免这种情形:同一组参数只需经过类型转换就可以传递给不同的重载方法。

四、返回零长度的数组

像下面这样的方法并不少见:

public Cheese[] getCheeses(){
if(cheesesInStock.size()==0)
return null;
...
}

有观点认为,返回null比零长度数组更好,因为它避免了分配数组所需要的开销,这种观点是站不住脚的,原因有两点:
第一,在这个层次上担心性能问题是不明智的,除非分析表明这个方法正是造成性能问题的真正源头;
第二,对于不返回任何元素的调用,每次都返回同一个零长度数组是有可能的,因为零长度数组是非可变的,而非可变对象有可能被自由地共享。

五、为所有导出的API元素编写文档注释

Java语言环境提供了一个javadoc的实用工具,从而使编写API文档这项任务变得容易。这个工具可以根据源代码自动产生API文档,它利用了源代码中特殊格式的文档注释(documentation comment,通常被写作doc comment)。

为了正确地编写API文档,你必须在每一个被导出的类、接口、构造函数、方法和域声明之前增加一个文档注释。

每一个方法的文档注释应该简洁地描述出它和客户之间的约定。这个约定应该说明了这个方法做了什么,而不是说明它是如何完成这项工作的。文档注释应该列举出这个方法所有的前提条件(precondition)和后置条件(postcondition),所谓前提条件是指为了使客户能够调用这个方法,而必须要满足的条件;所谓后置条件是指在调用成功完成之后,哪些条件必须要满足。典型情况下,前提条件有@throws标签所隐含描述的;每一个未被检查的异常都对于着一个被违背的前提条件。同样地,你也可以在一些受影响的参数的@param标记中指定前提条件。

除了前提条件(precondition)和后置条件(postcondition)之外,还应该描述其副作用(side effect),所谓副作用是指系统状态中一个可观察的变化,它不是为了获得后置条件而要求的变化。例如,如果一个方法启动了一个后台线程,那么文档中应该说明这一点。

@throws标签之后的文字应该包含单词“if”(如果),紧接着实一个名称短语,它描述了这个异常将在什么样的条件下会被抛出来。偶尔情况下用算术表达式来代替名称短语。如下摘自List接口的文档注释演示了所有这些习惯:

/**
* Returns the element at the specified position in this list.
*
* @param index index of element to return;must be nonnegative and less than the size of this list.
* @return the element at the specified position in this list.
* @throws IndexOutOfBoundsException if the index is out of range
* /

文档注释格式:

第一句话是注释所属元素的概要描述(summary description)。概要描述必须独立地描述目标实体的功能。为了避免混淆,同一个类或者接口中,不应该存在两个成员或者构造函数具有同样地概要描述。特别要注意重载的情形,特别要注意重载的情形,在这种情况下,往往自然地在描述中使用同样地第一句话。

小心,在文档注释的第一句话内部不要包括句号。如果你包括了句号,则它会终止整个概要描述。例如,一个以“A college degree,such as B.S.,M.S.,or Ph.D"开头的文档注释,它的概要描述为”A college degree,such as B."避免这种问题最容易的方法是,在概要描述中不要使用缩写和十进制小时,然而,在概要描述中使用句号也是可能地,你只需用句号的数字编码形式(numeric encoding)“."来代替它,虽然这样做可以工作,但不会生成漂亮的源代码。

Effective java笔记4--方法的更多相关文章

  1. Effective Java笔记一 创建和销毁对象

    Effective Java笔记一 创建和销毁对象 第1条 考虑用静态工厂方法代替构造器 第2条 遇到多个构造器参数时要考虑用构建器 第3条 用私有构造器或者枚举类型强化Singleton属性 第4条 ...

  2. Effective java笔记(二),所有对象的通用方法

    Object类的所有非final方法(equals.hashCode.toString.clone.finalize)都要遵守通用约定(general contract),否则其它依赖于这些约定的类( ...

  3. effective java笔记之单例模式与序列化

    单例模式:"一个类有且仅有一个实例,并且自行实例化向整个系统提供." 单例模式实现方式有多种,例如懒汉模式(等用到时候再实例化),饿汉模式(类加载时就实例化)等,这里用饿汉模式方法 ...

  4. effective java笔记之java服务提供者框架

    博主是一名苦逼的大四实习生,现在java从业人员越来越多,面对的竞争越来越大,还没走出校园,就TM可能面临失业,而且对那些增删改查的业务毫无兴趣,于是决定提升自己,在实习期间的时间还是很充裕的,期间自 ...

  5. Effective java笔记(五),枚举和注解

    30.用enum代替int常量 枚举类型是指由一组固定的常量组成合法值的类型.在java没有引入枚举类型前,表示枚举类型的常用方法是声明一组不同的int常量,每个类型成员一个常量,这种方法称作int枚 ...

  6. Effective java笔记(六),方法

    38.检查参数的有效性 绝大多数方法和构造器对于传递给它们的参数值都会有限制.如,对象引用不能为null,数组索引有范围限制等.应该在文档中指明所有这些限制,并在方法的开头处检查参数,以强制施加这些限 ...

  7. Effective java笔记(一),创建与销毁对象

    1.考虑用静态工厂方法代替构造器 类的一个实例,通常使用类的公有的构造方法获取.也可以为类提供一个公有的静态工厂方法(不是设计模式中的工厂模式)来返回类的一个实例.例如: //将boolean类型转换 ...

  8. Effective java笔记(四),泛型

    泛型为集合提供了编译时类型检查. 23.不要在代码中使用原生态类型 声明中具有一个或多个类型参数的类或接口统称为泛型.List<E>是一个参数化类,表示元素类型为E的列表.为了提供兼容性, ...

  9. Effective java笔记(九),并发

    66.同步访问共享的可变数据 JVM对不大于32位的基本类型的操作都是原子操作,所以读取一个非long或double类型的变量,可以保证返回的值是某个线程保存在该变量中的,但它并不能保证一个线程写入的 ...

  10. Effective java笔记(八),异常

    57.只针对异常的情况才使用异常 try { int i = 0; while(true) range[i++].climb(); }catch(ArrayIndexOutOfBoundsExcept ...

随机推荐

  1. Android消息机制之实现两个不同线程之间相互传递数据相互调用

    目的:实现两个不同线程之间相互传递数据相互调用方法. 线程一中定义mainHandler 并定义一个方法mainDecode 线程二中定义twoHandler 并定义一个方法twoEncode 实现当 ...

  2. SGU 149 Computer Network 树DP/求每个节点最远端长度

    一个比较经典的题型,两次DFS求树上每个点的最远端距离. 参考这里:http://hi.baidu.com/oi_pkqs90/item/914e951c41e7d0ccbf904252 dp[i][ ...

  3. CListCtrl使用方法汇总

    回顾: 刚刚写完,因为是分期写的,所以最初想好好做一下的文章格式半途而废了~说的也许会有点啰嗦,但是所有的基础用到的技术细节应该都用到了. 如果还有什么疑问,请回复留言,我会尽力解答. 如果有错误,请 ...

  4. C语言ASM汇编内嵌语法【转】

    转自:http://www.cnblogs.com/latifrons/archive/2009/09/17/1568198.html GCC 支持在C/C++代码中嵌入汇编代码,这些汇编代码被称作G ...

  5. Ubuntu 14.04怎样升级到Ubuntu 14.10

    Ubuntu 14.04怎样升级到Ubuntu 14.10     Ubuntu 14.10 Utopic Unicorn 将在10月23日正式发布,9月25日最终测试版本已经发布,Ubuntu 14 ...

  6. Android gingerbread eMMC booting

    Android gingerbread eMMC booting This page is currently under construction. The content of this page ...

  7. Perl文件读写

    Perl File Handling: open, read, write and close files #==================== Opening files Solution 1 ...

  8. 《OD大数据实战》HBase环境搭建

    一.环境搭建 1. 下载 hbase-0.98.6-cdh5.3.6.tar.gz 2. 解压 tar -zxvf hbase-0.98.6-cdh5.3.6.tar.gz -C /opt/modul ...

  9. hdu 1520 Anniversary party || codevs 1380 树形dp

    Anniversary party Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others ...

  10. 简单了解JAVA8的新特性

    JAVA8新特性会颠覆整个JAVA程序员的编程习惯 甚至如果您坚守JAVA7之前的编程习惯,今后你看比较年轻的程序员写的JAVA代码都会无法理解 所以为了保证不脱钩,我觉得有必要学习JAVA8的新特性 ...