Effective Java-通用程序设计

本章内容导图:


1.将局部变量的作用域最小化

同“使类和成员的可访问性最小化”一样,将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。
Java允许在任何可以出现语句的地方声明变量,所以,最好的方法就是在第一次使用它的地方声明并初始化

循环中提供了特殊的机会来将变量的作用域最小化,它们的作用域被限定在正好需要的范围之内。如果在循环终止之后不再需要循环变量的内容,for循环就优先于while循环。

for (Element e : c) {
    doSomething(e);
}

for (Iterator i = c.iterator(); i.hasNext(); ) {
    doSomething(i.next());
}

//对比之下,for/for-each循环要优于while循环
Iterator<Element> i = c.iterator();
while(i.hasNext()) {
    doSomething(i.next());
}

另外,与while循环相比,for循环更简短,可读性更强。

还有种“使局部变量作用域最小化”的方法是:使方法小而集中。方法小而功能集中,就可以减少变量的个数,方法中的局部变量仅和当前功能有关。

使局部变量作用域最小化的方法有三个:
1.在第一次使用的地方声明并初始化
2.优先使用for/for-each循环
3.使方法小而集中

2.for-each循环优先于传统的for循环

同传统的for循环相比,for-each循环可以隐藏迭代器或者索引变量,避免了混乱和出错的可能。

for (Iterator i = c.iterator(); i.hasNext(); ) {
    doSomething((Element) i.next);
}

for (int i = 0; i < a.length; i++) {
    doSomething(a[i]);
}

//隐藏了迭代器or索引变量
for (Element e : elements) {
    doSomething(e);
}

在对多个集合进行嵌套式迭代时,for-each循环相对于传统for循环的这种优势更加明显。

for-each循环不仅可以遍历集合和数组,它还可以遍历任何实现Iterable接口的对象。如果编写的类型表示的是一组元素,即使不实现Collection,也要实现Iterable。

public interface Iterable<E> {
    Iterator<E> iterator();
}

for-each循环在简洁性和预防bug方面有着传统的for循环无法比拟的优势,并且没有性能损失。

3.了解和使用类库

使用标准库的好处有:

  1. 使用标准类库,可以充分利用这些编写标准库的专家的知识,以及其他人的使用经验。
  2. 不必浪费时间为那些与工作不太相关的问题提供特别的解决方案,把时间花在应用程序上,而不是底层的细节上。
  3. 标准库的性能往往会随着时间的推移而不断提高。
  4. 可以使自己的代码融入主流,更易读、易维护、更容易被其他开发人员重用。

应该熟悉java.lang、java.util、java.io中的内容。

4.如果需要精确的答案,请避免使用float和double

float和double类型主要是为了科学计算和工程计算而设计的。它们执行二进制浮点计算,是为了在广泛的数值范围上提供较为精确的快速近似计算而精心设计的,它们并没有提供完全精确的结果,不应该被用于需要精确结果的场合。尤其不适合用于货币计算。

5.基本类型优先于装箱基本类型

每个基本类型都有一个对应的引用类型,称作装箱基本类型。Java1.5增加了自动装箱和自动拆箱。这些特性模糊了基本类型和装箱基本类型的区别,但它们还是有差别的。
基本类型和装箱基本类型之间有三个主要的区别
1.基本类型只有值,装箱基本类型则具有与它们的值不同的同一性(两个装箱基本类型可以具有相同的值和不同的同一性)。
2.基本类型只有功能完备的值,而每个装箱基本类型除了它对应基本类型的所有功能值之外,还有个非功能值:null。
3.基本类型通常比装箱基本类型更节省时间和空间。

public class Unbelievable {
    static Integer i;

    public static void main(String[] args) {
        if (i == 42) {
            System.out.println("Unbelievable");
        }
    }
}

上述程序在计算i == 42的时候将抛出NullPointerException异常。因为i被声明为基本装箱类型,它的初始值默认是null,在计算i == 42时将执行自动拆箱,null引用被自动拆箱就会抛出NullPointerException异常。

当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱。

    public static void main(String[] args) {
        Long sum = 0L;
        for (long i = 0; i < Integer.MAX_VALUE; i++) {
            sum += i;
        }
        System.out.println(sum);
    }

上述代码运行起来要比预计的慢很多,因为它将sum声明为装箱基本类型Long,而不是基本类型long。程序虽可正常执行,但变量被反复地装箱和拆箱,导致明显的性能下降。

装箱基本类型的用途有
1.作为集合中的元素、键、值
2.在参数化类型中
3.在进行反射的方法调用时

当可以选择的时候,基本类型要优先于装箱基本类型。基本类型更加简单,也更加快速。
如果必须使用装箱基本类型,要特别小心,自动装箱减少了使用装箱基本类型的繁琐性,但并没有减少它的风险。
1.当程序用==比较两个装箱基本类型时,它做了个同一性比较,两个装箱基本类型可以具有相同的值和不同的同一性。
2.当程序进行涉及装箱和拆箱基本类型的混合类型计算时,它会进行拆箱,当程序进行拆箱时,会抛出NullPointerExecption异常。
3.当程序装箱了基本类型值时,会导致高开销和不必要的对象创建。

6.如果其他类型更适合,则尽量避免使用字符串

不应该使用字符串的情形有:
1. 字符串不适合代替其他的值类型
当一段数据从文件、网络、键盘设备进入到程序之后,它通常以字符串的形式存在,自然的倾向是让它继续保留这种形式,但这是很不恰当的。它应该转化为同应用情景更匹配的类型。
2. 字符串不适合代替枚举类型
枚举类型比字符串更加适合用来表示枚举类型的常量。
3. 字符串不适合代替聚集类型
如果一个实体有多个组件,用一个字符串来表示这个实体通常是很不恰当的。
4. 字符串不适合代替能力表(capabilities)

如果可以使用更加适合的数据类型,就应该避免使用字符串来表示对象。使用不当,字符串会比其他类型更加笨拙、更不灵活、速度更慢、更易出错。
经常被错误地用字符串来代替的类型包括基本类型、枚举类型和聚集类型。

7.当心字符串连接的性能

字符串连接操作符(+)是把多个字符串合并为一个字符串的便利途径。要产生单独一行的输出,或构造一个字符串来表示一个较小的、大小固定的对象,使用连接操作符是非常适合的。字符串连接符不适合运用于大规模的场景中,因为字符串是不可变的,当两个字符串被连接在一起时,它们的内容都要被拷贝,会对性能带来较大的影响。
为了获得可以接受的性能,需使用StringBuilder替代String。

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

//使用StringBuilder优化性能
public String statement() {
    StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH);
    for (int i = 0; i < numItems(); i++) {
        b.append(lineForItem(i));
    }
    return b.toString();
}

不要使用字符串连接符合并多个字符串,应该使用StringBuilder的append方法。

8.通过接口引用对象

优先适用接口而不是类来引用对象。
如果有合适的接口类型存在,对于参数、返回值、变量、域,就都应该使用接口类型进行声明。只有当利用构造器创建某个对象的时候,才真正需要引用这个对象的类。

面向接口编程是面向对象编程的一个很重要的设计原则,它使得程序更加灵活,耦合性更低。

不适用接口引用对象的三种情形为
1.没有合适的接口存在,如值对象String、BigInteger。
2.对象属于一个框架,而框架的基本类型是类,而不是接口。
3.类实现了接口,但是它提供了接口中不存在的额外方法,如果程序依赖于这些额外的方法,这种类就应该只被用来引用它的实例。如LinkedHashMap。

值类很少会用多个实现编写,它们通常是final的,并且很少有对应的接口。

9.接口优先于反射机制

核心反射机制java.lang.reflect提供了通过程序来访问已装载类的信息的能力。给定一个Class实例,你可以获得Constructor、Method和Field实例,分别代表了该Class类所表示的类的构造器、方法、域。这些对象提供了通过程序来访问类的成员名称、域类型、方法签名等信息的能力

Constructor、Method和Field实例使你能够通过反射机制操作它们的底层对等体:通过调用Constructor、Method、Field实例上的方法,可以构造底层类的实例、调用底层类的方法,访问底层类中的域。

反射功能很强大,但也会带来一些负面的影响:
1.丧失了编译时类型检查的好处
2.执行反射访问所需要的代码非常笨拙和冗长
3.性能损失

如果只是以非常有限的形式使用反射机制,虽然也要付出少许代价,但是可以获得许多好处。对于有些程序,它们必须用到在编译时无法获取的类,但是在编译时存在适当的接口或者超类,通过它们可以引用这个类。若是这种情况,就可以以反射方式创建实例,然后通过它们的接口或者超类,以正常的方式访问这些实例

public class InterfaceReference {
    
    public static void main(String[] args) {
        Class<?> cl = null;
        
        try {
            cl = Class.forName("java.util.HashSet");
        } catch (ClassNotFoundException e) {
            System.out.println("class not found");
            System.exit(1);
        }
        
        Set<String> set = null;
        try {
            set = (Set<String>) cl.newInstance();
        } catch (InstantiationException e) {
            System.out.println("class not instantiable.");
            System.exit(1);
        } catch (IllegalAccessException e) {
            System.out.println("class not accessible");
            System.exit(1);
        }
        //以接口的方式访问实例
        set.addAll(Arrays.asList("Java", "Kotlin", "Python"));
        System.out.println(set);
    }
}

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

10.谨慎地使用本地方法

从历史上看,本地方法主要有三种用途
1.提供了“访问特定平台”的能力,比如访问注册表和文件锁。
2.提供了访问遗留代码库的能力,从而可以访问遗留数据。
3.通过本地语言,编写应用程序中注重性能的部分,从而提高系统的性能。

使用本地方法来访问特定平台的机制是合法的;使用本地方法来访问遗留代码也是合法的;使用本地方法来提高性能的做法不值得提倡,因为JVM的优化功能已经做的很好了。

使用本地方法有一些严重的缺点:
1.本地语言是不安全的,使用本地方法有内存毁坏的风险。
2.本地语言是与平台相关的,程序不能再自由移植。
3.使用本地方法的程序更难调试。
4.需要“胶合程序”的本地方法编写起来单调乏味且难以阅读。

在使用本地方法之前务必三思。极少数情况下会需要使用本地方法来提高性能。如果你必须要使用本地方法来访问底层的资源,或者遗留代码库,也要尽可能少用本地代码,并且要进行全面测试,因为本地方法的一个bug就有可能破坏整个程序。

11.谨慎地进行优化

优化的弊大于利,特别是不成熟的优化。在优化的过程中,产生的软件可能既不快速,也不正确,而且还不容易修正。

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

在设计过程中必须要考虑性能问题,因为实现上的问题可以通过后期的优化而得到修正,但遍布全局且限制性能的结构缺陷几乎是不可能被改正的。

设计的过程中要努力避免那些限制性能的设计决策,还要考虑API设计决策的性能后果

不要费力去编写快速的程序--应该努力编写好的程序,速度自然会随之而来。
在设计系统的时候,特别是设计API、线路层协议、永久数据格式的时候,一定要考虑性能因素
当构建完系统之后,要测量它的性能,如果性能达不到要求,要通过性能剖析器,找到问题的根源,设法优化相关的部分。第一个步骤是检查所选择的算法:再多的底层优化也无法弥补算法的选择不当。在每次改变之后都要测量性能,直到满意为止。

12.遵守普遍接受的命名惯例

通常,命名惯例分为两大类:字面的语法的

名称应该是层次状的,用“.”分割每个部分,每个部分包括小写字母。
类和接口的名称(包括枚举和注解)的名称,都应该包括一个或多个单词,每个单词首字母大写,如Timer、TimerTask,应该避免使用缩写,除非是一些首字母缩写和一些通用的缩写,如max和min。
方法和域的名称与类和接口的名称一样,都遵守相同的字面惯例,只不过方法和域的名称的第一个字母应该小写如remove、ensureCapacity。
局部变量名称的字面惯例与成员名称类似,只不过它允许缩写,单个字符和短字符序列的意义取决于局部变量所在的上下文环境。如i、xref、houseNumber。
类型参数名称通常由单个字母组成,这个字母通常是以下五种类型之一:
1.T表示任意的类型
2.E表示集合的元素类型
3.K和V表示映射的键和值类型
4.X表示异常
5.任何类型序列可以是T、U、V或者T1、T2、T3

语法命名惯例比字面惯例更加灵活。
没有语法命名惯例。
通常用一个名词或者名词短语命名,如Timer、BufferedWriter、ChessPiece。
接口的命名与类相似,如Collection、Comparator、或者用一个以-able-ible结尾的形容词来命名,如Runnable、Iterable、Accessible。
执行某个动作的方法通常用动词或者动词短语来命名,如append、drawImage。
返回boolean值的方法通常以is开头,后面跟名词或名词短语,或者任何具有形容词功能的单词或短语,如isDigit、isEmpty、isEnabled。
返回一个非boolean值的方法通常用名词、名词短语、或者get开头的动词短语来命名,如size、hashCode、getTime。如果方法所在的类是个Bean,就要强制使用以get开头的形式。
有些方法名称值得专门提及:
1.转换对象类型的方法、返回不同类型的独立对象的方法,通常被称为toType,如toString、toArray。
2.返回视图的方法通常被称为asType,如asList。
3.返回一个与被调用对象同值的基本类型方法,通常被称为typeValue,如intValue。
4.静态工厂常命名为valueOf、of、getInstance、newInstance、getType、newType

应该把标准的命名惯例当作一种内在的机制来看待,并且学着用它们作为第二特性。
命名是极为重要的,好的命名本身具有自注释的功能,可以大大的提高代码的可读性。很多经典书籍中都有介绍对程序元素命名的技巧,可以参考学习。
这类书籍有《编写可读代码的艺术》 、《代码整洁之道》、《实现模式》、《重构》等。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容