38、检查参数的有效性

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

  • 对于公有的方法,使用异常检查参数,并在Javadoc的@throws标签中说明违反参数限制时会抛出的异常。
  • 对于非公有的方法,使用断言来检查参数。断言如果失败,将会抛出AssertionError。若它们没起作用,本质上不会有成本开销。

断言仅用于代码调试,不要在公有的API方法中使用断言,因为断言默认是关闭的。

例如:

/**
* @param m the modulus, which must be positive
* @return this mod m
* @throws ArithmeticException if m is less than or equal to 0
*/
public BigInteger mod(BigInteger m) {
if(m.signum() <= 0) {
throw new ArithmeticException("Modulus <= 0: " + m)
}
....
} //递归排序的帮助类
private static void sort(long a[], int offset, int length) {
assert a != null;
assert offset >= 0 && offset <= a.length;
assert length >= 0 && length <= a.length - offset;
....
}

39、必要时进行保护性拷贝

虽然java是一门安全的语言,它能对缓冲区溢出、数组越界、非法指针以及它的内存错误自动免疫,但保护性的设计程序是很有必要的。

例如,设计一个类,它表示一段不可变的时间周期


public final class Period {
private final Date start;
private final Date end; 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 getStart() {
return start;
} public Date getEnd() {
return end;
}
}

这个类似乎是不可变的,并且强加了约束:起始时间(start)必须小于等于结束时间(end)。然而,由于Date类本身是可变的,并且构造函数中传递的是对象的引用,因此有可能违反这个约束条件。如:

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end); end.setTime(1000);
assert p.getStart().compareTo(p.getEnd()) <= 0; //报错

因此对于构造器的每个可变参数进行保护性拷贝是必要的,并且使用备份对象作为Period实例的组件。

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的访问方法提供了对其可变内部成员的访问能力,所以可用下面方法改变Period实例:

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end); p.getEnd().setTime(1000);
assert p.getStart().compareTo(p.getEnd()) <= 0; //报错

要解决这个问题,只需修改两个方法,使它返回可变内部域的保护性拷贝即可:

public Date getStart() {
return new Date(start.getTime());
} public Date getEnd() {
return new Date(end.getTime());
}

注意:长度非零的数组总是可变的,因此,把内部数组返回给客户端之前,必须进行保护性拷贝或给客户端返回该数组的不可变视图。方法见第13条,使类和成员的可访问性最小化


对于Period类,通常使用long基本类型作为内部时间表示法,而不是使用Date对象引用,主要是因为Date是可变的,而long是不可变的。

import java.util.*;

public final class Period {
private final long start;
private final long end; public Period(Date start, Date end) {
this.start = start.getTime();
this.end = end.getTime();
if(this.start > this.end)
throw new IllegalArgumentException(start + " after " + end);
} public Date getStart() {
return new Date(start);
} public Date getEnd() {
return new Date(end);
} public static void main(String[] args) {
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
//end.setTime(1000);
p.getEnd().setTime(1000); assert p.getStart().compareTo(p.getEnd()) <= 0;
}
}

总之,如果类具有从客户端得到或者返回到客户端的可变组件,类就必须保护性的拷贝这些组件。若拷贝的成本较高,并且类信任它的客户端不会修改组件,可以在文档中指明客户端不得修改这个组件,以此来代替保护性拷贝。

40、谨慎设计方法签名

  1. 谨慎的选择方法的名称。方法的名称应该始终遵循标准的命名习惯。
  2. 不要过于追求提供便利的方法。方法太多会使类难以学习、使用、文档化、测试和维护。
  3. 避免过长的参数列表。4个或者更少,杜绝相同类型的长参数列表。

缩减参数列表的方法:

  • 把方法分解为多个方法。
  • 创建辅助类,用来保存参数的分组。
  • 从对象构建到方法调用都采用Builder模式。特别是对于有些参数是可选的情况。

对于参数类型,要优先使用接口而不是类。

对于boolean参数,要优先使用两个元素的枚举类型,以便将来扩展。

41、慎用重载

对于重载方法的选择是静态的,而对于被覆盖的方法的选择则是动态的,即重载方法的选择在编译时决定,而覆盖方法的选择在运行时决定(多态,选择依据为被调用方法所在对象的运行时类型)。

例如:重载


import java.util.*;
public class OverloadTest { public static String mothod(Collection<?> col) {
return "unknown collection";
} public static String mothod(List<?> list) {
return "list";
} public static void main(String[] args) {
Collection<?>[] coll = {
new HashSet<String>(),
new ArrayList<String>()
};
for(Collection<?> c : coll) {
System.out.println(mothod(c)); //都调用mothod(Collection<?> col)方法
}
}
}

程序将打印两次"unknown collection",而没有打印"list"。因为程序调用哪个重载方法是在编译时确定的,在for循环中参数的编译时类型为Collection<?>,所以每次迭代都将调用mothod(Collection<?> col)方法。若期望编译器根据参数的运行时类型自动将调用分发给适当的重载方法(像方法覆盖一样),将导致错误。可以使用下面的方法实现这个功能:

public static String mothod(Collection<?> col) {
return col instanceof List ? "list" : unknown collection";
}

对于方法覆盖:


public class OverrideTest { public static void main(String[] args) {
Super[] supers = {
new Super(),
new SubClass()
};
for(Super s : supers) {
System.out.println(s.name());
}
}
} class Super {
String name() { return "super"; }
} class SubClass extends Super{
@Override
String name() { return "subClass";}
}

由于覆盖方法的调用时在运行时确定的,所以打印结果为"super subClass"。

注意:对于重载,jdk1.5后自动装箱可能会引起无意识的错误。例如:


List<Integer> list = new ArrayList<Integer>();
list.add(2); //调用add(E)
list.remove(2); //调用的是remove(int),不是remove(E),应该使用list.remove((Integer)2);

对于多个具有相同参数数目的方法,应尽量避免重载方法。同一组参数只需经过类型转换就可以被传递给不同的重载方法,这种情况应该被避免。若不能避免这种情况,就应该保证当传递同样的参数时,所有重载方法的行为必须一致(让具体的重载方法把调用转发给更一般的重载方法执行)。

42、慎用可变参数

可变参数机制先创建一个数组,然后将参数值传到数组中,最后将数组传递给方法。

有时要编写需要1个或多个(不是0个或多个)某种类型参数的方法,如,多个int参数的最小值

static int min(int ...args) {
if(args.length == 0) {
throw new IllegalArgumentException("Too few arguments");
}
int min = args[0];
for(int i=1; i< args.length; i++){
if(args[i] < min)
min = args[i];
}
}

这种方法的问题是,若调用这个方法时,没有传递参数,它就会在运行时而不是编译时失败,而且必须在方法内部进行有效性检查。

改进:

static int min(int firstArg, int ...remainingArgs) {
int min = firstArg;
for(int arg : remainingArgs){
if(arg < min)
min = arg;
}
}

可变参数的每次调用都会进行一次数组分配和初始化,这将影响程序性能。在定义参数数目不定的方法时,可变参数是一种很好的方式。但它们不应该被过度滥用。

43、返回零长度的数组或集合,而不是null

对于一个返回null而不是零长度数组或集合的方法,每次调用都需要额外的代码来处理null返回值。如:

private final List<String> stuNames = ....;

public String[] getNames() {
if(stuNames.size() == 0)
return null;
....
} //调用,
String[] names = getNames();
if(names != null) {
....
}

对于上面的代码,每次使用names前都要进行判断。这样很容易出错(由于忘记进行判断)。另外认为null返回值比零长度数组更好,因为它避免了分配数组所需要的开销,这是不对的,原因有两点:

  • 对于不返回任何元素的调用,因为零长度的数组是不可变的,所以可以每次都返回同一个零长度的数组。
  • 返回零长度数组的调用一般为常数级的,时间复杂度为O(1),所以对程序性能影响很小。

改进:

private final List<String> stuNames = ....;
private static final Cheese[] EMPTY_STUDENT_ARRAY = new String[0]; public String[] getNames() {
return stuNames.toArray(EMPTY_STUDENT_ARRAY); //若集合为空,将使用EMPTY_STUDENT_ARRAY数组
}

Collection.toArray(T[] arg):如果输入数组arg大到足以容纳这个集合,它就将返回这个输入数组。

同样的对于集合的改进:

public List<String> getNames() {
if(stuNames.size() == null)
return Collection.emptyList();
else
new ArrayList<String>(stuNames); //返回一个新的对象
}

总之,返回类型为数组或集合的方法,不要返回null,而应该返回一个零长度的数组或集合。

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

Javadoc利用特殊格式的文档注释,根据源代码自动产生API文档。为了正确的编写API文档,必须在每个被导出的类、接口、构造器、方法和域声明之前增加文档注释。若类是可序列化的,应该对它的序列化形式编写文档注释。

1、方法的文档注释应该简洁的描述出它和客户端之间的约定。除了专门为继承而设计的类外,这个约定应该说明这个方法做了什么,而不是说明它是如何完成这项工作的。文档注释应该列举出这个方法是所有前提条件和后置条件,副作用,线程安全性等。如:

/**
* Return the element at the specified position in this list
*
* <P>This method is <i>not</i> guaranteed to run in constant
* time. In some implementations it may run in time proportional
* to the element position.
*
* @param index index of element to return: must be non-negative 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
* ({@code index < 0 || index >= this.size()})
*/
E get(int index);

注:Javadoc工具会把文档注释翻译成HTML,文档中包含的HTML元素都会出现在HTML文档中。

{@code }标签用来标记代码片段,其中的代码不会被HTML文档转译。标记多行代码使用<pre>{@code 多行代码}</pre>{@literal }标签用来处理小于号(<)、大于号(>)、与(&)等特殊字符,使它们能在HTML文档中显示出来。如:

	//The triangle inequality is {@literal |x + y| < |x| + |y| }.

2、每个文档注释的第一句是该注释所属元素的概要描述,概要描述必须独立的描述目标元素的功能。同一个类或接口中的两个成员或方法,不应该具有同样的概要描述。(特别是重载的情况)

  • 对于方法和构造器而言,概要描述应该是完整的动词性短语。如:Collection.size()——Return the number of elements in this collection.
  • 对于类、接口或域,概要描述应该是一个名称性短语。如:TimerTask—— A task that can be scheduled for one-time or repeated execution by a Timer.
  • 当为泛型类或方法编写文档时,要在文档中说明所有的类型参数。如:
/**
* An object that maps keys to values. A map cannot contain
* duplicate keys; each key can map to at most on value.
*
* @param <K> the type of keys maintained by this map
* @param <V> the type of mapped values
*/
public interface Map<K,V> {
....
}
  • 为枚举类型编写文档时,要在文档中说明所有常量。
  • 为注解类型编写文档时,要在文档中说明所有成员。

Javadoc具有“继承”方法注释的能力。若API元素没有文档注释,接口的文档注释优于超类的文档注释被使用。

总之,对所有可导出的API元素,都应该强制性的使用文档注释。文档编写的具体规则可以参考官方文档 How to Write Doc Comments for the Javadoc Tool

Effective java笔记(六),方法的更多相关文章

  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]第六章 枚举和注解

    声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...

  7. Effective java笔记7--线程

    一.对可共享数据的同步访问 synchronized关键字可以保证在同一时刻,只有一个线程在执行一条语句,或者一段代码块.正确地使用同步可以保证其他任何方法都不会看到对象处于不一致的状态中,还能保证通 ...

  8. Effective java笔记6--异常

    充分发挥异常的优点,可以提高一个程序的可读性.可靠性和可维护性.如果使用不当的话,它们也会带来负面影响. 一.只针对不正常的条件才使用异常 先看一段代码: //Horrible abuse of ex ...

  9. Effective java笔记5--通用程序设计

    一.将局部变量的作用域最小化      本条目与前面(使类和成员的可访问能力最小化)本质上是类似的.将局部变量的作用域最小化,可以增加代码的可读性和可维护性,并降低出错的可能性. 使一个局部变量的作用 ...

随机推荐

  1. 详解Maple如何公式推导和生成代码

    公式推导 直观自然的数学表达式,智能的关联菜单,交互式助手等协助您从容通过推导过程,让您更容易地完成解决方案的开发,快速.无错! 分析 Maple 内置超过大量的计算函数,包括积分变换,微分方程求解器 ...

  2. IE7中使用Jquery动态操作name问题

    问题:IE7中无法使用Jquery动态操作页面元素的name属性. 在项目中有出现问题,某些客户的机器偶尔会有,后台取不到前台的数据值. 然开发和测试环境总是不能重现问题.坑爹之处就在于此,不能重现就 ...

  3. 打造程序员的高效生产力工具-mac篇

    打造程序员的高效生产力工具-mac篇 1   概述 古语有云:“工欲善其事,必先利其器” [1] ,作为一个程序员,他最重要的生产资源是脑力知识,最重要的生产工具是什么?电脑. 在进行重要的脑力成果输 ...

  4. LVS原理与使用(1)

    负载均衡,无论是否真正了解过,但我相信所有跟编程打交道的读者都有听说.同时,它(负载均衡)也是被认为一个大型网站的标识性技术之一(但负载均衡的作用肯定不止这点用途).虽然网上也有不少关于LVS配置实用 ...

  5. gulp使用小结(一)

    这篇文章不会介绍 gulp 的起源.发展:不会去一个个讲解 gulp API:也不想出现大段大段的 gulpfile.js 代码:更木有帮你分析 gulp 实现原理,只有一些我自己对 gulp 的使用 ...

  6. TODO:Linux安装PHP MongoDB驱动

    TODO:Linux安装PHP MongoDB驱动 PHP利于学习,使用广泛,主要适用于Web开发领域. MongoDB的主要目标是在键/值存储方式(提供了高性能和高度伸缩性)以及传统的RDBMS系统 ...

  7. 带你走近AngularJS - 基本功能介绍

    带你走近AngularJS系列: 带你走近AngularJS - 基本功能介绍 带你走近AngularJS - 体验指令实例 带你走近AngularJS - 创建自定义指令 ------------- ...

  8. Dynatree使用

    最近用到了Dynatree的树形结构,记录一下它的用法. 需求: 1.jquery.js 2.jquery-ui.custom.js 3.jquery.cookie.js 下载dynatree,放到资 ...

  9. 一个美术需求引发的Custom Inspector

    需求 Editor模式下,在运行或者非运行状态下,能够按照指定的变化率来自动改变material中属性数值. 需求分析 如何在Editor模式下获得一个游戏对象及其组件,尤其是在非运行状态下?我们知道 ...

  10. 在Thinkphp3.2.3框架下实现自动获取客户端IP地址的get_client_ip()函数

    在Thinkphp框架下使用get_client_ip()函数获取客户端IP地址十分方便: 一行代码便可以实现:$ip = get_client_ip(); 但当我们测试时会遇到后台获取的IP地址显示 ...