`

Effective Java (方法)

 
阅读更多

三十八、检查参数的有效性:

      绝大多数方法和构造器对于传递给它们的参数值都会有些限制。比如,索引值必须大于等于0,且不能超过其最大值,对象不能为null等。这样就可以在导致错误的源头将错误捕获,从而避免了该错误被延续到今后的某一时刻再被引发,这样就是加大了错误追查的难度。就如同编译期能够报出的错误总比在运行时才发现要更好一些。事实上,我们不仅仅需要在函数的内部开始出进行这些通用的参数有效性检查,还需要在函数的文档中给予明确的说明,如在参数非法的情况下,会抛出那些异常,或导致函数返回哪些错误值等,见如下代码示例:

复制代码
1     /**
2      * Returns a BigInteger whose value is(this mod m). This method
3 * differs from the remainder method in that it always returns a
4      * non-negative BigInteger.
5      *@param m the modulus, which must be positive.
6      *@return this mod m.
7      *@throws ArithmeticException if m is less than or equal to 0.
8 */
9      public BigInteger mod(BigInteger m) {
10          if (m.signum() <= 0)
11              throw new ArithmeticException("Modulus <= 0: " + m);
12          ... //Do the computation.
13      }
复制代码

       是不是我们为所有的方法均需要做出这样的有效性检查呢?对于未被导出的方法,如包方法等,你可以控制这个方法将在哪些情况下被调用,因此这时可以使用断言来帮助进行参数的有效性检查,如:

1      private static void sort(long a[],int offset,int length) {

2          assert(a != null);

3          assert(offset >= 0 && offset <= a.length);

4          assert(length >= 0 && length <= a.length - offset);

5          ... //Do the computation6      }

       和通用的检查方式不同,断言在其条件为真时,无论外部包得客户端如何使用它。断言都将抛出AssertionError。它们之间的另一个差异在于如果断言没有起到作用,即-ea命令行参数没有传递给java解释器,断言将不会有任何开销,这样我们就可以在调试期间加入该命令行参数,在发布时去掉该命令行选项,而我们的代码则不需要任何改动。
       需要强调的是,对于有些函数的参数,其在当前函数内并不使用,而是留给该类其他函数内部使用的,比较明显的就是类的构造函数,构造函数中的很多参数都不一样用于构造器内,只是在构造的时候进行有些赋值操作,而这些参数的真正使用者是该类的其他函数,对于这种情况,我们就更需要在构造的时候进行参数的有效性检查,否则一旦将该问题释放到域函数的时候,再追查该问题的根源,将不得不付出更大的代价和更多的调试时间。
       对该条目的说法确实存在着一种例外情况,在有些情况下有效性检查工作的开销是非常大的,或者根本不切实际,因为这些检查已经隐含在计算过程中完成了,如Collections.sort(List),容器中对象的所有比较操作均在该函数执行时完成,一旦比较操作失败将会抛出ClassCastException异常。因此对于sort来讲,如果我们提前做出有效性检查将是毫无意义的。
     
三十九、必要时进行保护性拷贝:

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

复制代码
1     public final class Period {
2         private final Date start;
3         private final Date end;
4         public Period(Date start,Date end) {
5             if (start.compareTo(end) > 0) {
6                 throw new IllegalArgumentException(start + "After " + end);
7             this.start = start;
8             this.end = end;
9         }
10         public Date start() {
11             return start;
12         }
13         public Date end() {
14             return end;
15         }
16     }
 
复制代码

      从表面上看,该类的实现确实对约束性的条件进行了验证,然而由于Date类本身是可变了,因此很容易违反这个约束,见如下代码:

1     public void testPeriod() {

2         Date start = new Date();

3         Date end = new Date();

4         Period p = new Period(start,end);

5         end.setYear(78);  //该修改将直接影响Period内部的end对象。
6     }

      为了避免这样的攻击,我们需要对Period的构造函数进行相应的修改,即对每个可变参数进行保护性拷贝。

1     public Period(Date start,Date end) {

2         this.start = new Date(start.getTime());

3         this.end = new Date(end.getTime());

4         if (start.compareTo(end) > 0) {

5             throw new IllegalArgumentException(start + "After " + end);

6     }

      需要说明的是,保护性拷贝是在坚持参数有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始对象的。这主要是为了避免在this.start = new Date(start.getTime())到if (start.compareTo(end) > 0)这个时间窗口内,参数start和end可能会被其他线程修改。
      现在构造函数已经安全了,后面我们需要用同样的方式继续修改另外两个对象访问函数。

1     public Date start() {

2         return new Date(start.getTime());

3     }

4     public Date end() {

5         return new Date(end.getTime());

6     }

      经过这一番修改之后,Period成为了不可变类,其内部的“周期的起始时间不能落后于结束时间”约束条件也不会再被破坏。
      参数的保护性拷贝并不仅仅针对不可变类。每当编写方法或者构造器时,如果它要允许客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象是否有可能是可变的。如果是,就要考虑你的类是否能够容忍对象进入数据结构之后发生变化。如果答案是否定的,就必须对该对象进行保护性拷贝,并且让拷贝之后的对象而不是原始对象进入到数据结构中。例如,如果你正在考虑使用有客户提供的对象引用作为内部Set实例的元素,或者作为内部Map实例的键(Key),就应该意识到,如果这个对象在插入之后再被修改,Set或者Map的约束条件就会遭到破坏。
    
四十一、谨慎重载:

      见下面一个函数重载的例子:

复制代码
1     public class CollectionClassfier {
2         public static String classify(Set<?> s) {
3             return "Set";
4         }
5         public static String classify(List<?> l) {
6             return "List";
7         }
8         public static String classify(Collection<?> c) {
9             return "Unknown collection";
10         }
11         public static void main(String[] args) {
12             Collection<?>[] collections = {
13                 new HashSet<String>(),
14                 new ArrayList<BigInteger>(),
15                 new HashMap<String,String>().values()
16             };
17             for (Collection<?> c : collections)
18                 System.out.println(classify(c));
19         }
20     }
 
复制代码

      这里你可能会期望程序打印出
      //Set
      //List
      //Unknown Collection
      然而实际上却不是这样,输出的结果是3个"Unknown Collection"。为什么会是这样呢?因为函数重载后,需要调用哪个函数是在编译期决定的,这不同于多态的运行时动态绑定。针对此种情形,该条目给出了一个修正的方法,如下:

1     public static String classify(Collection<?> c) {

2         return c instanceof Set ? "Set" : c instanceof List 
3             ? "List" : "Unknown Collection";

4     }

      和override不同,重载机制不会像override那样规范,并且每次都能得到期望的结果。因此在使用时需要非常谨慎,否则一旦出了问题,就会需要更多的时间去调试。该条目给出以下几种尽量不要使用重载的情形:
      1.    函数的参数中包含可变参数;
      2.    当函数参数数目相同时,你无法准确的确定哪一个方法该被调用时;
      3.    在Java 1.5 之后,需要对自动装箱机制保持警惕。
      我们先简单说一下第二种情形。比如两个重载函数均有一个参数,其中一个是整型,另一个是Collection<?>,对于这种情况,int和Collection<?>之间没有任何关联,也无法在两者之间做任何的类型转换,否则将会抛出ClassCastException的异常,因此对于这种函数重载,我们是可以准确确定的。反之,如果两个参数分别是int和short,他们之间的差异就不是这么明显。
      对于第三种情形,该条目给出了一个非常典型的用例代码,如下:

复制代码
1     public class SetList {
2         public static void main(String[] args) {
3             Set<Integer> s = new TreeSet<Integer>();
4 List<Integer> l = new ArrayList<Integer>();
5             for (int i = -3; i < 3; ++i) {
6                 s.add(i);
7                 l.add(i);
8             }
9             for (int i = 0; i < 3; ++i) {
10                 s.remove(i);
11                 l.remove(i);
12             }
13             System.out.println(s + " " + l);
14         }
15     }
 
复制代码

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

复制代码
1     public class SetList {
2         public static void main(String[] args) {
3             Set<Integer> s = new TreeSet<Integer>();
4 List<Integer> l = new ArrayList<Integer>();
5             for (int i = -3; i < 3; ++i) {
6                 s.add(i);
7                 l.add(i);
8             }
9             for (int i = 0; i < 3; ++i) {
10                 s.remove(i);
11                 l.remove((Integer)i); //or remove(Integer.valueOf(i));
12             }
13             System.out.println(s + " " + l);
14         }
15     }
 
复制代码

      该条目还介绍了一种实现函数重载,同时又尽可能避免上述错误发生的方式。即其中的一个重载函数,在其内部通过一定的转换逻辑转换之后,再通过转换后的参数类型调用其他的重载函数,从而确保即便使用者在使用过程中出现重载误用的情况,也因两者可以得到相同的结果而规避了潜在错误的发生。

四十二、慎用可变参数:

      可变参数方法接受0个或者多个指定类型的参数。可变参数机制通过先创建一个数组,数组的大小为在调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法,如:

1     static int sum(int...args) {

2         int sum = 0;

3         for (int arg : args)

4             sum += arg;

5         retrun sum;

6     }

      上面的方法可以正常的工作,但是在有的时候,我们可能需要至少一个或者多个某种类型参数的方法,如:

复制代码
1     static int min(int...args) {
2         if (args.length == 0)
3             throw new IllegalArgumentException("Too few arguments.");
4         int min = args[0];
5         for (int i = 0; i < args.length; ++i) {
6             if (args[i] < min)
7                 min = args[i];
8         }
9         return min;
10     }
 
复制代码

      对于上面的代码主要存在两个问题,一是如果调用者没有传递参数是,该函数将会在运行时抛出异常,而不是在编译期报错。另一个问题是这样的写法也是非常不美观的,函数内部必须做参数的数量验证,不仅如此,这也影响了效率。将编译期可以完成的事情推到了运行期。下面提供了一种较好的修改方式,如下:

复制代码
1     static int min(int firstArg,int...remainingArgs) {
2         int min = firstArgs;
3         for (int arg : remainingArgs) {
4             if (arg < min)
5                 min = arg;
6         }
7         return min;
8     }
 
复制代码

      由此可见,当你真正需要让一个方法带有不定数量的参数时,可变参数就非常有效。
      有的时候在重视性能的情况下,使用可变参数机制要特别小心。可变参数方法的每次调用都会导致进行一次数组分配和初始化。如果确定确实无法承受这一成本,但又需要可变参数的灵活性,还有一种模式可以弥补这一不足。假设确定对某个方法95%的调用会有3个或者更少的参数,就声明该方法的5个重载,每个重载方法带有0个至3个普通参数,当参数的数目超过3个时,就使用一个可变参数方法:

1     public void foo() {}

2     public void foo(int a1) {}

3     public void foo(int a1,int a2) {}

4     public void foo(int a1,int a2,int a3) {}

5     public void foo(int a1,int a2,int a3,int...rest) {}

      所有调用中只有5%参数数量超过3个的调用需要创建数组。就像大多数的性能优化一样,这种方法通常不恰当,但是一旦真正需要它时,还是非常有用处的。
    
四十三、返回零长度的数组或者集合,而不是null:

      见如下代码:

复制代码
1     public class CheesesShop {
2         private final List<Cheese> cheesesInStock = new List<Cheese>();
3         public Cheese[] getCheeses() {
4             if (cheesesInStock.size() == 0)
5                 return null;
6             return cheeseInStock.toArray(null);
7         }
8     }
 
复制代码

      从以上代码可以看出,当没有Cheese的时候,getCheeses()函数返回一种特例情况null。这样做的结果会使所有的调用代码在使用前均需对返回值数组做null的判断,如下:

1     public void testGetCheeses(CheesesShop shop) {

2         Cheese[] cheeses = shop.getCheeses();

3         if (cheese != null && Array.asList(cheeses).contains(Cheese.STILTON))
4             System.out.println("Jolly good, just the thing.");

5     }

      对于一个返回null而不是零长度数组或者集合的方法,几乎每次用到该方法时都需要这种曲折的处理方式。很显然,这样是比较容易出错的。如果我们使getCheeses()函数在没有Cheese的时候不再返回null,而是返回一个零长度的数组,那么我的调用代码将会变得更加简洁,如下:

1     public void testGetCheeses2(CheesesShop shop) {

2         if (Array.asList(shop.getCheeses()).contains(Cheese.STILTON))

3             System.out.println("Jolly good, just the thing.");

4     }

      相比于数组,集合亦是如此。

分享到:
评论

相关推荐

    Effective Java读书笔记.pdf

    Effective Java是一本关于Java编程语言的经典书籍,本笔记主要总结了Java语言的发展历程、静态工厂方法的应用、构造器模式的使用等重要知识点。 一、Java语言的发展历程 Java语言的发展可追溯到1991年,当时由...

    Effective Java第三版1

    《Effective Java》是Java编程领域的一本经典著作,由Joshua Bloch撰写,该书的第三版继续提供了关于如何编写高效、优雅、可维护的Java代码的指导。以下是基于给出的目录和部分内容提取的一些关键知识点: ### 第一...

    effective-java.pdf

    标题“effective-java.pdf”与描述“effective-java.pdf”表明本文档是关于Java编程实践的指南,且内容可能来自于一本名为《Effective Java》的书籍,该书是由Joshua Bloch编写,被广泛认为是Java编程的权威指南。...

    effectiveJava课件分享

    在编程领域,特别是Java开发中,"Effective Java"是一本非常经典的书籍,由Joshua Bloch撰写,书中提出了一系列最佳实践和设计原则,以帮助开发者编写出更高效、更安全的代码。根据提供的标题和描述,我们将探讨三个...

    《Effective Java》读书分享.pptx

    "Effective Java 读书分享" 《Effective Java》读书分享.pptx 是一本 Java 编程语言指南,旨在帮助开发者编写高质量、可维护的 Java 代码。该书包含 90 个条目,每个条目讨论一条规则,涵盖了 Java 编程语言的...

    Effective java 3 学习记录.docx

    本学习记录主要介绍了 Effective Java 3 中的静态工厂方法和 Builder 模式两部分内容。 一、静态工厂方法 静态工厂方法是指返回类实例的命名规则,例如:from、of、valueOf、instance 或 getinstance、create 或 ...

    effective java 读书笔记

    《Effective Java》是Java开发领域的经典著作,作者Joshua Bloch深入浅出地阐述了编写高效、健壮的Java代码的技巧和最佳实践。以下是对该书部分内容的详细解释: 1. **产生和销毁对象** - Item1:静态工厂方法相比...

    Effective.Enterprise.Java.中文版 高清pdf 下载

    《Effective Enterprise Java》是Java开发领域的一本经典著作,由著名技术专家Bill Venners编著,被广大Java开发者誉为“四大名著”之一。这本书深入探讨了在企业级Java开发中如何写出高效、可维护和易于理解的代码...

    Effective Java.zip

    《Effective Java》是一本经典Java编程指南,作者是Joshua Bloch,这本书深入探讨了如何编写高质量、高效、可维护的Java代码。以下是对压缩包中各章节主要知识点的详细阐述: 1. **第2章 创建和销毁对象** - 单例...

    effective-java 配套代码

    《Effective Java》是Java开发领域的一本经典著作,由Joshua Bloch撰写,书中提出了一系列编程最佳实践和设计模式,帮助开发者写出更高效、更可靠、更易于维护的Java代码。配套代码`effective-java-examples-master`...

    Effective Enterprise Java(中文) Effective Enterprise Java(中文)

    本书详细介绍企业级计算技术中的常见问题,并描述使用企业级Java平台技术处理这些问题的方法。本书以若干条建议、揸南的形式,言简意赅地介绍了J2EE开发中的微妙之处。无论你是否是Java开发人员,本书都将为你开发...

    effective enterprise java 中文版

    《Effective Enterprise Java》是一本由James Gosling、Bill Venners和Cay S. Horstmann合著的经典著作,旨在帮助Java开发者深入理解和利用企业级Java技术。这本书提供了78条具体的建议,涵盖了从设计模式到并发编程...

    effectiveJava的笔记

    《Effective Java》是Java开发领域的经典著作,由Joshua Bloch编写,旨在提供一系列实用的编程准则和最佳实践。这本书的第三版包含了大量更新,涵盖了Java语言和平台的新发展,如Java 8和Java 9的新特性。以下是对...

    Effective Enterprise Java

    6. **安全性**:书中的安全章节涵盖了身份验证、授权、加密和SSL/TLS等关键话题,提供了如何在Java EE环境中实现安全控制的方法。 7. **JMS(Java Message Service)**:对于消息传递和异步处理,书中讲解了JMS的...

    effecctivejava 第三版中文

    《Effective Java》是Java编程领域的一本经典著作,由Joshua Bloch撰写,现在已经更新到第三版。这本书深入探讨了如何编写高效、可维护且设计良好的Java代码,是每一个Java开发者提升技能的重要参考资料。以下是对该...

    Effective-Java:Effective Java中文版第二版示例代码

    《Effective Java》是Java开发领域的经典著作,由Joshua Bloch撰写,中文版第二版更是深受广大Java开发者喜爱。这本书提供了许多实用的编程实践和经验教训,帮助开发者编写出更高效、可维护的Java代码。这里我们将...

Global site tag (gtag.js) - Google Analytics