美文网首页effective Java
《Effective Java》读书笔记 —— 方法

《Effective Java》读书笔记 —— 方法

作者: 666真666 | 来源:发表于2017-04-11 17:51 被阅读61次

本文大多数内容适用于构造器,也适用于普通方法,焦点集中在可用性、健壮性和灵活性上。

1.检查参数的有效性

一个原则:应该在发生错误之后尽快检测出错误

不检查参数有效性的后果:

  • 方法在处理的过程中失败,并且产生令人费解的异常
  • 方法正常返回,但计算出错误的结果
  • 方法正常返回,但却使得某个对象处于被破坏的状态,将来在某个不确定的时候引发错误

常见方法参数的一些限制:

  • 索引值必须是非负数
  • 集合类的索引不能大于集合长度-1
  • 对象引用不能为 null

public 方法参数有效性检查

步骤:

  1. 用 Javadoc 的@throw标签在文档中说明违法参数值限制时抛出的异常,异常通常为 IllegalArgumentException、IndexOutOfBoundsException、NullPointerException等
  2. 公有方法内部进行参数有效性检查,并抛出相应异常
    /**
     * Returns a BigInteger whose value is.
     * @param m
     * @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/package-private 方法参数有效性检查

非公有方法通常应该使用断言来检查它们的参数有效性

断言与普通有效性检查的区别:

  • 断言如果失败,抛出 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;
        // next
    }

构造方法参数有效性检查

对于有些参数,方法本身没有用到,但被保存起来供以后使用。比如静态工厂方法、构造方法。检查参数有效性非常重要,避免构造出来的对象违反了这个类的约束条件

不需要检查参数有效性的情况

  • 有效性检查很昂贵,或者不切实际,比如 sort 方法,检查集合中每一个对象是否可以比较
  • 计算会隐式执行必要的有效性检查,比如 sort 方法,如果对象不能比较,就会抛出 ClassCastException

总结

  • 编写方法前,考虑好它的参数有哪些限制
  • 把限制写到方法开头的文档中
  • 通过显式的检查来实施限制

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

先介绍一个概念,不可变性,之前介绍过,要尽可能创建不可变的类,因为它有很多优点,其中创建不可变类有几条规则:
- 如果类具有指向可变对象的域,必须确保该类的客户端无法获得执行这些对象的引用
- 在构造器中,永远不要用客户端提供的对象引用来初始化这样的域
- 在访问方法中,也不要返回该对象引用
- 在构造器、访问方法和 readObject 方法中请使用保护性拷贝技术

创建 Period 类,由于Date类是可变的,所以外部可能拿到内部的 start 和 end 信息,进而会修改这个信息

    public final class Period {
        private final Date start;
        private final Date end;

        public Period(Date start, Date end) {
            this.start = start;
            this.end = end;
        }
        
        public Date start() {
            return start;
        }
        
        public Date end() {
            return end;
        }
    }

对于构造器的每个可变参数进行保护性拷贝

为了避免内部信息被攻击,对于构造器的每个可变参数进行保护性拷贝,创建新的对象,而不是使用客户端传入的对象

    public Period(Date start, Date end) {
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());
    }

注意:

  • 保护性拷贝是在检查参数有效性之前进行
  • 保护性拷贝,没有使用 clone 方法,因为 Date 是非 final 的,clone 方法不能保证一定会返回 Date 对象,可能返回出于恶意目的而设计的不可信子类的实例

使访问方法返回可变内部域进行保护性拷贝

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

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

总结

  • 参数(和返回值)的保护性拷贝不仅仅针对不可变类,每当允许客户提供的对象进入内部数据结构中,则有必要考虑一下,客户提供的对象是否有可能是变化的
  • 长度非零的数组总是可变的,在把内部数组返回给客户端时,总要进行保护性拷贝
  • 真正的启示:尽可能使用不可变对象,不必再担心保护性拷贝
  • 对于Date,通常不要直接使用Date的引用,而是使用Date.getTime()返回的long基本类型作为时间的表示,防止Date对象的可变性导致的问题

最后,如果类具有从客户端得到或者返回到客户端的可变组件,类就必须保护性地拷贝这些组件,如果拷贝的成本受到限制,并且类信任它的客户端不会不恰当地修改组件,就可以在文档中指明客户端的职责是不得修改受到影响的组件,以此来代替保护性拷贝。

3.谨慎设计方法签名

API 设计技巧总结:

  • 谨慎地选择方法的名称
    • 易于理解
    • 与同一个包中的其他名称风格一致的名称
    • 选择与大众认可的名称
  • 不要过于追求提供便利的方法
  • 避免过长的参数列表
    • 四个参数或更少

缩短长参数列表的方式:

  • 把方法分解成多个方法,每个方法只需要这些参数的一个子集
  • 创建辅助类,用来保存参数的分组,一般是静态成员类,如果一个频繁出现的参数序列可以被看作代表了某个独特实体,则建议使用这种方式
  • 使用Builder模式,参数很多,且有些是可选的。

类参数的使用技巧:

  • 对于参数类型,要优先使用接口而不是类
    • 比如,没有理由使用 HashMap 作为参数,应当使用 Map 接口作为参数
  • 对于 boolean 参数,要优先使用两个元素的枚举类型
    • 代码更易于阅读和编写

4.慎用重载

重载方法的选择是静态的,是在编译时做出决定的,只能调用与此明确对应的重载方法,而不是其父类或者子类。与此不同的是覆盖方法,覆盖方法是动态的,是在运行时决定要调用子类还是父类的方法。

看一个例子。

private class CollectionClassifier {
    
    public static String classify(Set<?> s) {
        return "Set";
    }

    public static String classify(List<?> s) {
        return "List";
    }

    public static String classify(Collection<?> s) {
        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”,对于for循环的三次迭代,只能调用在编译时确定的参数为 Collection<?> 的方法。

解决方案:用单个方法替换三个重载方法

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

普通方法避免重载

具体,对于write方法,如果就有变形,不应该使用重载,而是增加诸如writeBoolean、writeInte这样的签名方法。

构造器方法避免重载

不能使用不同名称的构造器,但可以选择导出静态工厂,而不是重载构造器

总结

“能够重载方法”并不意味着“应该重载方法”。一般情况下,对于多个具有相同参数数目的方法来说,应该尽量避免重载方法。

5.慎用可变参数

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

可变参数性能问题:可变参数方法的每次调用功能会导致进行一次数组分配和初始化。

只有对于参数数目不确定的情况,才会使用可变参数。

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

对于一个返回null而不是零长度的数组或者集合的方法,编写客户端的程序员很可能会忘记这种专门的代码来处理null返回值。

返回零长度数组不会增加开销,零长度数组是不可变的,是自由共享的。

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

相关文章

网友评论

    本文标题:《Effective Java》读书笔记 —— 方法

    本文链接:https://www.haomeiwen.com/subject/dvyuattx.html