美文网首页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