zoukankan      html  css  js  c++  java
  • Effective java笔记4--方法

    一、检查参数的有效性

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

       对于公有的方法,使用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)“&#46;"来代替它,虽然这样做可以工作,但不会生成漂亮的源代码。

  • 相关阅读:
    Roslyn 编译器和RyuJIT 编译器
    go语言---defer
    go语言---for
    go语言--time.After
    go语言---for range
    用gulp打包带参数资源做法与asp.net/java项目结合的自动构建方案探讨
    JQuery判断元素是否存在
    gulp实现打包js/css/img/html文件,并对js/css/img文件加上版本号
    gulp同步执行任务
    gulp-rev同时将js和css文件写在一个rev-manifest.json文件里面的方式探讨
  • 原文地址:https://www.cnblogs.com/duanxz/p/3422229.html
Copyright © 2011-2022 走看看