文章目录
8 通用程序设计
第45条 将局部变量的作用域最小化
C语言会要求局部变量在一个代码块的开头处进行声明。但是java不需要,Java允许在任何可以出现语句的地方声明变量。
**局部变量作用域最小化的最有力的方法就是在第一次使用它的地方声明。**在使用之前声明会分散读者的注意力,降低可读性。
几乎每个局部变量的声明都应该包含一个初始化表达式。
for循环优先于while循环 举例如下:
while循环在此方面的缺点。
Iterator<Element> i = c.iterator();
while(i.hasNext()) {
doSomething(i.next());
}
Iterator<Element> i2 = c2.iterator();
while(i.hasNext()) { // bug
doSomething(i2.next());
}
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
doSomething(i.next());
}
// compile-time error: can not find symbol : i;
for (Iterator<Element> i2 = c2.iterator(); i.hasNext(); ) {
doSomething(i2.next());
}
最后一种局部变量作用域最小化的方法就是:使方法小而集中。
第46条 for-each循环优先于传统的for循环
for-each循环并不会有性能损失。甚至在某些情况下比for循环还有优势,因为它对数组索引的边界值只计算一次。
// 看一个有问题的代码
/**
* Exception in thread "main" java.util.NoSuchElementException
at java.util.AbstractList$Itr.next(AbstractList.java:364)
at rule46.Poker.main(Poker.java:36)
**/
package rule46;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
enum Suit {club, diamond, heat, spade}
enum Rank {one, two, three, four, five, six, seven, eight, nine, ten}
class Card {
Suit suit;
Rank rank;
public Card(Suit suit, Rank rank) {
this.suit = suit;
this.rank = rank;
}
}
public class Poker {
public static void main(String[] args) {
Collection<Suit> suits = Arrays.asList(Suit.values());
Collection<Rank> ranks = Arrays.asList(Rank.values());
List<Card> deck = new ArrayList<Card>();
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); ) {
for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); ) {
deck.add(new Card(i.next(), j.next()));
}
}
}
}
上述代码的问题就在于suits调用了太多次的next,suits的next应该在外层循环调用。这里在内层循环调用了多次。 for-each循环就完美的避免了这种问题。
for (Suit suit : suits) {
for (Rank rank : ranks) {
deck.add(new Card(suit, rank));
}
}
只要实现了Iterable接口,就可以使用for-each循环了。
有三种常见情况无法使用for-each循环:
(1)过滤——删除特定的元素,需要显示使用迭代器,一遍可以调用器remove方法
(2)转换——修改部分元素的值
(3)平行迭代——同时遍历多个集合,需要显示控制iterator或者index。
第47条 了解和使用类库
通过使用标准类库,可以充分利用这些编写标准类库的专家的知识,以及在你之前的其他人的使用经验。
了解每一个重要发行版本的新特性是值得的。
每一个程序员都应该熟悉java.lang、java.util还有java.io中的内容,其他的根据需要随时学习。
不要重复发明轮子。
类库代码受到的关注远远超过大多数普通程序员在同样功能上所能够给予的投入。
第48条 如果需要精确的答案,请避免使用float和double
float和double类型尤其不适合用于货币计算。
// 预期 0.61
System.out.println(1.03 - .42);
// 实际: 0.6100000000000001
// 预期 0.1
// 实际: 0.09999999999999998
System.out.println(1.00 - .10 * 9);
public static void main(String[] args) {
double founds = 1.00;
int itemsBought = 0;
for (double price = .10; founds >= price; price += 0.10) {
founds -= price;
itemsBought++;
}
System.out.println(itemsBought + " items bought");
System.out.println("Change : " + founds);
}
3 items bought
Change : 0.3999999999999999
如果介意性能,需要使用int或者long,如果数值没有超过9位十进制数字,就可以使用int; 如果不超过18位数字,可以使用long;如果数值超过了18位数字,就必须使用BigDecimal。
第49条 基本类型优先于装箱基本类型
基本类型和装箱基本类型有三个主要的区别:
(1) 基本类型只有值。装箱类型具有与它们的值不同的同一性。
(2) 基本类型只有功能完备的值。装箱类型还有非功能值:null
(3) 基本类型比装箱类型更节省时间和空间。
对装箱基本类型运用==操作符几乎总是错误的。
当在一项操作中混合使用基本类型和装箱类型时,装箱类型就会拆箱。
使用装箱类型的场景:
(1) 集合中的元素、键和值。
(2) 泛型。ThreadLocal
(3) 反射的方法调用。
第50条 如果其他类型更合适,则尽量避免使用字符串
字符串不适合代替其他的值类型
字符串不适合代替枚举类型
字符串不适合代替聚集类型
字符串不适合代替能力表(capabilities)
KV的情况,不太适合使用字符串作为key。
一旦两个客户端使用了相同的字符串,实际上就无意中共享了这个变量,往往会导致两个客户端都失败。
public final class ThreadLocal<T> {
public ThreadLocal() {}
public void set(T value);
public T get()
}
第51条 当心字符串连接的性能
为了获得可以接受的性能,请使用StringBuilder代替String
第52条 通过接口引用对象
如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型来进行声明。
如果没有合适的接口存在,完全可以用类而不是接口来引用对象。
不存在适当接口类型的三种情形:
(1)值类。 例如String和Integer。值类很少会用多个实现编写,通常也是final的,并且很少有对应的接口。
(2)如果对象属于基于类的框架(class-based framework),就应该使用相应的基类来引用这个对象。
(3)类实现了接口,但是又提供了接口中不存在的额外方法。如果要使用这个额外的方法,就只能用类来引用实例。
第53条 接口优先于反射机制
反射机制允许一个类使用另一个类,即使当前者被编译的时候后者还根本不存在。
反射需要付出的代价:
(1) 丧失了编译时类型检查的好处,包括异常检查。 如果企图用反射方式调用不存在或者不可访问的方法,运行时可能会失败。
(2)执行反射访问所需要的代码非常笨拙和冗长。
(3)性能损失。
反射功能只是在设计时(design time)被用到。通常,普通应用程序在运行时不应该以反射方式访问对象。
使用反射机制的合理场景:
- 类浏览器、对象监视器、代码分析工具、解释型的内嵌式系统、RPC
- 有些程序,它们必须用到编译时无法获取的类,但是在编译时存在适当的接口或者超类,可以通过它们来引用这个类。
public static void main(String[] args) {
// 获取到Class对象
Class<?> cl = null;
try {
cl = Class.forName(args[0]);
} catch (ClassNotFoundException e) {
System.err.println("Class not found");
System.exit(1);
}
// 实例化set
Set<String> s = null;
try {
s = (Set<String>) cl.newInstance();
} catch (IllegalAccessException e) {
System.err.println("Class not accessible");
System.exit(1);
} catch (InstantiationException e) {
System.err.println("Class not instantiable.");
System.exit(1);
}
// 后面的参数丢到set中,然后打印出来
s.addAll(Arrays.asList(args).subList(1, args.length));
System.out.println(s);
}
上面的示例暴露了反射的两个缺点:
(1) 产生了太多的运行时错误,如果改为new的方式,那么这些运行时错误就变为了编译时的错误。
(2) 创建对象的代码冗长,如果new只需要一行。
第54条 谨慎地使用本地方法
本地方法(native method): 用本地程序设计语言(c或者cpp)编写的特殊方法。
本地方法主要有三种用途:
(1) 提供了“访问特定于平台的机制”的能力。比如访问注册表(regestry)和文件锁(file lock)
(2) 提供了可以访问遗留代码库的能力。从而可以访问遗留数据(legacy data)
(3) 使用本地方法来编写注重性能的部分。
不提倡使用本地方法来提高性能的做法。因为JVM变得越来越快了。
本地方法的严重的缺点:
(1) 本地语言不是安全的。不能避免内存毁坏的错误。
(2) 本地语言是与平台相关的。使用本地方法的程序不再是可以自由移植的了。
(3) 使用本地方法的程序更难于调试。
(4) 降低性能,进入和退出本地方法的时候,有固定的开销。
(5) 可读性差。 本地方法的可读性比较差。
第55条 谨慎地进行优化
要努力地编写好的程序而不是快的程序。
必须在设计的过程中考虑到性能问题。 尽力避免那些限制性能的决策。 程序中最难改的组件就是:指定了模块间交互关系以及模块与外界交互关系的组件。在这些组件中最终要的是API、线路层(wire-level)协议以及永久数据格式。
要考虑API设计决策的性能后果。 例如:(1)使公有类型成为可变的(mutable),可能会导致大量的不必要的保护性拷贝。(2)在适合使用复合模式的共有类中使用继承,会把这个类和它的超类束缚在一起,从而人为地限制了子类的性能。 (3)如果在api中使用的是实现类型而不是接口,会把api束缚在一个具体的实现上。即时将来出现更快的实现也无法使用。
如果为了好的性能而对api进行包装,这是一种非常不好的想法。 导致你对api进行包装的性能因素可能会在平台未来的发行版本中或者在将来的底层软件中不复存在,但是被包装的api以及由它引起的问题将永远困扰着你。
在每一次做优化前后,要对性能进行测试。 java语言没有很强的性能模型(performance model),会导致程序员编写的代码和cpu真正运行的代码之间存在gap,所以,在java语言中对优化结果进行测量尤为重要。
第56条 遵守普遍接受的命名规则
包的名称应该是层次状的,用.分隔每个部分。
(1)组织域名开头,并且放在顶级域名的前面
例如edu.cmu、 com.sun gov.nsa
(2) 包名的其余部分应该包括一个或者多个描述该包的组成部分。这些组成部分应该比较简短,通常不超过8个字符。鼓励使用有意义的缩写(例如使用util而不是utilities),可以接受只取首字母的缩写形式(例如awt)。每个组成部分通常由一个单词或者缩写词组成。
类、接口、枚举和注解的名称应该包括一个或者多个单词,每个单词的首字母大写,例如Timer和TimerTask。应该尽量避免使用缩写,除非是一些首字母缩写和通用的缩写(例如max和min)。强烈建议使用仅有首字母大写的形式,即使连续出现多个首字母缩写的形式,显然HttpUrl
要比HTTPURL
要好很多。
方法和域的要求一样,不过要使用小驼峰。
常量域的名称是一个或者多个全大写的单词组成,中间用下划线隔开。 例如VALUES
和 NEGATIVE_INFINITY
。
类型参数通常由单个字母组成。T表示任意类型,E表示集合的元素类型,K和V表示键值对,X表示异常。任何类型的序列可以使T、U、V或者是T1、T2和T3
接口的命名与类名相似,例如Collection或者Comparator,或者用-able -ible进行结尾,例如Runnable、Iterable或者Accessible。
方法有返回值,并且返回的对象是一个非boolean的函数或者属性,那么这个方法名应该用名词、名词短语或者get开头的动词短语来命名,例如size、hashCode或者getXXX。如果方法所在的类是一个Bean,那么方法就强制使用get开头。
用于类型转换类的方法通常定义为toType,例如toString和toArray。返回视图的方法通常被称为asType,例如asList。如果转换前后的类型时一样的,通常被定义为typeValue,例如intValue。