Effecive Java 读书笔记(四)

来源:互联网 发布:开淘宝网店怎么弄客服 编辑:程序博客网 时间:2024/06/04 17:59


Effective Java 第七章 方法


(1)检查参数的有效性

    绝大多数方法和构造器对于传递给它们的参数值都会有些限制。比如,索引值必须大于等于0,且不能超过其最大值,对象不能为null等。这样就可以在导致错误的源头将错误捕获,从而避免了该错误被延续到今后的某一时刻再被引发,这样就是加大了错误追查的难度。就如同编译期能够报出的错误总比在运行时才发现要更好一些。

    事实上,我们不仅仅需要在函数的内部开始出进行这些通用的参数有效性检查,还需要在函数的文档中给予明确的说明,如在参数非法的情况下,会抛出那些异常,或导致函数返回哪些错误值等

    对于公有的方法,要用Javadoc的@throws标签在文档中说明违反参数值限制时会抛出的异常,如:   

/**     * Returns a BigInteger whose value is(this mod m). This method     * differs from the remainder method in that it always returns a     * non-negative BigInteger.     * @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);         ... //Do the computation.     }
对于非公有的方法应使用断言来检查它们的参数,不同于一般的有效性检查,断言如果失败,将会抛出AssertionError,也不同于一般的有效性检查,如果它们没有起到作用,本质上也不会有成本开销。
      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);         ... //Do the computation     }
    需要强调的是,对于有些函数的参数,其在当前函数内并不使用,而是留给该类其他函数内部使用的比较明显的就是类的构造函数,构造函数中的很多参数都不一样用于构造器内,只是在构造的时候进行有些赋值操作,而这些参数的真正使用者是该类的其他函数,对于这种情况,我们就更需要在构造的时候进行参数的有效性检查
    此条规则也有一个例外:在有些情况下有效性检查工作的开销是非常大的,或者根本不切实际,因为这些检查已经隐含在计算过程中完成了如Collections.sort(List),容器中对象的所有比较操作均在该函数执行时完成,一旦比较操作失败将会抛出ClassCastException异常。因此对于sort来讲,如果我们提前做出有效性检查将是毫无意义的

(2)必要时进行保护性拷贝

如果你的对象没有做很好的隔离,那么对于调用者而言,则有机会破坏该对象的内部约束条件,因此我们需要保护性的设计程序。该破坏行为一般由两种情况引起,首先就是恶心的破坏,再有就是调用者无意识的误用,这两种条件下均有可能给你的类带来一定的破坏性

保护性拷贝的实现方法阐述,看下面的例子

import java.util.Date;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 start(){return start;}public Date end(){return end;}//remainder omitted}
虽然这个类加了约束条件,但是由于Date 类本身是可变类,于是很容易违反这个约束条件,下面是第一种攻击示例
Date start = new Date();Date end = new Date();Period period = new Period(start, end);end.setYear(78);System.out.println(period.end());
为了保护Period实例的内部信息避免受到修改,导致问题,对于构造器的每个可变参数进行保护性拷贝(defensive copy)是必要的,并且使用备份对象作为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(this.start + " after " + this.end);}}
注意保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象

进行保护性拷贝有两种方法,一种是使用备份对象作为要保证其不变性的实例的组件,一种是使用Clone方法。使用Clone方法要求此类必须是final的,这里Date是非final的,故只能用第一种方法

下面是第二种攻击示例

Date start = new Date();Date end = new Date();Period period = new Period(start, end);p.end().setYear(78);

为了防御第二种攻击,只能修改这两个访问方法,使它返回可变内部域的保护性拷贝 或者使用Clone都可以(访问方法和构造器不同,它们在进行保护性拷贝时允许使用clone方法)

public Date start(){      return new Date(start.getTime());  }public Date end(){      return new Date(end.getTime());  }
或者这样:

public Date start(){return (Date)this.start.clone();}public Date end(){return (Date)this.end.clone();}

参数的保护性拷贝不仅仅针对不可变类。每当编写编写方法和构造器时,如果他要允许客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象是否有可能是可变的,我是否能够容忍这种可变性。特别是你用到list、map之类连接元素时。
在内部组件返回给客户端的时候,也要考虑是否可以返回一个指向内部引用的数据。或者,不使用拷贝,你也可以返回一个不可变对象。如:Colletions.unmodifiableList(List<? extends T> list
启示:只要有可能,都应该使用不可变对象作为对象内部的组件

(3)慎用重载

public class CollectionClassifier {    public static String classify(Set<?> s) {        return "Set";    }     public static String classify(List<?> lst) {        return "List";    }     public static String classify(Collection<?> c) {        return "Unknown Collection";    }     public static void main(String[] args) {        Collection<?>[] collections = {            new HashSet<String>(),            new ArrayList<BigInteger>(),            new HashMap<String, String>().values()        };         for (Collection<?> c : collections)            System.out.println(classify(c));    }} #输出Unknown Collection
 
  这里你可能会期望程序打印出
      //Set
      //List
      //Unknown Collection
      然而实际上却不是这样,输出的结果是3个"Unknown Collection"。为什么会是这样呢?因为函数重载后,需要调用哪个函数是在编译期决定的,这不同于多态的运行时动态绑定 ,对于重载方法的选择是静态的,而对于被覆盖方法的选择是动态的。

    针对此种情形,该条目给出了一个修正的方法,如下:   

/**public static String classify(Set<?> s) {        return "Set";    }    public static String classify(ArrayList<?> l) {        return "List";    }    public static String classify(Collection<?> c) {        return "Unknown collection";    }  **/public static String classify(Collection<?> c) {        return c instanceof Set ? "Set" : c instanceof ArrayList             ? "List" : "Unknown Collection";     }

    说明:用单个方法来替换这三个重载的classify方法,并在这个方法中做一个显式的instanceof测试

    建议,永远不要导出两个具有相同参数数目的重载方法,对于可变参数的方法,不要去重载它

public class SetList {    public static void main(String[] args) {        Set<Integer> set = new TreeSet<Integer>();        List<Integer> list = new ArrayList<Integer>();         for (int i = -3; i < 3; i++) {            set.add(i);            list.add(i);        }         for (int i = 0; i < 3; i++) {            set.remove(i);            list.remove(i);        }         System.out.println(set + " " + list);    }}

在执行该段代码前,我们期望的结果是Set和List集合中大于等于零的元素均被移除出容器,然而在执行后却发现事实并非如此,其结果为:
      [-3,-2,-1] [-2,0,2]
      这个结果和我们的期望还是有很大差异的,为什么Set中的元素是正确的,而List则不是,是什么导致了这一结果的发生呢?下面给出具体的解释:
      1. set.remove(i)调用的是Set中的remove(E),这里的E表示Integer,Java的编译器会将i自动装箱到Integer中,因此我们得到了想要的结果。
      2. list.remove(i)实际调用的是List中的remove(int index)重载方法,而该方法的行为是删除集合中指定索引的元素。这里分别对应第0个,第1个和第2个。

      混乱行为的原因:List<E>接口有两个重载的remove方法:remove(E),remove(int).Java语言中添加了泛型和自动装箱后,破坏了这个接口.我们需要让List明确的知道,我们需要调用的是remove(E)重载函数,而不是其他的,这样我们就需要对原有代码进行如下的修改:

public class SetList {        public static void main(String[] args) {            Set<Integer> s = new TreeSet<Integer>();            List<Integer> l = new ArrayList<Integer>();            for (int i = -3; i < 3; ++i) {                s.add(i);                l.add(i);            }            for (int i = 0; i < 3; ++i) {                s.remove(i);                l.remove((Integer)i); //or remove(Integer.valueOf(i));            }            System.out.println(s + " " + l);        }    }
上述例子的启示:   尽量做到,当传递同样的参数时,所有重载方法的行为必须一致

(3)慎用可变参数


Java1.5增加了新特性:可变参数:适用于参数个数不确定,类型确定的情况,java把可变参数当做数组处理。注意:可变参数必须位于最后一项。当可变参数个数多余一个时,必将有一个不是最后一项,所以只支持有一个可变参数。因为参数个数不定,所以当其后边还有相同类型参数时,java无法区分传入的参数属于前一个可变参数还是后边的参数,所以只能让可变参数位于最后一项。

可变参数的特点:

(1)、只能出现在参数列表的最后; 

(2)、...位于变量类型和变量名之间,前后有无空格都可以;

(3)、调用可变参数的方法时,编译器为该可变参数隐含创建一个数组,在方法体中一数组的形式访问可变参数。

public class Varargs {     // Simple use of varargs - Page 197    static int sum(int... args) {        int sum = 0;        for (int arg : args)            sum += arg;        return sum;    }      // The WRONG way to use varargs to pass one or more arguments! - Page 197//  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];//      return min;//  }     // The right way to use varargs to pass one or more arguments - Page 198    static int min(int firstArg, int... remainingArgs) {        int min = firstArg;        for (int arg : remainingArgs)            if (arg < min)                min = arg;        return min;    }     public static void main(String[] args) {        System.out.println(sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));        System.out.println(min(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));    }}

2.int[] digits = {3,2,1,6,4,5,8}

Arrays.asList(digits) 打印结果为[Ljava.lang.int:@3e25a5是无意义的

如果Integer[] digits= {3,2,1,6,4,5,8} 打印结果为[3,2,1,6,4,5,8]

只在对象引用类型的数组上才有用,对基本类型的数组不行。

3.在重性能的情况下,使用可变参数机制要特别小心。可变参数方法的每次调用都会导致进行一次数组分配和初始化;


0 0
原创粉丝点击