一、检查参数的有效性

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

对于公有的方法,使用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. J-link V8固件升级记

    http://blog.sina.com.cn/s/blog_5bdee3020101khfy.html 好久没为电子工程事业尽份力了!今天也稍微努把力!写写我是如何升级J-link的固件的吧! J- ...

  2. mysql主从配置(转载)

    原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://369369.blog.51cto.com/319630/790921 还可以参考 ...

  3. 谈谈MVC模式

    谈谈MVC模式   作者: 阮一峰 1. 如何设计一个程序的结构,这是一门专门的学问,叫做"架构模式"(architectural pattern),属于编程的方法论. MVC模式 ...

  4. JavaWeb笔记——利用过滤器实现页面静态化

    1.说明 页面静态化是把动态页面生成的html保存到服务器的文件上,然后再有相同请求时,不再去执行动态页面,而是直接给用户响应上次已经生成的静态页面. * 核心思路为拦截请求,实现请求转发指向静态页面 ...

  5. C++:异常的处理

    6.4 异常处理 程序中常见的错误分为两大类:编译时期的错误和运行时期的错误. 编译时期的错误比较简单容易发现:主要是语法错误,如关键字拼写错误.缺分号.括号不匹配等 运行时期的错误比较难发现,甚至是 ...

  6. uploadify+批量上传文件+java

    <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding= ...

  7. linux网络相关

    ethtool:http://www.ibm.com/developerworks/cn/linux/1304_wangjy_ethtools/ 网卡特性:http://blog.chinaunix. ...

  8. mysql中的连接

    SQL join 用于根据两个或多个表中的列之间的关系,从这些表中查询数据. join可以分为内连接和外连接,外连接分为左连接.右连接和全连接 现有两个表 员工表和部门表 员工表 部门表 1.内连接( ...

  9. matlab 扩大虚拟内存

    今天服务器挂了..用了自己电脑结果爆内存,分享一个扩大虚拟内存的方法,经测试有效.. 使用Matlab生成很大的图片时,碰到了"out of memory"的错误,导致图片无法生成 ...

  10. Java-HTTP连接时如何使用代理(二)—— Proxy类方式

    阅读这篇文章之前,请先阅读 Java-HTTP连接时如何使用代理(一)——System.Property方式 除了使用 System.setProperty() 的方式之外,还可使用 Proxy 的方 ...