本章主要讨论:
局部变量的处理
控制结构
类库的用法
各种数据类型的用法
反射、本地方法
优化、命名惯例
第45条:将局部变量的作用域最小化
将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。
要使局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明。
过早地声明局部变量不仅会使它的作用域过早地扩展,而且结束的过于晚了——如果变量是在“使用它的块”之外被声明的,当程序退出该块之后,该变量仍是可见的。
每个局部变量都应该包含一个初始化表达式。
下面是一种遍历集合的首选做法:
for(Element e : c) {
doSth() ...
}
或者:
for (int i = 0, n = expensiveComputation; i < n; ++i) {
doSth() ...
}
第46条:for-each循环优于传统for循环
首先,for-each循环不会有性能损失,其次,可以减少出错的可能。
看下面的代码:
//代码会在执行第五次循环时抛出NoSuchElementException
enum Suit {CLUB, DIAMOND, HEART, SPADE}
enum Rank {ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING}
...
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()));
}
}
而下面的代码将打印从“ONE ONE”到“SIX SIX”的6个重复的词,而不是36种组合:
enum Face {ONE, TWO, THREE, FOUR, FIVE, SIX }
...
Collection<Face> faces = Arrays.asList(Face.values());
for(Iteratoer<Face> i = faces.iterator; i.hasNext()) {
for(Iterator<Face> j = faces.iterator;j.hasNext()) {
System.out.println(i.next() + " " + j.next());
}
}
下面的方式可以修复bug,但不美观:
for(Iterator<Suit> i = suits.iterator();i.hasNext();) {
Suit suit = i.next();
for (Iterator<>Rank) j = ranks.iterator();j.hasNext();){
deck.add(new Card(suit,j.next()));
}
}
//而如果用for-each循环就能很好的解决
for (Suit suit : suits)
for(Rank rank : ranks)
deck.add(new Card(suit,rank));
for-each循环的另一个好处是,它不仅可以遍历集合和数组,还可以遍历任何实现Iterable接口的对象:
//Iterable接口
public interface Iterable<E> {
Iterator<E> iterator();
}
总之for-each循环的优势比for循环大很多。但是,以下三种情况没法使用for-each循环:
1、过滤——如果要在遍历过程中删除某些元素,就应当使用传统for循环,以便调用其remove方法。
2、转换——如果要在遍历过程中替换某些元素,就只能使用for循环。
3、平行迭代——需要并行遍历多个集合,需使用传统for循环。
第47条:了解和使用类库
考虑下面的方法:
//如希望产生位于0和某个上界之间的随机整数
static int random(int n) {
return Math.abs(rnd.nextInt)) % n;
}
该方法存在几个问题:
1、若n是个较小的2的乘方,那么它产生的随机数将会重复。
2、若n不是2的乘方,那么有些数会比其他数出现的更加频繁。
3、如果nextInt()返回Integer.MIN_VALUE
,那么Math.abs也返回Integer.MIN_VALUE
,假设不是n不是2的乘方,那么取模操作符将返回一个负数。
产生上述问题的原因是未考虑伪随机数、数论和2的求补算法的相关知识。
所以,应当使用Java API 1.2引入的方法Random.nextInt(),该方法已经把上述问题考虑进去。这就是使用标准类库的好处:产生的问题少,且不必把时间花在底层细节上,另外它们的性能会不断提高。
总结,不要重复发明轮子。
第48条:如果需要精确的答案,请避免使用float 和double
float和double类型主要为了科学计算和工程计算而设计的,它可以提供较为精确的快速而近似的计算。然而它们并没有提供完全精确的结果。所以在需要精确计算时,不应该使用float和double。而且 ,它们尤其不适用于货币计算。
而使用货币等精确计算应使用BigDecimal、int或者long。
public static void main(String[] args) {
final BigDecimal TEN_CENTS = new BigDecimal(".10");
int itemsBought = 0;
BifDecimal funds = new BigDecimal("1.00");
for(BigDecimal price = TEN_CENTS; funds.compareTo(price) >= 0; price = price(TEN_CENTS)) {
itemBought++;
funds = funds.subtract(price);
}
System.out.println(itemBought + " items bought.");
System.out.println("Money left over: $" + funds);
}
然而BigDecimal有两个缺点:使用起来不便;效率低。
解决方式是使用int或是long
public static void main(String[] args) {
int itemBought = 0;
int funds = 100;
for( int price = 10;funds >= price; price += 10) {
itemBought++;
funds -= price;
}
System.out,println(itemBought + " item bought.");
System.out,println("Money left over: " + funds + " cents");
}
总而言之,对于精确的计算,请不要使用float或double。如果需要计算小数的计算,请使用BigDecimal,如果只是用整数,且数值不太大,就是用int,如果数值范围超过了9位十进制数,就是用long,如果超过了十八位,必须使用BigDecimal。
第49条:基本类型优于对应的装箱类型
Java1.5增加了自动装箱和自动拆箱功能。
基本类型和装箱类型有三个主要区别:
1、基本类型只有值,而装箱类型具有与他们值不同的同一性(同一性:是否为两个相同对象)。
2、基本类型只有功能完备的值,而装箱类型除了具备功能完备的值,还有一个非功能的值:null。
3、基本类型更节省空间和时间。
Comparator<Integer> naturalOrder = new Comparator<Integer>() {
public int compare(Integer first,Integer second) {
return first < second ? -1 : (first == second ? 0 : 1);
}
};
当调用:
naturalOrder.Compare(new Integer(42), new Integer(42));
时,期望的输出时0,结果却是1。
原因是,当比较first < second的时候,编译器会先将Integer类型拆箱成基本类型,显然42 < 42为假,所以执行(first == second ? 0 : 1),而此时first和second将是对象的引用,即它们将比较的为 是不是同一个对象(这就是所谓的同一性问题),结果显然是假,所以返回1。所以,对装箱类型使用==几乎总是错误的。
解决方式是加入两个局部变量,所有比较都在这两个局部变量上进行,从而避免了同一性的问题。
Comparator<Integer> naturalOrder = new Comparator<Integer>() {
public int compare(Integer first,Integer second) {
int f = first;
int s = second;
return f < s ? -1 : (f == s ? 0 : 1);
}
}
考虑下面的程序:
public class Unbelievable {
static Integer i;
public static void main(String[] args) {
if(i == 42){
System.out.println("Unbelievable");
}
}
}
上面的程序会直接抛出NullPointerException异常,原因是i是一个Integer引用类型,而42是基本类型,当编译器遇到==操作符的两端是基本类型和装箱类型时,会先将装箱类型自动拆箱。但i还没有初始化,所以引用为null,引用为null的装箱类型自动拆箱会直接抛出NullPointerException异常。解决办法是把i的类型修改为int。
那么什么时候适合使用装箱类型呢?答案是1、当作为集合中的键或值的时候。2、当作为泛型参数的时候。如ThreadLocal<Integer>
。2、在进行反射方法调用时必须使用装箱类型。。
第50条:如果其他类型更适合,则尽量避免使用字符串
字符串不适合代替其他的值类型。一些int、float、BigInteger类型不要用String类型表示;
字符串不适合代替枚举类型;
字符串不适合代替聚集类型:如果一个实体有多个组件,用一个字符串来表示这个实体是不恰当的:
String compoundKey = className + "#" + i.next();
,这会造成混乱。应当简单地编写一个类来描述这个数据集,通常是一个私有的静态成员类(见第22条)。字符串不适合代替能力表:下面以ThreadLocal类来说明。
ThreadLocal是线程局部变量,可以把它理解成一个Map,该对象在每个线程中都维护着自己的变量,通过“Key”就可以获得相应线程的值。吐过把Key设计成String类型的,就会造成如果有两个线程给“key”取得名字是一样的,那么这个key就变成了共享变量,这两个线程在通过key读取值的时候,会发生错误。
改进:
public final class ThreadLocal<T> {
public ThreadLocal(){}
public void set(T value);
public T get();
}
总之,如果可以使用更加合适的数据类型,或者编写更加适当的数据类型,就应当避免用字符串来表示对象。
第51条:当心字符串链接的性能
字符串连接符(+)可以把多个字符串合并为一个字符串,这是个便利的方式。但它不是和连接多个字符串,因为String是不可变类,两个字符串连接时,都要被拷贝。
//不好的方式
public String statement() {
String result = "";
for (int i = 0;i < numItem(); ++i) {
result += lineForItem(i);
}
return result;
}
为了改善性能,应使用StringBuilder代替String,前者是非线程安全的:
(StringBuffer已经过时,它是线程安全的)
public String statement() {
StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH);
for (int i = 0;i < numItems(); ++i) {
b.append(lineForItem(i));
}
return b.toString();
}
第52条:通过接口引用对象
如果有合适的类型接口存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明。:
考虑Vector,它是List接口的一个实现:
//应该这样声明,使用接口当做类型
List<Subscriber> subscriber = new Vector<Subscriber>();
//不应该这样声明
Vector<Subscriber> subscribers = new Vector<Subscriber>();
使用接口作为类型的好处是使得程序更加灵活,当决定改变实现时,只需要改变构造器的名称即可:
//将上面的第一句的Vector时间改为ArrayList
List<Subscriber> subscribers = new ArrayList<Subscriber>();
第53条:接口优先于反射机制
反射机制(java.lang.reflect)提供了“通过程序来访问关于已装载的类的信息”的能力。给定一个Class实例,亦可以获得Constructor、Method、Field实例,分别表示Class实例对应的类的Constructor、Method、Field。这些对象提供了“通过程序来访问类的成员名称、域类型、方法签名等信息”的能力。
例如,Method.invoke可以调用任何类的任何对象的任何方法。反射机制允许一个类使用另一个类。即使当前者按编译的时候后者还根本不存在。然而,反射也有缺点:
丧失了编译时类型检查的好处:程序企图用反射调用不存在的或者不可访问的方法,在运行时它将会失败。
执行反射访问所需的代码非常笨拙;
性能损失:反射方法的调用比普通方法慢了许多。
所以反射机制应该只在设计时被用到。通常,普通应用程序在运行时不应该以反射的方式访问对象。
第54条:谨慎地使用本地方法
JNI(Java Native Interface)允许Java应用程序调用本地方法,所谓本地方法,就是本地应用程序需设计语言(如C或C++)来编写的特殊方法本地方法在本地语言中可以执行任意的计算任务,并返回Java程序设计语言。
本地方法主要有三个用途:
本地方法提供了“访问特定于平台的机制”的能力,如访问注册表和文件锁。
本地方法提供了访问遗留代码库的能力,从从而可以访问遗留数据。
本地方法可以通过本地语言,编写应用程序中注重性能部分,以提高系统的性能。
然而,使用本地方法来提高性能的做法不值得提倡。因为如今的JVM越来越快了,对于大多数任务,现在即便不适用本地方法也可以获得相当的性能。
总之,使用本地方法之前务必三思,极少数情况下会需要使用本地方法来提高性能。
第55条:谨慎进行优化
路。。。
第56条:请遵守普遍接受的命名惯例
略。。。