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. Independent Components Analysis:独立成分分析

    一.引言 ICA主要用于解决盲源分离问题.需要假设源信号之间是统计独立的.而在实际问题中,独立性假设基本是合理的. 二.随机变量独立性的概念 对于任意两个随机变量X和Y,如果从Y中得不到任何关于X的信 ...

  2. 突破瓶颈,对比学习:Eclipse开发环境与VS开发环境的调试对比

    曾经看了不少Java和Android的相关知识,不过光看不练易失忆,所以,还是写点文字,除了加强下记忆,也证明我曾经学过~~~ 突破瓶颈,对比学习: 学习一门语言,开发环境很重,对于VS的方形线条开发 ...

  3. Javascript判断两个日期是否相等

    大家一定遇到过这样的情况,有两个日期对象,然后需要判断他们是否相等. 例如: var date1 = new Date("2013-11-29"); var date2 = new ...

  4. 作业二:个人编程项目——编写一个能自动生成小学四则运算题目的程序

    1. 编写一个能自动生成小学四则运算题目的程序.(10分)   基本要求: 除了整数以外,还能支持真分数的四则运算. 对实现的功能进行描述,并且对实现结果要求截图.   本题发一篇随笔,内容包括: 题 ...

  5. TCP状态

    TCP状态 TCP连接中包含不同的状态,如何通过状态来判断程序问题尤为重要. 三次握手 图中的connection部分为三次握手. 四次握手 图中的close部分为四次握手. CLOSE_WAIT 服 ...

  6. JavaScript状态机程序逻辑编辑器

    制作背景 之前做Win8 Metro动态加载内容框架的时候,由于采用了XAML+JavaScript的方法,程序复杂的执行逻辑是由JavaScript控制的,而页面一多,流程一复杂,制作起来就非常麻烦 ...

  7. iOS block种类和切换

    block 分为三种 NSGlobalBlock,NSStackBlock, NSMallocBlock. NSGlobalBlock:类似函数,位于text段: NSStackBlock:位于栈内存 ...

  8. CSharpGL(5)解析3DS文件并用CSharpGL渲染

    CSharpGL(5)解析3DS文件并用CSharpGL渲染 我曾经写过一个简单的*.3ds文件的解析器,但是只能解析最基本的顶点.索引信息,且此解析器是仿照别人的C++代码改写的,设计的也不好,不方 ...

  9. Fiddler调式使用知多少(一)深入研究

    Fiddler调式使用(一)深入研究 阅读目录 Fiddler的基本概念 如何安装Fiddler 了解下Fiddler用户界面 理解不同图标和颜色的含义 web session的常用的快捷键 了解we ...

  10. Sql Server系列:Update语句

    1 UPDATE语法结构 [ WITH <common_table_expression> [...n] ] UPDATE [ TOP ( expression ) [ PERCENT ] ...