Effective Java——通用程序设计(下)

                        目录
五十一、当心字符串连接的性能
五十二、通过接口引用对象
五十三、接口优先于反射机制
五十四、谨慎地使用本地方法
五十五、谨慎地进行优化
五十六、遵守普遍接受的命名惯例


五十一、当心字符串连接的性能

        字符串连接操作(+)是把多个字符串合并为一个字符串的最为便利的途径。因此如果仅仅是对两个较小字符串进行一次连接并输出连接结果,这样是比较合适的。然而如果是为n个字符串而重复地使用字符串连接操作符,则需要n的平方级的时间。这是由于字符串对象本身是不可变的,在连接两个字符串时,需要copy两个连接字符串的内容并形成新的连接后的字符串。见如下代码:

public String statement() {
    String result = "";
    for (int i = 0; i < numItems(); i++) {
        result += lineForItem(i);
    }
    return result;
}

        此时如果项目数量巨大,这个方法的执行时间将难以估量。为了获得可以接受的性能,请使用StringBuilder替代String,见如下修正后的代码:

public String statement() {
    StringBuilder b = new StringBuilder(numItems * LINE_WIDTH);
    for (int i = 0; i < numItems(); i++)
        b.append(lineForItem(i));
    return b.toString();
}

        上述两种做法在性能上的差异是巨大的,如果numItems()返回100,而lineForItem返回一个固定长度为80的字符串,后者将比前者块85倍。由于第一种做法的开销是随项目数量呈平方级增加,而第二种做法是线性增加的,所以数目越大,差异越大。

        原则很简单:不要使用字符串连接操作符来合并多个字符串,除非性能不管紧要。相反,应该使用StringBuilder的append方法。另一种方法是,使用字符或者数组,或者每次只处理一个字符串,而不是将它们组合起来。


五十二、通过接口引用对象

        一般来讲,在函数参数、返回值、域变量等声明中,应该尽量使用接口而不是类作为它们的类型。只有当你利用构造器创建某个对象的时候,才真正需要引用这个对象的类,如:

List<Subscriber> subscribers = new Vector<Subscriber>();

        而不是像下面这样的声明:

Vector<Subscriber> subscribers = new Vector<Subscriber>();

        如果你养成了用接口作为类型的习惯,你的程序将更加灵活。对于上面的例子,在今后的改进中,如果不想使用Vector作为实例化对象,我们只需在如下一出进行修改即可:

List<Subscriber> subscribers = new ArrayList<Subscriber>();

        如果之前该变量的类型不是接口类型,而是它实际类型的本身,那么在做如此修改之前,则需要确认在所有使用该变量的代码行是否用到了Vector的特性,从而导致不行直接进行替换。如果该变量的接口为接口,我们将不受此问题的限制。

        那么在哪些情况下不是使用接口而是使用实际类呢?见如下情况:
        1. 没有合适的接口存在,如String和BigInteger等值类对象,通常它们都是final的,也没有提供任何接口。
        2. 对象属于一个框架,而框架的基本类型是类,不是接口。如果对象属于这种基于类的框架,就应使用基类来引用该对象,如java.util.TimerTask抽象类。
        3. 类实现了接口,但是它提供了接口中不存在的额外方法。如果程序此时依赖于这些额外的方法,这种类就应该只被用来引用他的实例。

        简而言之,如果类实现了接口,就应该尽量使用其接口引用该类的引用对象,这样可以使程序更加灵活,如果不是,则使用类层次结构中提供了必要功能的最基础的类。


五十三、接口优先于反射机制

        Java中提供了反射的机制,如给定一个Class实例,你可以获取Constructor、Method和Field等实例,分别代表了该Class实例所表示的类的Constructor(构造器)、Method(方法)和Field(域)。与此同时,这些实例可以使你通过反射机制操作它们的底层对等体。例如,Method.invoke使你可以调用任何类的任何对象上的任何方法。然而这种灵活是需要付出一定代价:
        1. 丧失了编译时类型检查的好处,包括异常检查和类型检查等。
        2. 执行反射访问所需要的代码往往非常笨拙和冗长,阅读起来也非常困难,通常而言,一个基于普通方式的函数调用大约1,2行,而基于反射方式,则可能需要十几行。
        3. 性能损失,反射方法的调用比普通方法调用慢了许多。

        核心反射机制最初是为了基于组件的应用创建工具而设计的。它们通常需要动态装载类,并且用反射功能找出它们支持哪些方法和构造器,如类浏览器、对象监视器、代码分析工具、解释性的嵌入式系统等。在通常情况下,如果只是以非常有限的形式使用反射机制,虽然也要付出少许代价,但是可以获得许多好处。对于有些程序,它们必须用到编译时无法获取的类,但是在编译时却存在适当的接口或超类,通过它们可以引用这个类。如果是这样,可以先通过反射创建实例,然后再通过它们的接口或超类,以正常的方式访问这些实例。见如下代码片段:

public static void main(String[] args) {
    Class<?> cl = null;
    try {
        c1 = Class.forName(args[0]);
    } catch (ClassNotFoundException e) {
        System.err.println("Class not found.");
        System.exit(1);
    }
    Set<String> s = null;
    try {
        s = (Set<String>)c1.newInstance();
    } catch (IllegalAccessException e) {
        System.err.println("Class not accessible");
        System.exit(1);
    } catch (InstantiationException e) {
        System.err.println("Class not instantiation.");
        System.exit(1);
    } 
    s.addAll(Arrays.asList(args).subList(1,args.length));
    System.out.println(s);
}

        上面的程序创建了一个Set<String>实例,它的类是由第一个命令行参数指定的。该程序把其余命令行参数插入到这个集合中,然后打印该集合。这些参数的打印顺序取决于第一个参数中指定的类。如果指定“java.util.HashSet”,这些参数就会以随机的顺序打印出来;如果指定“java.util.TreeSet”,则它们就会按照字母顺序打印出来。

        上面的代码中体现出了反射的两个缺点。第一,这个例子有3个运行时异常的错误,如果不使用反射方式实例化,这3个错误都会成为编译时错误。第二,根据类名生成它的实例需要20行冗长的代码,而调用构造器可以非常简洁的只使用一行代码。然而这些缺点仅仅局限于实例化对象的那部分代码。一旦对象被实例化,它与其他的Set实例就难以区分。

        简而言之,反射机制是一种功能强大的机制,对于特定的复杂系统编程任务,它是非常必要的。如果你编写的程序必须要与编译时未知的类一起工作,如有可能,就应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类。


五十四、谨慎地使用本地方法

        JNI允许Java应用程序可以调用本地方法,所谓本地方法是指用本地程序设计语言,如C/C++
来编写的特殊方法。本地方法在本地语言中可以执行任意的计算任务,并最终返回Java程序。它的主要用途就是访问一些本地的资源,如注册表、文件锁等,或者是访问遗留代码中的一些遗留数据。当然通过本地方法在有些应用场景中是可以大大提高提高系统执行效率的。
        随着Java平台的不断成熟,它提供了越来越多以前只有宿主平台才有的特性,如java.util.prefs和java.awt.SystemTray等。与此同时,随着JVM的不断优化,其效率也在不断的提高,因此只有在很少的情况下才会考虑使用JNI,使用本地方法来提高性能的做法不值得提倡。还需要指出的是,JNI中胶合Java和C++的代码部分非常冗长且难以理解。


五十五、谨慎地进行优化

        关于优化有个深刻的真理:优化的弊大于利,特别是不成熟的优化。不要因为性能而牺牲合理的结构,要努力编写好的程序而不是快的程序。如果好的程序不够快,它的结构将使它可以得到优化。好的程序体现了信息隐藏的原则:只要有可能,他们就会把设计决策集中在单个模块中,改变这个模块不会影响到系统其它部分。

        努力避免那些限制性能的设计决策。当一个系统设计完成之后,其中最难以更改的组件是那些指定了模块之间交互关系以及模块与外界交互关系的组件。这些设计组件之中,最主要的是API、线路层协议以及永久数据格式。这些设计组件不仅事后难以甚至不可能改变,而且它们都有可能对系统本该达到的性能产生严重的限制。

        在每次试图做优化之前和之后,要对性能进行测量。性能剖析工具有助于你决定应该把优化的重心放在哪里。代码越多,使用性能剖析器就显得越发重要。JDK带了简单的性能剖析器,现在的IDE也提供了更加成熟的性能剖析工具。

        总而言之,不要费力去编写快速的程序——应该努力编写好的程序,速度自然会随之而来。在设计系统的时候,特别是在设计API、线路层协议和永久数据格式的时候,一定要考虑性能的因素。当构建完系统之后,要测量它的性能。第一个步骤是检查所选择的算法:再多低层的优化也无法弥补算法的选择不当。


五十六、遵守普遍接受的命名惯例

        命名惯例大多来源于生活积累和项目规定。如:类型参数名称通常由单个字母组成。这个字母通常是以下五种类型之一:T表示任意的类型,E表示集合的元素类型,K和V表示映射的键和值类型,X表示异常。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值