Effective java笔记5--通用程序设计

时间:2021-08-01 08:31:29

一、将局部变量的作用域最小化

     本条目与前面(使类和成员的可访问能力最小化)本质上是类似的。将局部变量的作用域最小化,可以增加代码的可读性和可维护性,并降低出错的可能性。

使一个局部变量的作用域最小化,最有力的技术是在第一次使用它的地方声明。

几乎每一个局部变量的声明都应该包含一个初始化表达式。

在循环中经常要用到最小化变量作用域这一个规则。for循环使你可以声明循环变量(loop varialbe),它们的作用域被限定在正好需要的范围值内(这个范围包括循环体,以及循环体之前的初始化、测试、更新部分)。因此,如果在循环终止之后循环变量的内容不再被需要的话,则for循环优于while循环。
例如,下面是一种对集合进行迭代的首选做法:

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

对比while循环,以及一个错误:

Iterator i = c.iterator();
while(i.hasNext()){
doSomething(i.next());
}
... Iterator i2 = c2.iterator();
while(i.hasNext()){ //BUG!
doSomethingElse(i2.next());
}

如果类似的”剪切-粘贴“错误发生在前面for循环的用法中,则结果代码根本不能通过编译。

for循环比while循环的另外一个优势,使用for循环要少一行代码,有助于把方法装入到大小固定的编辑器窗口中,从而增强可读性。

二、了解和使用库

1、通过使用标准库,你可以充分利用这些编写标准库的专家知识,以及在你之前其他人的使用经验。

2、使用标准库,它们的性能会不断提高,而无需你做任何努力。

3、标准库也会随着时间而增加新的功能。

4、使用标准库,会使自己的代码融入主流。

三、如果要求精确的答案,请避免使用float和double

float和double类型的主要设计目标是为了科学计算和工程计算。它们执行二进制浮点运算,这是为了在广域数值范围上提供较为精确的快速近似计算而精心设计的。然而没哟提供完全精确的结果,所以不应该被用于要求精确结果的场合。float和double类型对于货币计算尤为不合适。因为要让一个float或者double精确地表达0.1(或者10的任何其他负数次方值)是不可能的。

例如,假设你的口袋中有$1.03,花掉了42之后还剩下多少钱呢?下面是一个很简单的程序片段,企图回答这个问题:

System.out.println(1.03-.42);

不幸的是,它的输出结果是0.6000000000001.

要解决这个问题正确的方法是使用BigDecimal、int或者long进行货币计算。然而,使用BigDecimal有两个缺点:与使用原语运算类型相比,这样做很不方便,而且更慢。

总而言之,对于有些要求精确答案的计算任务,请不要使用float或者double。如果你希望系统来处理十进制小数点,并且不介意因为不使用原语类型而带来的不便,那么请使用BigDecimal.使用BigDecimal还有一个额外的好处,它允许你完全控制舍入:当一个操作涉及到舍入的时候,它让你从8种舍入模式中选择其一。如果你正在进行商务计算,并且要求特别的舍入行为,那么使用BigDecimal是非常方便的。

如果性能非常关键,并且你又不介意自己处理十进制小数点,而且所涉及的数值又不太大,那么可以使用int或者long(采用更小的单位,如上面采用美分)。如果数值范围没有超过9位十进制数字,则你可以使用int;如果不超过18位数字,则可以使用long。如果数值范围超过了18位数字,你就必须使用BigDecimal。

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

字符串被用来表示文本,它在这方面也确实做得很好。因为字符串很常用,并且java也支持的很好,所以自然地就会有这样一种倾向:即使在不适合于使用字符串的场合,人们往往也会 使用字符串。

字符串不适合代替其他的值类型。

字符串不适合代替枚举类型。

字符串不适合代替聚集类型。

字符串也不适合代替能力表(capabilities)。

五、了解字符串连接的性能

字符串连接操作符(+,string concatenation operator)是把多个字符串合并为一个字符串的便利途径。为连接n个字符串而重复地使用字符串连接操作符,要求n的平方级的时间。这是由于字符串是非可变的而导致的不幸结果。当两个字符串被连接的时候,它们的内容都要被拷贝。

为了获得可接受的性能,请使用StringBuffer替代String,用来存储构造过程中的账单声明:

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

+做法的开销随项目数量呈平方级增加,StringBuffer做法是线性增加,所以,项目越大,性能的差别会越显著。

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

六、通过接口引用对象

例如,Vector是List接口的一个实现,在声明变量的时候应该养成这样的习惯:

//Good -uses interface as type
List subscribers = new Vector(); //而不是这样声明:
//Bad -uses class as type!
Vector subscribers = new Vector();

如果你养成了使用接口作为类型的习惯,那么你的程序将会更加灵活。

如果没有合适的接口存在的话,那么,用类而不是接口引用一个对象,是完全合适的。

第一、值类(value class),比如String和BigInteger。记住,值类很少有多种实现。它们通常是完全合适的。更一般地,如果一个具体类没有相关联的接口,那么不管它是否表示一个值,你都没有别的选择,只有通过这个类来引用它的对象。
第二、对象属于一个框架,而框架的基本类型是类,不是接口。如果一个对象属于这样一个基于类的框架(class-based framework),那么应该用相关的基类(base class)来引用这个对象,而不是使用它的实现类。java.util.TimerTask类就属于这种情形。
第三、一个类实现了一个接口,但是它提供了接口中不存在的额外方法--例如LinkedList。如果程序依赖于这些额外的方法,那么这样的类应该只被用来引用它的实例:它永远也不应该被用作参数类型。

七、接口优先于映像机制

映像设施(reflection facility)java.lang.reflect提供了”通过程序访问关于已装载的类的信息“的能力。映射机制(reflection)允许一个类使用另一个类,即使当前者被编译的时候后者还根本不存在,然而,这种能力也需要付出代价:

  • 你损失了编译时类型检查的好处,也包括异常检查,如果一个程序企图用映射方式调用一个不存在的方法,或者一个不可访问的方法,那么在允许时它将会失败,除非你采取了特别的预防措施。
  • 要求执行映射访问的代码非常笨拙和冗长。编写这样的代码非常乏味,阅读这样的代码页很困难。
  • 性能损失。映射机制调用比普通方式调用慢2倍。

如果只是在很有限的情况下使用映像机制,那么虽然也会付出少许代价,但你可以获得许多好处。

八、谨慎地使用本地方法

Java Native Interface(JNI)允许Java应用可以调用本地方法(native method),所谓本地方法是指本地程序设计语言(比如C或者C++)来编写特殊方法。

从历史上看,本地方法主要有三种途径:

  • 它们提供了”访问于平台相关的设施“的能力,比如访问注册表(registry)和文件锁。
  • 它们也提供了访问老式代码库的能力,通过这些老式代码库进一步可以访问老式数据(legacy data)。
  • 应用程序可以使用本地语言,实现性能关键部分,以提供系统的性能。(随着高版本的出现,这个方法已经不提倡)

九、谨慎的进行优化

     优化的三条格言:

  •      很多计算上的过失都被归咎于效率原因(没有获得必要的效率),而不是其他的原因---甚至包括盲目地做傻事。
  • 不要去计较一些小的效率上的得失,在97%的情况下,不成熟的优化是一切问题的根源。
  • 在优化方面的两条规则:

A、规则1:不要做优化。

B、规则2:(仅针对专家)还是不要做优化---也就是说,在你还没有绝对清晰的未优化方案之前,请不要做优化。

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

Java平台有一套建立得很好的命名惯例(naming convention),其中许多被包含在The java language specification中。不严格地讲,这些命名惯例分为两大类:字面的(typographical)和语法的(grammatical)。

字面命名惯例:
包的名字应该是层次状的,用句号分隔每一部分。每一部分包括小写字母和数字(很少使用数字)。如果你的包将在你的组织之外被使用,那么包的名字应该以你的组织的Internet域名作为开头,并且*域名放在前面,例如,edu.cmu、com.sun、gov.nsa。标准库和一些课选的库,其名字以java和javav作为开头,它们是这条规则的例外。
包名字的剩余部分应该包括一个或者多个描述该包的组成部分。通常不超过8个字符。鼓励使用有意义的缩写形式,例如,使用util而不是utilities,只取首字母的缩写形式也是可以接受的。如awt。
类和接口的名字应该包括一个或者多个单词,每个单词的首字母大写,缩写应该尽量避免,除非是一些首字母缩写和一些通用的缩写,如max和min等。

方法和域的名字与类和接口的名字遵循相同的字面惯例,只不过方法或者域名的名字第一个字母应该小写,如remove、ensureCapacity.

“常量域”的名称应该包含一个或者多个大写的单词,中间用下划线符号隔开。

局部变量名称的字面命名惯例与成员名称类似,只不过它也允许缩写。

类型参数名称通常由单个字母组成,这个字母通常是以下五种类型之一:T表示任意的类型,E表示集合的元素类型,K和V表示映射的键和值类型,X表示异常。

语法惯例:

语法命名惯例比字面惯例更灵活,也更有争议。

对于包而言,没有语法命名惯例。

类通常用一个名词或者名词短语命名。如Timer或者BufferedWriter

接口的命名与类相似,如,Collection。或者用一个以“-able”或“-ible”结尾的形容词来命名。如Runnable或Accessible。

由于注解类型有那么多用处,因此没有单独安排词类。

执行某个动作的方法通常用动词或者动词短语来命名。对于返回boolean值的方法,其名称往往以单词“is”开头,后面跟名词或名词短语,或者任何具有形容词功能的单词或短语。

如果方法返回被调用对象的一个非boolean的函数或者属性,它通常用名词、名词短语,或者以动词“get”开头的动词短语来命名。

如果方法所在的类是个Bean,就要强制使用以“get”开头的形式。

转换对象类型的方法、返回不同类型的独立对象的方法,通常被称为toType,例如toString和toArray。返回视图的方法通常被称为asType,例如asList。返回一个与被调用对象同值的基本类型的方法,通常被称为typeValue,例如intValue。静态工厂的常用名称为valueOf、of、getInstance、netInstance、getType和NewType。

boolean类型的域的命名与boolean类型的访问方法很类似,但是省去了初始的“is”。其他类型的域通常用名词或者名词短语来命名。局部变量的语法惯例类似于域的语法惯例,但是更弱一些。