专栏首页维C果糖效率编程 之「方法」

效率编程 之「方法」

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

绝对多数方法和构造器对于传递给它们的参数值都会有某些限制。例如,索引值必须是非空的、对象引用不能为null等。我们应该在文档中清楚地指明所有这些限制,并且在方法体的开头处检查参数,以强制施加这些限制。对于非公有的方法,我们也可以使用断言来检查它们的参数,例如下面的冒泡排序方法:

private static void bubbleSort(int[] array) {
    // 使用断言
    assert array != null;

    // 冒泡排序核心算法
    for (int i = array.length - 1; i > 0; i--) {
        for (int j = 0; j < i; j++) {
            if (array[j] > array[j + 1]) {
                int temp = array[j];
                array[j] = array[j + 1];
                array[j + 1] = temp;
             }
        }
    }
}

从本质上将,断言是在声称被断言的条件将会为真,无论外围包的客户端如何使用它。不同于一般的有效性检查,如果断言失败,将会抛出AssertionError,如我们将null传递给上面的bubbleSort()方法,将会得到如下错误信息:

此外,如果要开启断言(默认是不开启断言模式的),需要我们手动配置VM启动参数。例如,在 IntelliJ IDEA 中,我们可以通过在VM options中设置-ea参数来开启断言:

简而言之,每当编写方法或者构造器的时候,我们应该考虑它的参数有哪些限制,也应该把这些限制写到文档中,并且在这个方法体的开头出,通过显式的检查来实施这些限制。

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

要假设类的客户端会尽其所能来破坏这个类的约束条件,因此我们必须保护性地设计程序。例如,考虑下面的类,它声称可以表示一段不可变的时间周期:

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类本身是可变的,因此很容易违反这个约束条件:

Date start = new Date();
Date end = new Date();

Period period = new Period(start,end);
System.out.println("Period: start = " + period.start + ", end = " + period.end);

end.setYear(78);
System.out.println("Period: start = " + period.start + ", end = " + period.end);

如上图所示,显然,在我们创建Period之后,其结束时间被改变了,打破我们设置的约束条件。为了保护Period实例的内部信息避免受到这种攻击,对于构造器的每个可变参数进行保护性拷贝是必要的,并且使用备份对象作为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(start + "after " + end);
}

如上图所示,用了新的构造器之后,上述的攻击对于Period实例不再有效。注意,保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始对象。同时也请注意,我们没有用Dateclone()方法来进行保护性拷贝,因为Date是非final的,不能保证clone()方法一定返回类为java.util.Date的对象:它有可能返回专门出于恶意的目的而设计的不可信任子类的实例。例如,这样的子类可以在每个实例被创建的时候,把指向该实例的引用记录到一个私有的静态列表中,并且允许攻击者访问这个列表。这将使得攻击者可以自由地控制所有的实例。为了阻止这种攻击,对于参数类型可以被不可信任方子类化的参数,请不要使用clone()方法进行保护性拷贝。虽然替换构造器就可以成功避免上述的攻击,但是改变Period实例仍然是有可能的,因为它的访问方法提供了对其可变内部成员的访问能力:

Date start = new Date();
Date end = new Date();

Period period = new Period(start,end);
System.out.println("Period: start = " + period.start + ", end = " + period.end);

period.end().setYear(78);
System.out.println("Period: start = " + period.start + ", end = " + period.end);

为了防御这第二种攻击,只需修改两个访问方法,使它返回可变内部域的保护性拷贝即可:

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

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

如上图所示,采用了新的构造器和新的访问方法之后,Period真正是不可变的了。值得一提的是,有经验的程序员通常使用Date.getTime()返回long基本类型作为内部的时间表示法,而不是使用Date对象引用,因为Date是可变的。同理,长度非零的数组总是可变的,因此在把内部数组返回给客户端之前,应该总要进行保护性拷贝;另一种方案是,给客户端返回该数组的不可变视图。简而言之,如果类具有从客户端得到或者返回到客户端的可变组件,类就必须保护性地拷贝这些组件。

第 3 条:谨慎设计方法签名以及慎用重载

遵守下面的建议,可以帮助我们设计一个比较好的方法签名:

  • 谨慎地选择方法的名称,方法的名称应该始终遵循标准的命名习惯;
  • 不用过于追求提供便利的方法,每个方法都应该尽其所能;
  • 避免过长的参数列表,目标是四个参数,或者更少,相同类型的长参数列表格外有害;
  • 对于参数类型,要优先使用接口而不是类;
  • 对于boolean类型的参数,要优先使用两个元素的枚举类型。

此外,对于方法的重载,我们也要特别注意,例如:

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

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

    public static String classify(Collection<?> collection) {
        return "Unknown Collection";
    }

    public static void main(String[] args) {
        Collection<?>[] collections = {
                new HashSet<String>(),
                new ArrayList<BigInteger>(),
                new HashMap<String, String>().values()
        };

        for (Collection<?> collection : collections) {
            System.out.println(classify(collection));
        }
    }
}

如上述程序所示,它的意图是好的,它试图根据一个集合是SetList,或者其他类型的集合,来对它进行分类。但实际上,运行该程序后,它并没有按我们的预期执行,而是连续打印了Unknown Collection三次。这是因为classify()方法重载了,而要调用哪个重载方法是在编译时做出决定的。对于for循环中的全部三次迭代,参数的编译时类型都是相同的:Collection<?>。虽然每次迭代的运行时的类型都是不同的,但这并不影响对重载方法的选择。因为该参数的编译时类型为Collection<?>,所以,唯一合适的重载方法是第三个:classify(Collection<?>),在循环的每次迭代中,都会调用这个重载方法。

对于重载方法的选择是静态的,而对于被覆盖方法的选择则是动态的。选择被覆盖的方法的正确版本是在运行时进行的,选择的依据是被调用方法所在对象的运行时类型。我们应该避免胡乱地使用重载机制,最安全而保守的策略是,永远都不要导出具有相同参数数目的重载方法。简而言之,“能够重载方法”并不意味着就“应该重载方法”。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Akka 指南 之「集群感知路由器」

    所有「routers」都可以知道集群中的成员节点,即部署新的路由(routees)或在集群中的节点上查找路由。当一个节点无法访问或离开集群时,该节点的路由将自动...

    CG国斌
  • 二叉树的前序、中序、后序、层序以及蛇形遍历的实现方式

    如上述所示,蛇形层次遍历的顺序为:先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行。

    CG国斌
  • 修改 IntelliJ IDEA 模板注释中的 user 内容

    在博文“ 设置 IntelliJ IDEA 主题和字体的方法 ”中,咱们进一步了解了 IntelliJ IDEA 的个性化设置功能,包括主题和字体的常用设置等,...

    CG国斌
  • Python中最快的格式化字符串方式

    第一种是传承自C语言printf函数的使用%占位符格式化字符串,如'%d' % 100,这种方式严格来说是使用%作为算数运算符进行的二元运算,而且有一个限制是只...

    ★忆先★
  • 使用DataV制作的一个数据报表

    之前接到一个做数据报表的需求,当时准备使用echarts自己画。后来考虑时间来不及,着急要,再加上一直在使用阿里云的产品,就在阿里云上个找了找数据大屏的服务。于...

    Ryan-Miao
  • 剑指 offer代码解析——面试题38数字在排序数组中出现的次数

    题目:统计一个有序数组中K出现的次数。 分析:本题最直观的思路就是遍历数组,统计K出现的次数即可。 这种方式的时间复杂度为O(n)。下面我们充分利用“有序...

    大闲人柴毛毛
  • 使用Galera部署MariaDB集群

    使用Galera进行MariaDB复制可为站点数据库添加冗余。通过数据库复制,多个服务器充当数据库集群。数据库群集对于高可用性网站配置特别有用。本教程使用三个单...

    圣人惠好可爱
  • Ribbon源码分析

    对于Spring中的AnnotationMetadata不太熟悉的同学,可以跑一下下面的CASE

    用户2032165
  • idea启动多个tomcat失败

    Intellij idea中,为在本地调试两个系统之间的调用,配置两个本地tomcat server,设置不同的端口号,如8081和8082,Deploy中加入...

    欢醉
  • 超级网络

    在这篇文章中,我将介绍一下我们最近的文章[1609.09106] HyperNetworks。我作为Google Brain Resident工作在这篇论文上-...

    DEXIN

扫码关注云+社区

领取腾讯云代金券