本章讨论方法(构造方法、普通方法)设计的几个方面:

  • 如何处理参数和返回值
  • 如何设计方法签名
  • 如何为方法编写文档

38. 检查参数的有效性

绝大多数方法和构造器对于传递给它们的参数值都会有某些限制。应该在文档中清楚的指明所有这些限制,并在方法体的开头处检查参数,以强制施加这些限制。这样就遵照了应该在发生错误之后尽快检测出错误这个原则。

  • 传递无效的参数值给方法,这个方法在执行之前先对参数进行校验,那么就能很清晰的抛出异常;如果跳过了这个步骤,可能会导致令人费解的异常、返回错误的结果、或者返回正确的结果并在某个状态下崩溃。
  • 对于公有的方法,对参数进行检验并抛出异常,同时在文档中使用@throws标签声明如果传入参数违反了相关规定就会抛出异常,这样的写法是完善的。让调用API者很清晰的就能了解。
/**
     * @throws NullPointerException if params is null
     */
    private void testOne(String s) {
        if (s == null) {
            throw new NullPointerException("params s can not be null");
        }
        System.out.print(s.toCharArray());
    }
  • 对于非公有的方法、即未向外导出的,因为是程序员自己调用并使用的,也确认参数的合法性,可以使用断言(assertion)来检查它们的参数。
    这个看情况吧,因为assert语句只有在编译器开启了-ea后才会起作用,在团队开发时并不是好事;所以还是要做参数校验并抛出异常。
  • 对于有些参数,方法本身没有用到,却被保存起来以后使用,检验这类参数的有效性尤为重要。
    例如在构造方法中对属性进行初始化时,一定要进行参数检验,避免违法了这个类的约束条件。
  • 在有些情况下,有效性检验非常昂贵,或者是不切实际的,并且该检验已隐含在计算过程中了,在这种情况下进行检验就没必要了。
    例如Collection.sort()方法。
  • 对参数的进行任何限制并不是一件好事。在设计方法时,要尽可能的通用、并符合实际需要。
    假如方法对于它能接受的所有参数值都能够完成工作,对参数的限制就应该越少越好。

总之,在写方法或者构造器时,应该考虑它的参数有哪些限制。
应该把这些限制写到文档中,并且在这个方法开头处,通过显示的检查来实施这些限制。

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

Java是一门安全的语言,不必考虑缓冲区溢出、数组越界、非法指针等其他的内存破坏错误,而这些在C、C++中确实要考虑的;
假设类的客户端会尽其所能的来破坏这个类的约束条件,因为你必须保护性的设计程序。因为即便是类型安全的语言,也无法与其他类隔离开来。
所以编写一些面对客户的不良行为时仍能保持健壮的类,是非常值得投入时间去做的事情。

// 该类的约束为: start < end
public class Period {
    private final Date mStart;
    private final Date mEnd;

    public Period(Date start, Date end) {
        // 对构造器的每个可变参数进行保护性拷贝
        this.mStart = new Date(start.getTime());
        this.mEnd = new Date(end.getTime());

        //检查参数有效性
        if (mStart.compareTo(mEnd) > 0) {
            throw new IllegalArgumentException("start must be ahead of end");
        }
    }

    public Date getmStart() {
        return new Date(mStart.getTime()); //返回可变内部域的保护性拷贝,可以使用clone方法,因为mStart就是Date类型的,这是我们自己知道的
    }

    public Date getmEnd() {
        return new Date(mEnd.getTime());
    }
}
  • 保护性拷贝是在检查参数有效性之前进行的,并且有效性检查是针对拷贝后的对象进行的。
    这样做可以在危险阶段期间另一个线程来改变类的参数。危险阶段:从检查参数开始,直到拷贝参数之间的时间段。
  • 对于参数类型可以被不可信任的子类化的参数,不要使用该对象的clone方法进行保护性拷贝。
    不要使用Date的clone方法来进行保护性拷贝,因为参数Date是非final的,不能保证clone方法一定返回Date对象:可能是出于恶意目的而设计的不可信任的子类。
  • 参数的保护性拷贝并不仅仅针对不可变类。
  • 当编写方法或构造方法时,如果它要允许客户端提供的对象进入内部数据结构时,就要考虑客户提供的对象是否有可能是可变的:如果是不可变的,就直接提供保护性拷贝;如果是可变的,就要考虑是否能够容忍对象进入数据结构之后发生的变化:如果不能忍,就提供保护性拷贝。
  • 在内部组件被返回给客户端之前,对它们进行保护性拷贝也是同样的道理。认真考虑是否应该把一个指向内部可变的引用返回给客户端。
  • 但其实,只要有可能,都应该使用不可变的对象作为对象内部的组件,这样就不必考虑保护性拷贝的问题。
    例如Period那个例子,可以使用long值作为内部属性。
  • 可以在类的文档中清楚的讲明,调用者不能修改传入的参数值或返回值。这样可不用使用保护性拷贝,但这仅仅使用于代码都是自己写的的情形下。

总之,如果类具有可变参数、并且是可以从外界传入或返回的(并且在外界修改后违反类的约束的),类就必须提供保护性拷贝;
如果拷贝成本受到限制,并且信任它的客户端不会不恰当的修改组件,就可以在文档中声明客户端不能修改类内部的组件,以此来替代保护性拷贝。

40. 谨慎的设计方法签名

  • 谨慎的选择方法名
    易于理解的、与同一个包中其他名称风格一致的、大众认可的
  • 不要过于追求提供便利的方法,每个方法都应该尽其所能。
    方法太多会使类难以学习、使用、文档化、测试和维护。接口尤其如此。
  • 避免过长的参数列表,最多四个
    尽量使用简短的参数列表;
    相同类型的长参数列表格外有害,弄反了程序依然运行,排查困难很难;
    有几个方法可以尝试:
  • 把一个方法分解成多个方法,每个方法只需要这些参数的一个子集。通过提升这些子方法的正交性来避免产生的子方法过多。
  • 创建辅助类,用来保存参数的分组。
    如果一个频繁出现的参数序列可以被看作是代表了某个独特的实体,则建议使用这种方法。
  • Build模式
  • 静态工厂模式,方法名不同
  • 对于参数类型,优先使用接口而不是类
    只要有适当的接口可用来定义参数,就优先使用接口,而不是该接口的实现类。
  • 对于boolean参数,优先使用两个元素的枚举类型

41. 慎用重载overload

  • 程序运行时,对于重载方法overloaded method的选择是静态的,即根据编译时期的类型,而对于复写方法overriden method的选择是动态的,即根据运行时期的类型。
  • 避免胡乱使用重载机制
    如果用户根本不知道对于一组给定的参数,其中的哪个重载方法将会被调用,那么这样的API就很可能导致错误。这些错误很隐蔽且难以排查。
  • 怎么避免胡乱使用重载机制
    永远不要导出两个具有相同参数数目的重载方法;
    也不要重载具有可变参数的方法;
    使用不同的方法名;
  • 确定选择哪个重载方法的规则是非常复杂的,大多数情况下尽可能谨慎的使用,尽量不用,除非参数类型是不相关的。

总之,能够重载方法并不意味着应该重载方法
1. 对于多个具有相同参数数目的方法来说,应该尽量避免重载方法。
2. 第一条做不到时,避免同一组参数只需经过类型转换就可以传递给不同的重载方法;
3. 第二条做不到时,就要保证当传递同样的参数时,所有重载方法的行为必须一致;

42. 慎用可变参数

可匹配不同长度的变量的方法:variable arity method。
可变参数:接收0个或多个指定类型的参数。
可变参数机制通过先创建一个数组,数组的大小为调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法。

  • 可变参数方法的每次调用都会导致进行一次数组分配和初始化。所以在重视性能的情况下,可以考虑在4个参数以为不使用可变参数。

在定义参数数目不定的方法时,可以使用可变参数;但不应该过渡滥用。

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

  • 返回null而不是零长度数组或空集合会使调用者在几乎用到该方法时都要进行判空操作,相对来说比较麻烦;而且忘记该操作的话,就会出错。
  • 性能方面
    在这个级别上担心性能问题是不明智的,除非分析表明这个方法正是性能问题的真正源头。
  • 返回一个零长度数组或空集合是可能的,因为零长度数组是不可变的,而不可变对象是可以被共享的。
    Collections.emptyList()等。

可以考虑返回空数组或空集合,看情况而定吧。

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

使用Javadoc来根据源码的文档注释生成相应的文档。

  • 为了正确的编写API文档,必须在每个被导出的类、接口、构造器、方法和域声明之前增加一个文档注释。
  • 方法的文档注释应该简介的描述出它和客户端之间的约定
  • 对于类、接口、域,概要描述应该是一个名词短语,它描述了该类或者接口的实例,或者域本身所代表的事物。
  • 对于泛型、枚举、注解,确保要在文档中说明所有的类型参数。
  • 对于枚举,一定要确保在文档中说明常量。
  • 对于注解,要确保在文档中说明所有成员,以及类型本身。

可以查看《How to Write Doc Comments》这本书。