Effective Java读书笔记六:方法(38-44)

第38条:检查参数的有效性

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

/**
     * 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.
     }

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

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
      }

和通用的检查方式不同,断言在其条件为真时,无论外部包得客户端如何使用它。断言都将抛出AssertionError。它们之间的另一个差异在于如果断言没有起到作用,即-ea命令行参数没有传递给java解释器,断言将不会有任何开销,这样我们就可以在调试期间加入该命令行参数,在发布时去掉该命令行选项,而我们的代码则不需要任何改动。

需要强调的是,对于有些函数的参数,其在当前函数内并不使用,而是留给该类其他函数内部使用的,比较明显的就是类的构造函数,构造函数中的很多参数都不一样用于构造器内,只是在构造的时候进行有些赋值操作,而这些参数的真正使用者是该类的其他函数,对于这种情况,我们就更需要在构造的时候进行参数的有效性检查,否则一旦将该问题释放到域函数的时候,再追查该问题的根源,将不得不付出更大的代价和更多的调试时间。

对该条目的说法确实存在着一种例外情况,在有些情况下有效性检查工作的开销是非常大的,或者根本不切实际,因为这些检查已经隐含在计算过程中完成了,如Collections.sort(List),容器中对象的所有比较操作均在该函数执行时完成,一旦比较操作失败将会抛出ClassCastException异常。因此对于sort来讲,如果我们提前做出有效性检查将是毫无意义的。

第39条:必要时进行保护性拷贝

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

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;
        }
    }

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

public void testPeriod() {
         Date start = new Date();
         Date end = new Date();
         Period p = new Period(start,end);
         end.setYear(78);  //该修改将直接影响Period内部的end对象。
     }

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

public Period(Date start,Date end) {
         this.start = new Date(start.getTime());
         this.end = new Date(end.getTime());
         if (start.compareTo(end) > 0) {
             throw new IllegalArgumentException(start + "After " + end);
     }

需要说明的是,保护性拷贝是在坚持参数有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始对象的。这主要是为了避免在this.start = new Date(start.getTime())到if (start.compareTo(end) > 0)这个时间窗口内,参数start和end可能会被其他线程修改。

现在构造函数已经安全了,后面我们需要用同样的方式继续修改另外两个对象访问函数。

    public Date start() {
         return new Date(start.getTime());
     }
    public Date end() {
         return new Date(end.getTime());
     }

经过这一番修改之后,Period成为了不可变类,其内部的“周期的起始时间不能落后于结束时间”约束条件也不会再被破坏。

参数的保护性拷贝并不仅仅针对不可变类。每当编写方法或者构造器时,如果它要允许客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象是否有可能是可变的。如果是,就要考虑你的类是否能够容忍对象进入数据结构之后发生变化。如果答案是否定的,就必须对该对象进行保护性拷贝,并且让拷贝之后的对象而不是原始对象进入到数据结构中。

例如,如果你正在考虑使用有客户提供的对象引用作为内部Set实例的元素,或者作为内部Map实例的键(Key),就应该意识到,如果这个对象在插入之后再被修改,Set或者Map的约束条件就会遭到破坏。

第40条:谨慎设计方法签名

  1. 谨慎地选择方法的名称
  2. 避免过长的参数列表,目标是四个参数或者更少,如果多于四个了就该考虑重构这个方法了(把方法分解多个小方法、创建辅助类、从对象构建到方法调用都采用Builder模式)。
  3. 对于参数类型、要优先使用接口而不是类。如果使用的是类而不是接口,则限制了客户端只能传入特定的实现,如果碰巧输入的数据是以其他的形式存在,就会导致不必要的、可能非常昂贵的拷贝操作。
  4. 对于boolean参数,优先使用两个元素的枚举类型

第41条:慎用重载

下面的例子根据一个集合是Set、List还是其他的集合类型,来对它进行分类:

  public class CollectionClassfier { 
            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<?>[] collections = {new HashSet<String>(), new ArrayList<BigInteger>(), new HashMap<String,String>().values()}; 
                for (Collection<?> c : collections) 
                    System.out.println(classify(c)); 
            } 
        } 

这里你可能会期望程序打印出Set、List、Unknown Collection,然而实际上却不是这样,输出的结果是3 个”Unknown Collection”。
因为classify方法被重载了,需要调用哪个函数是在编译期决定的,for中的三次迭代参数的编译类型是相同的:

Collection<?>

对于重载方法的选择是静态的,而对于被覆盖的方法的选择则是动态的。选择被覆盖的方法的正确版本是在运行时进行的,选择的依据是被调用的方法所在对象的运行时类型。这里重新说明一下,当一个子类包含的方法声明与其祖先类中的方法声明具有同样的的签名时,方法就被覆盖了。如果实例方法在子类中被覆盖了,并且这个方法是在该子类的实例上被调用的,那么子类中的覆盖方法将会执行,而不管该子类实例的编译时类型到底是什么。

        class Wine{ 
            String name() {return "wine"; } 
        } 
        class SparklingWine extends Wine{ 
            @Override String name(){return "sparkling wine"; } 
        } 
        class Champagne extends Wine{ 
            @Override String name(){return "Champagne"; } 
        } 
        public class Overriding{ 
            public static void main(String[] args){ 
                Wine[] = {
                new Wine(), 
                new SparklingWine(), 
                new Champagne() }; 
            } 
            for(Wine wine : wines){ 
                System.out.println(wine.name()); 
            } 
        } 

正如你所预期的那样,这个程序打印出“wine, sparkling wine, champagne”,当调用被覆盖的方法时,对象的编译时类型不会影响到哪个方法将被执行。最为具体的那个覆盖版本总是会得到执行。

对于开始的集合输出类的最佳修正方案是,用单个方法来替换这三个重载的classity方法,如下:

public static String classify(Collection<?> c) { 
            return c instanceof Set ? "Set" : c instanceof List ? "List" : "Unknown Collection"; 
        } 

因此,应该避免胡乱地使用重载机制。
一、安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法。比如两个重载函数均有一个参数,其中一个是整型,另一个是Collection<?>,对于这种情况,int 和Collection<?>之间没有任何关联,也无法在两者之间做任何的类型转换,否则将会抛出ClassCastException 的异常,因此对于这种函数重载,我们是可以准确确定的。反之,如果两个参数分别是int 和short,他们之间的差异就不是这么明显。
二、如果方法使用可变参数,保守的策略是根本不要重载它。
三、对于构造器,你没有选择使用不同名称的机会,一个类的多个构造器总是重载的,但是构造器也不可能被覆盖。
四、在Java 1.5 之后,需要对自动装箱机制保持警惕。
演示如下:

 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(i); 
                } 
                System.out.println(s + " " + l); 
            } 
        } 

在执行该段代码前,我们期望的结果是Set 和List 集合中大于等于的元素均被移除出容器,然而在执行后却发现事实并非如此,其结果为:[-3,-2,-1] [-2,0,2]。这个结果和我们的期望还是有很大差异的,为什么Set 中的元素是正确的,而List 则不是,是什么导致了这一结果的发生呢?

下面给出具体的解释:

s.remove(i)调用的是Set 中的remove(E),这里的E 表示Integer,Java 的编译器会将i 自动装箱到Integer 中,因此我们得到了想要的结果。

l.remove(i)实际调用的是List 中的remove(int index)重载方法,而该方法的行为是删除集合中指定索引的元素。这里分别对应第0 个,第1 个和第2 个。

为了解决这个问题,我们需要让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); 
            } 
        } 

总结,对于多个具有相同参数数目的方法来说,应该尽量避免重载方法。我们应当保证:当传递同样的参数时,所有重载方法的行为必须一致。

第42条:慎用可变参数

可变数组机制是通过先创建一个数组,数组的大小为在调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法。

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

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

所有调用中只有5%参数数量超过3个的调用需要创建数组。就像大多数的性能优化一样,这种方法通常不恰当,但是一旦真正需要它时,还是非常有用处的。

在定义参数数目不定的方法时,可变参数方法是一种很方便的方式,但是它们不应该过度滥用。如果使用不当,会产生混乱的结果。

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

有时候会有人认为:null返回值比零长度数据更好,因为它避免了分配数组所需要的开销。
这种观点是站不住脚的,原因有两点。

  1. 在这个级别上担心性能问题是不明智的,除非分析表明这个方法正是造成性能问题的真正源头。
  2. 对于不返回任何元素的调用,每次都返回同一个零长度数组是有可能的,因为零长度数组是不可变的,而不可变对象有可能被自由地共享。
private static final Cheese[] EMPTY_CHEESE_ARRAY= new Cheese[0];

相比于数组,集合亦是如此。
在Collections中有专门针对List,Set,Map的空的实现。如:

Collections.emptyList()
Collections.emptySet();
Collections.emptyMap();

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

《Effective Java中文版 第2版》PDF版下载:
http://download.csdn.net/detail/xunzaosiyecao/9745699

作者:jiankunking 出处:http://blog.csdn.net/jiankunking

阅读终点,创作起航,您可以撰写心得或摘录文章要点写篇博文。去创作
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 《Effective Java第三版》是由Joshua Bloch所著的一本Java编程指南。这本书是基于第二版的更版本,目的是给Java程序员提供一些最佳实践和经验,以编写高效、可维护和可靠的Java代码。 这本书共分为15个章节,每个章节都讲解了一个与Java开发有关的重要主题。比如,章节一讲述了使用静态工厂方法代替构造器的优点,章节二则介绍了如何用Builder模式来构建复杂的对象。此外,书还提及了Java对象的等价性、覆盖equals方法和hashCode方法、避免创建不必要的对象、使用泛型、枚举、lambda表达式等等。 《Effective Java第三版》通过具体的代码示例和清晰的解释来说明每个主题的关键概念,使读者能够更好地理解和应用。此外,书还提供了一些实用的技巧和技术,例如避免使用原始类型、尽量使用接口而非类来定义类型等。 总的来说,这本书提供了很多实用的建议和技巧,可以帮助Java开发者写出高质量的代码。无论是初学者还是有经验的开发者,都可以从受益匪浅。无论你是打算从头开始学习Java编程,还是已经有一定经验的开发者,这本书都是值得推荐的读物。 ### 回答2: 《Effective Java 第三版》是由Joshua Bloch 所著的一本Java编程指南,是Java程序员必读的经典之作。该书共包含90个条目,涵盖了各种Java编程的最佳实践和常见问题的解决方法。 本书分为多个部分,每个部分都侧重于一个特定的主题。作者探讨了Java编程的各种问题和挑战,并提供了解决方案和建议。这些建议包括如何选择和使用合适的数据结构和算法,如何设计高效的类和接口,如何处理异常和错误,以及如何编写可读性强的代码等等。 《Effective Java 第三版》还关注了Java编程的性能优化和安全性问题。作者强调了遵循Java语言规范、使用标准库、防范常见安全漏洞等重要原则。此外,本书还介绍了Java 8及其后续版本的特性和用法,如Lambda表达式、流式编程和Optional类等。 这本书的特点之一是每个条目都独立于其他条目,可以单独阅读和理解。每个条目开头都有一个简洁的总结,让读者能够快速掌握主要观点。此外,书还有大量的示例代码和解释,帮助读者更好地理解和运用所学知识。 总的来说,《Effective Java 第三版》是一本非常实用和全面的Java编程指南。它适用于各个层次的Java程序员,无论是初学者还是有经验的开发人员,都可以从获得宝贵的经验和知识。无论是编写高质量的代码、优化性能还是确保安全性,这本书都是一本不可或缺的参考书籍。 ### 回答3: 《Effective Java 第3版(文版)》是由 Joshua Bloch 所著的一本关于使用 Java 编程语言的指南书。该书是对 Java 语言的最佳实践的详尽描述,为高级 Java 开发人员提供了许多实用的建议和技巧。 该书的主要内容包括Java 语言的优雅编程风格、类和接口的设计、Lambda 表达式和流的使用、泛型、异常和并发编程等方面的最佳实践。 在《Effective Java 第3版(文版)》,许多传统的 Java 开发的陷阱、常见错误和不良习惯都得到了深入的剖析和解答。它不仅提供了可供开发人员参考的示例代码,还解释了为什么某种方式是有问题的,以及如何更好地进行改进。 该书的深度和广度非常适合正在努力提高 Java 编程技能的开发人员。它涵盖了多个关键领域,为读者提供了在实际项目解决常见问题的方法和思路。 此外,《Effective Java 第3版(文版)》还介绍了最版本的一些特性和改进。例如,它详细说明了如何正确地使用 Java 8 增的 Lambda 表达式和流,以及如何充分利用 Java 9、10 和 11 功能。 总之,这本书是 Java 开发人员必备的指南之一。通过深入理解和应用书的实践建议,读者可以更加高效地编写、优化和维护 Java 代码。无论是想提升职业技能还是在项目减少错误和问题,这本《Effective Java 第3版(文版)》都是一本非常有帮助的参考书。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

衣舞晨风

给作者加个鸡腿呗

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值