- 浏览: 56842 次
- 性别:
- 来自: 成都
文章分类
最新评论
-
chiqinghaichi:
楼主好!菜鸟一枚,试了下你的程序,当访问 http://loc ...
SpringMVC -
随意而生:
复习复习 不错
SpringMVC -
ccii:
cylboke 写道他的意思是“orcle”写错了吧?我猜的! ...
Oracle基本操作 -
happy200318:
给个赞,不错
SpringMVC -
cylboke:
他的意思是“orcle”写错了吧?我猜的!!!
Oracle基本操作
本章内容:
1. 将局部变量的作用域最小化
2. for-each循环优先于传统的for循环
3. 了解和使用类库
4. 如果需要精确的答案,请避免使用float和double
5. 基本类型优先于装箱基本类型
6. 如果其他类型更适合,则尽量避免使用字符串
7. 当心字符串连接的性能
8. 通过接口引用对象
9. 接口优先于反射机制
10. 谨慎地使用本地方法
11. 谨慎地进行优化
12. 遵守普遍接受的命名惯例
1. 将局部变量的作用域最小化
将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。在C 语言中要求局部变量必须在一个代码块的开头处进行声明,出于习惯,有些开发者延续了这样的做法。这个习惯需要改正,Java 提供了你在代码块的任何地方声明变量的语法支持。
(1)要使局部变量的作用域最小化,最有力的实践就是在第一次使用它的地方声明。如果过早的声明,开发者就有可能在真正使用该变量的时候忘记了它的类型或者初始值了,而且也会带来代码块内变量名的名字污染问题。
(2)几乎每个局部变量的声明都应该包含一个初始化表达式。如果你没有足够的信息来满足对一个变量进行有意义的初始化,就应该推迟这个声明,直到可以初始化为止。这条规则有个例外的情况与try-catch语句有关。如果一个变量被一个方法初始化,而这个方法可能会抛出一个异常,该变量就必须在try 块内初始化,如果这个变量的值也必须在try 块之外被访问,它就必须在try 块之前被声明,但是遗憾的是在try 块之前,它还不能被"有意义地初始化"。
(3)循环中提供了特殊的机会将变量的作用域最小化,它们的作用域正好被限定在需要的范围之内。因此,如果在循环终止之后不再需要变量的内容,for 循环就优先于while 循环,见如下代码片段:
Iterator<Element> i = c.iterator();
while (i.hasNext()) {
doSomething(i.next());
}
Iterator<Element> i2 = c2.iterator();
while (i.hasNext()) { //BUG!
doSomethingElse(i2.next());
}
可以看到在第二个循环的循环条件判断处有一个非常明显的BUG,这极有可能是copy-paste 所致。然而该类错误如果出现在for 循环里,将直接引发编译期错误。见如下代码片段:
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
doSomething(i.next());
}
for (Iterator<Element> i2 = c2.iterator(); i.hasNext(); ) {
doSomethingElse(i2.next());
}
如果使用for循环,犯这种copy-paste 错误的可能性大大降低,因为通常没有必要在两个循环中使用不同的变量名。循环是完全独立的,所以重用元素(或者迭代器)变量的名称不会有任何危害。
使用for循环与使用while循环相比还有另外一个优势:更简短,从而增强了可读性。
2. for-each循环优先于传统的for循环
for-each 循环是在Java1.5发行版本之后才支持的,之前只能使用传统的for循环。相比于普通for 循环,for-each大大提高了代码可读性,由此也减少了低级BUG 出现的几率。
for-each循环不会有性能损失,甚至用于数组也一样。实际上,在某些情况下,比起普通的for循环,它还稍有些性能优势,因为它对数组索引的边界值只计算一次。
在对多个集合进行嵌套式迭代时,for-each循环相对于传统for循环的这种优势还会更加明显。下面就是人们在试图对两个集合进行嵌套迭代时常会犯的错误:
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()); //BUG, i被多次迭代
}
上面代码的BUG 是比较隐匿的,很多专家级的程序员也会偶尔犯类似的错误。问题在于,在迭代器上对外部的集合调用了太多次next方法。它应该从外部的循环进行调用,以便每种花色调用一次。下面我们来修复一下上面的代码:
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接口的对象。
遗憾的是,有三种常见的情况无法使用for-each循环:
(1)过滤:如果需要遍历集合,并删除选定的元素,就需要使用显式的迭代器,以便可以调用它的remove方法。
(2)转换:如果需要遍历列表或数组,并取代它部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值。
(3)平行迭代:如果需要并行的遍历多个集合,就需要显式的控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移。
3. 了解和使用类库
假设你希望产生位于0和某个上界之间的随机整数。面对这个任务,许多程序员会编写如下所示的方法:
private static final Random rnd = new Random();
static int random(int n){
return Math.abs(rnd.nextInt()) % n; //rnd.nextInt()返回值在-MaxValue到MaxValue
}
这个方法看起来不错,其实不然。它有三个缺点,第一个是如果n是一个比较小的2的乘方,经过一段相当短的周期之后,它产生的随机数序列会重复。第二个缺点是如果n不是2的乘方,那么平均起来,有些数会比其他的数出现得更为频繁。第三个缺点是它会返回一个落在指定范围之外的数,因为这个方法试图通过调用Math.abs,将rnd.nextInt()映射为一个非负整数int。如果nextInt()返回Integer.MIN_VALUES,那么Math.abs也会返回Integer.MIN_VALUE,假设n不是2的乘方,那么取模操作符%将返回一个负数。
幸运的是,我们并不需要自己来做这些工作,已经有现成的类库可以为你所用,Random.nextInt(int n),返回0到n的随机数,你无需关心实现细节。已经有很多高级算法工程师花了大量的时间来设计、实现和测试这个方法。
使用标准类库的第二个好处是,不必浪费时间为那些与工作不太相关的问题提供特别的解决方案。就像大多数程序员一样,应该把时间花在应用程序上,而不是底层的细节上。
使用标准类库的第三个好处是,它们的性能往往会随着时间的推移而不断提高,无需你做任何努力。因为提供这些标准类库的组织有强烈的动机要使它们运行得更快。
使用标准类库的再一个好处是,可以使自己的代码融入主流,这样的代码更易读,更易维护、更易被大多数的开发人员重用。
标准类库太庞大了,以至于不可能去学习所有的文档,但是每个程序员都应该熟悉java.lang、java.util,某种程序上还有java.io中的内容。
4. 如果需要精确的答案,请避免使用float和double
float 和double 类型主要是为了科学计算和工程计算而设计的。它们执行二进制浮点运算,这是为了在广泛的数值范围上提供较精确的快速近似计算而精心设计的。然而,它们并没有提供完全精确的结果,所以不应该被用于需要精确结果的场合,如货币计算等。
示例一:System.out.println(1.0.3 - .42); 它输出的结果却是0.6100000000000001。
示例二:
如果你手里有1 美元,超市货架上有一排糖果,它们的售价分别为10 美分、20 美分、30 美分,以此类推直到1美元。你打算从标价10 美分的开始买,每个糖果买1 颗,直到不能支付货架上下一中价格的糖果为止,那么你可以买多少糖果?还会找回多少零头呢?见如下代码:
public static void main(String[] args) {
double funds = 1.00;
int itemsBought = 0;
for (double price = .10; funds >= price; price += .10) {
funds -= price;
itemsBought++;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Change: $" + funds);
}
如果真正运行这个程序,你会发现你可以支付3颗糖果,并且还剩下$0.39999999999999。造成这一结果的主要原因就是double 类型
的精度问题。解决该问题的正确办法是使用BigDecimal、int 或者long 进行货币计算。如下:
public static void main(String[] args) {
final BigDecimal TEN_CENTS = new BigDecimal(".10");
int itemsBought = 0;
BigDecimal funds = new BigDecimal("1.00");
for (BigDecimal price = TEN_CENTS; funds.compareTo(price) >=0; price.add(TEN_CENTS)) {
itemsBought++;
funds = funds.substract(price);
}
System.out.println(itemsBought + " items bought.");
System.out.println("Money left over: $" + funds);
}
运行这个程序,就会发现你可以支付4颗糖果,还剩下$0.00。这才是正确的答案。
然而,使用BigDecimal有两个主要缺点:和使用基本运算类型相比,这样做很不方便,而且效率也低。除了该方法之外我们还可以使用int 或者long,至于使用哪种具体类型,需要视所涉及的数值大小而定。现在我们需要将计算单位转换为分,而不再是以元为单位,如下:
public static void main(String[] args) {
int itemsBougth = 0;
int funds = 100;
for (int price = 0; funds >= price; price += 10) {
itemsBought++;
fund -= price;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Money left over: $" + funds + " cents.");
}
总而言之,对于任何需要精确答案的计算任务,请不要使用float或者double。如果你想让系统来记录十进制小数点,并且不介意因为不使用基本类型而带来的不便,就请使用BigDecimal。使用BigDecimal还有一些额外的好处,它允许你完全控制舍入,每当一个操作涉及舍入的时候,它允许你从8种舍入模式中选择其一。如果你正通过法定要求的舍入行为进行业务计算,使用BigDecimal是非常方便的。如果性能非常关键,并且你又不介意自己记录十进制小数点,而且所涉及的数值又不太大,就可以使用int或者long。需要指出的是,如果数值所涉及的范围没有超过9 位十进制数字,就可以使用int,没有超过18 位可以使用long,一旦超过,则必须使用BigDecimal。
5. 基本类型优先于装箱基本类型
Java 的类型系统中主要包含两个部分,分别是基本类型,如int、double、long,还有就是引用类型,如String、List 等。其中每个基本类型都对应着一种引用类型,被称为装箱基本类型,如分别和int、double、long 对应的装箱类型Integer、Double 和Long等。
Java 在1.5 中新增了自动装箱的和自动拆箱的功能。这些特性仅仅是模糊了基本类型和装箱类型之间的区别,但是并没有完全消除他们之间的差异,而这些差别往往会给我们的程序带来一些潜在的问题。我们先看一下他们之间的主要区别:
(1)基本类型只有值,在进行比较时可以直接基于值进行比较,而装箱类型在进行同一性比较时和基本类型相比有着不同的逻辑,毕竟他们是对象,是Object 的子类,它们需要遵守Java 中类对象比较的默认规则。
(2)基本类型只有功能完备的值,而每个装箱类型除了它对应基本类型的所有功能之外,还有一个非功能值:null。
(3)基本类型通常比装箱类型更节省时间和空间。
如下示例:
public class MyTest {
private static int compare(Integer first,Integer second) {
return first < second ? -1 : (first == second ? 0 : 1);
}
public static void main(String[] args) {
Integer first = new Integer(42);
Integer second = new Integer(42);
System.out.println("Result of compare first and second is " + compare(first,second));
}
}
这段代码看起来非常简单,它的运行结果也非常容易得出,然而当我们真正运行它的时候却发现,实际输出的结果和我们的期望是完全不同的,分析: 对表达式first < second执行计算导致被first和secound引用的Integer实例被自动拆箱,将能够正常工作并得到正确的结果,即first < second 为false; 在进行相等性比较的时候问题出现了,如前所述,Integer 毕竟是对象,在进行对象之间的同一性比较时它将遵守对象的同一性比较规则,由于这两个参数对象的地址是不同的,因为我们是通过两次不同的new 方法构建出的这两个参数对象。结果可想而知,first == second 返回false;
下面我们看一下如何修正以上代码中存在的错误:
public class MyTest {
private static int compare(Integer first,Integer second) {
int f = first;
int s = second;
return f < s ? -1 : (f == s ? 0 : 1);
}
public static void main(String[] args) {
Integer first = new Integer(42);
Integer second = new Integer(42);
System.out.println("Result of compare first and second is " +
compare(first,second));
}
}
现在让我们再看一段代码片段:
public class Unbelievable {
static Integer i;
public static void main(String[] args) {
if (i == 42)
System.out.println("Unbelievable");
}
}
程序的运行结果并没有打印出"Unbelievable",而是抛出了空指针异常。这是因为装箱类型的i 变量并没有被初始化,即它本身为null,当程序计算表达式(i == 42)时,它会将Integer 与int进行比较。几乎在任何一种情况下,当在一项操作中混合使用基本类型和装箱基本类型时,装箱类型就会自动拆箱,这种情况无一例外。如果null对象引用被自动拆箱,就会得到一个NullPointerException。修正这一问题也非常简单,只需将i 的类型从Integer 变为int 即可。
再看一下最后一个代码示例:
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; ++i) {
sum += i;
}
System.out.println(sum);
}
这段代码虽然不像之前的两个示例那样有着明显的Bug,然而在运行时却存在着明显的性能问题。因为在执行for 循环时,会有不断的自动装箱和自动拆箱的操作发生。修改该代码也是非常容易的,只需将sum 的类型从Long 变为long 即可。
那么什么时候应该使用装箱基本类型呢?第一个是作为集合中的元素、键和值。你不能将基本类型放在集合中,因此必须使用装箱基本类型。在参数化类型中,必须使用装箱基本类型作为类型参数,因为java不允许使用基本类型(Java 泛型中的类型参数)。另外,在进行反射的方法调用时必须使用装箱基本类型。
总之,当可以选择的时候,基本类型要优先于装箱基本类型。基本类型更加简单,也更加快速。最后,当程序装箱基本类型值时,会导致高开销和不必要的对象创建。
6. 如果其他类型更适合,则尽量避免使用字符串208
字符串被用来表示文本,它在这方面也确实做得很好。因为字符串很通用,并且Java语言也支持得很好。下面讨论一些不应该使用字符串的情形:
(1)字符串不适合代替其他的值类型。如果它是数值,就应该被转换为适当的数值类型,如果它是“是或否”它就应该被转换为boolean类型。
(2)字符串不适合代替枚举类型。
(3)字符串不适合代替聚集类型。如果用来分隔域的字符也出现在某个域中,结果就会出现混乱。
(4)字符串也不适合代替能力表。
7. 当心字符串连接的性能
字符串连接操作(+)是把多个字符串合并为一个字符串的最为便利的途径。因此如果仅仅是对两个较小字符串进行一次连接并输出连接结果,这样是比较合适的。然而如果是为n 个字符串而重复地使用字符串连接操作符,则需要n 的平方级的时间。这是由于字符串对象本身是不可变的,在连接两个字符串时,需要copy 两个连接字符串的内容并形成新的连接后的字符串。见如下代码:
public String statement() {
String result = "";
for (int i = 0; i < numItems(); i++) {
result += lineForItem(i);
}
return result;
}
此时如果项目数量巨大,这个方法的执行时间将难以估量。为了获得可以接受的性能,请使用StringBuilder(非同步StringBuilder类代替了已经过时的StringBuffer类)替代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方法。另一种方法是,使用字符数组,或者每次只处理一个字符串,而不是将它们组合起来。
8. 通过接口引用对象
一般来讲,在函数参数、返回值、域变量等声明中,应该尽量使用接口而不是类作为它们的类型。只有当你利用构造器创建某个对象的时候,才真正需要引用这个对象的类,如:
List<Subscriber> subscribers = new Vector<Subscriber>();
而不是像下面这样的声明:
Vector<Subscriber> subscribers = new Vector<Subscriber>();
如果你养成了用接口作为类型的习惯,你的程序将更加灵活。对于上面的例子,在今后的改进中,如果不想使用Vector 作为实例化对象,我们只需在如下一出进行修改即可:
List<Subscriber> subscribers = new ArrayList<Subscriber>();
周围的所有代码都可以继续工作。周围的代码并不知道原来的实现类型,所有它们对于这种变化并不在意。但有一点值得注意:如果之前的实现提供了某种特殊的功能,而这种功能并不是这个接口的通用约定所要求的,并且周围的代码又依赖于这种功能,那么很关键的一点,新的实现也要提供同样的功能。例如:如果上面的第一个声明依赖于Vector的同步策略,在声明中用ArrayList代替Vector就是不正确的。如果依赖于实现的任何特殊属性,就要在声明变量的地方给这些需求建立相应的文档说明。
那么,为什么要改变实现呢?因为新的实现提供了更好的性能,或者因为它提供了期望得到的额外功能。
那么在哪些情况下不是使用接口而是使用实际类呢:
(1)没有合适的接口存在,如String 和BigInteger 等值对象,值类通常都是final的,很少有对应的接口。
(2)对象属于一个框架,而框架的基本类型是类,不是接口。如果对象属于这种基于类的框架,就应使用基类来引用该对象,如TimerTask。
(3)类实现了接口,但是它提供了接口中不存在的额外方法。如果程序此时依赖于这些额外的方法,这种类就应该只被用来引用他的实例。
简而言之,如果类实现了接口,就应该尽量使用其接口引用该类的引用对象,这样可以使程序更加灵活,如果不是,则使用类层次结构中提供了必要功能的最基础的类。
9. 接口优先于反射机制
核心反射机制提供了通过程序来访问关于已装载的类的信息的能力。给定一个Class 实例,你可以获取Constructor、Method和Field等实例,分别代表了该Class实例所表示的类的Constructor(构造器)、Method(方法)和Field(域)。这些对象提供了通过程序来访问类的成员名称、域类型、方法签名等信息的能力。
而且,Constructor、Method和Field实例可以使你能够通过反射机制操作它们的底层对等体:通过调用Constructor、Method和Field实例上的方法,可以构造底层类的实例、调用底层类的方法,并访问底层类中的域。反射机制允许一个类使用另一个类,即使当前者被编译的时候后者还根本不存在。然而这种能力是需要付出代价的:
(1)丧失了编译时类型检查的好处,包括异常检查。如果程序企图用反射方式调用不存在的或者不可访问的方法,在运行时它将会失败,除非采取了特别的预防措施。
(2)执行反射访问所需要的代码往往非常笨拙和冗长,编写和阅读起来都非常困难,通常而言,一个基于普
通方式的函数调用大约1,2 行,而基于反射方式,则可能需要十几行。
(3)性能损失,反射方法的调用比普通方法调用慢了许多。
核心反射机制最初是为了基于组件的应用创建工具而设计的。它们通常需要动态装载类,并且用反射功能找出它们支持哪些方法和构造器。这些工具允许用户交互式地构建出访问这些类的应用程序,但是所产生出来这些应用程序能够以正常的方式访问这些类,而不是以反射的方式。反射功能只是在设计时被用到,通常普通应用程序在运行时不应该以反射方式访问对象。
有一些复杂的应用程序需要使用反射机制。如类浏览器、对象监视器、代码分析工具、解释性的嵌入式系统等。
如果只是以非常有限的形式使用反射机制,虽然也要付出少许代价,但是可以获得许多好处。对于有些程序,它们必须用到编译时无法获取的类,但是在编译时却存在适当的接口或超类,通过它们可以引用这个类。如果是这样,就可以先通过反射方式创建实例,然后再通过它们的接口或超类,以正常的方式访问这些实例。如果适当的构造器不带参数,甚至根本不需要使用java.lang.reflect包,Class.newInstance方法就已经提供了所需的功能。如下:
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“,则它们就会按照字母顺序打印出来,因为TreeSet中的元素是排好序的。
尽管这个程序就像一个玩偶,但是它所演示的这种方法是非常强大的。这个玩偶程序可以很容易地变成一个通用的集合测试器,通过侵入式地操作一个或者多个集合实例,并检查是否遵守Set接口的约定,以此来验证指定的Set实现。同样地,它也可以变成一个通用的集合性能分析工具,实际上,它所演示的这种方法足以实现一个成熟的服务提供者框架。绝大多数情况下,使用反射机制时需要的也正是这种方法。
但是上面的代码中体现出了反射的两个缺点:一是这个例子有3 个运行时异常的错误,如果不使用反射方式实例化,这3 个错误都会成为编译时错误。第二是根据类名生成它的实例需要20 行冗长的代码,而调用构造器可以非常简洁的只使用一行代码。
另一个值得注意的附带问题是,这个程序使用了System.exit。很少有需要调用这个方法的时候,它会终止整个VM。但是它对于命令行有效性的非法终止是很合适的。
简而言之,反射机制是一种功能强大的机制,对于特定的复杂系统编程任务,它是非常必要的。如果你编写的程序必须要与编译时未知的类一起工作,如有可能,就应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类。
10. 谨慎地使用本地方法
Java Native Interface(JNI)允许Java应用程序可以调用本地方法,所谓本地方法是指用本地程序设计语言,如C/C++来编写的特殊方法。本地方法在本地语言中可以执行任意的计算任务,并返回到Java程序设计语言。
本地方法主要有三种用途,第一是提供了“访问特定于平台的机制”的能力,比如注册表、文件锁等。第二是它们还提供了访问遗留代码 库的能力,从而可以访问遗留数据。第三种是本地方法可以通过本地语言,编写应用程序中注重性能的部分,以提高系统的本能。
使用本地方法来访问特定于平台的机制是合法的,随着Java 平台的不断成熟,它提供了越来越多以前只有宿主平台才有的特性,如java.util.prefs包提供了注册表的功能,java.awt.SystemTray提供了访问桌面系统托盘区有能力。
使用本地方法有一些严重的缺点。因为本地语言不是安全的,所有,使用本地方法的应用程序也不再能免受内存毁坏错误的影响。因为本地语言是平台无关的,使用本地方法的应用程序也不再是可自由移植的。使用本地方法的应用程序也更难调试。在进入和退出本地代码时,需要相关的固定开销,如果本地代码只做少量的工作,本地方法就可能降底性能。最后本地方法编写起来单调乏味,并且难以阅读。
总而言之,极少数情况下会需要使用本地方法来提高性能。如果你必须要使用本地方法来访问底层资源,或者遗留代码库,也要尽可能少用本地代码,并且要全面测试,本地代码中的一个Bug就有可能破坏整个应用程序。
11. 谨慎地进行优化
有三条与优化有关的格言是每个人都应该知道的:
(1)很多计算上的过失都被归咎于效率(没有必要达到的效率),而不是任其他的原因——甚至包括盲目的做傻事。
(2)不要去计较效率上的一些小小的得失,在97%的情况下,不成熟的优化才是一切问题的根源。
(3)在优化方面我们应该遵守两条规则:一是不要进行优化,二是还是不要进行优化——也就是说,在你还没有绝对清晰的未优化方案之前,请不要进行优化。
不要因为性能而牺牲合理的结构,要努力编写好的程序而不是快的程序。如果好的程序不够快,它的结构将使它可以得到优化。
这并不意味着,在完成程序之前就可以忽略性能问题。实现上的问题可以通过后期的优化而得到修正,但是,遍布全局并且限制性能的结构缺陷几乎是不可能被改正的,除非重新编写系统。在系统完成之后再改变设计的某个基本方面,会导致系统的结构很不好,从而难以维护和改进。因此,必须在设计过程中考虑到性能问题。
努力避免那些性能的设计决策。当一个系统设计完成之后,其中最难更改的组件是那些指定了模块之间交互关系以及模块关系以及模块与外界交互关系的组件。在这些设计组件中,最主要的是API、线程层协议以及永久数据格式。这些设计组件不仅在事后难以甚至不可能改变,而且它们都有可能对系统本该达到的性能产生严重的限制。
要考虑API设计决策的性能后果。使公有的类型成为可变的,这可能会导致大量不必要的保护性拷贝。同样地,在适合使用复合模式的公有类中使用继承,会把这个类与它的超类永远地束缚在一起,从而人为地限制了子类的性能。还有在API中使用实现类型而不是接口,会把束缚在一个具体的实现上,即使将来出现更快的实现你也无法使用。
性能剖析工具有助于你决定应该把优化的重心放在哪里,这样的工具可以为你提供运行时的信息,比如每个方法大致上花费了多少时间、它被调用了多少次。除了确定优化的重点之外,它还可以警告你是否需要改变算法。如果一个平方级的算法潜藏在程序中,无论怎么调整和优化都很难解决问题。你必须用更有效的算法来替换原来的算法。
总之,不要费力去编写快速的程序——应该努力编写好的程序,速度自然会随之而来。在设计系统的时候,特别是在设计API、线路层协议和永久数据格式的时候,一定要考虑性能的因素。当构建完系统之后,要测量它的性能。如果它足够快,你的任务就完成了。如果不够快,则可以在性能剖析器的帮助下,找到问题的根源,然后设法优化系统中相关的部分。第一个步骤是检查所选择的算法:再多的低层优化也无法弥补算法的选择不当。必要时重复这个过程,在每次改变之后都要测量性能,直到满意为止。
12. 遵守普遍接受的命名惯例
Java平台建立一套很好的命名惯例,其中有许多命名惯例包含在了《The Java Language Specification》。
如果API违反了这些惯例,它使用起来可能会很困难。如果实现违反了它们,它可能会难以维护。这两种情况下,违反惯例都会潜在地给使用这些代码的其他程序员带来困惑和苦恼,并且使他们做出错误的假设,造成程序出错。
包的名称应该是层次状的,其名称都应该以组织的Internet域名开头,并且顶级域名放在最前面。其余部分应该包括一个或者多个描述该包的组成部分。
类和接口的名称,包括枚举和注解类型的名称,都应该包括一个或者多个单词,每个单词的首字母大写。
方法和域的名称,也应该包括一个或者多个单词,每个单词的首字母小写。
上述规则的唯一例外是"常量域",它的名称应该包含一个或者多个大写的单词,中间用下划线隔开。
类型参数名称通常由单个字母组成:T表示任意的类型,E表示集合的元素类型,K和V表示映射的键和值类型。
1. 将局部变量的作用域最小化
2. for-each循环优先于传统的for循环
3. 了解和使用类库
4. 如果需要精确的答案,请避免使用float和double
5. 基本类型优先于装箱基本类型
6. 如果其他类型更适合,则尽量避免使用字符串
7. 当心字符串连接的性能
8. 通过接口引用对象
9. 接口优先于反射机制
10. 谨慎地使用本地方法
11. 谨慎地进行优化
12. 遵守普遍接受的命名惯例
1. 将局部变量的作用域最小化
将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。在C 语言中要求局部变量必须在一个代码块的开头处进行声明,出于习惯,有些开发者延续了这样的做法。这个习惯需要改正,Java 提供了你在代码块的任何地方声明变量的语法支持。
(1)要使局部变量的作用域最小化,最有力的实践就是在第一次使用它的地方声明。如果过早的声明,开发者就有可能在真正使用该变量的时候忘记了它的类型或者初始值了,而且也会带来代码块内变量名的名字污染问题。
(2)几乎每个局部变量的声明都应该包含一个初始化表达式。如果你没有足够的信息来满足对一个变量进行有意义的初始化,就应该推迟这个声明,直到可以初始化为止。这条规则有个例外的情况与try-catch语句有关。如果一个变量被一个方法初始化,而这个方法可能会抛出一个异常,该变量就必须在try 块内初始化,如果这个变量的值也必须在try 块之外被访问,它就必须在try 块之前被声明,但是遗憾的是在try 块之前,它还不能被"有意义地初始化"。
(3)循环中提供了特殊的机会将变量的作用域最小化,它们的作用域正好被限定在需要的范围之内。因此,如果在循环终止之后不再需要变量的内容,for 循环就优先于while 循环,见如下代码片段:
Iterator<Element> i = c.iterator();
while (i.hasNext()) {
doSomething(i.next());
}
Iterator<Element> i2 = c2.iterator();
while (i.hasNext()) { //BUG!
doSomethingElse(i2.next());
}
可以看到在第二个循环的循环条件判断处有一个非常明显的BUG,这极有可能是copy-paste 所致。然而该类错误如果出现在for 循环里,将直接引发编译期错误。见如下代码片段:
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
doSomething(i.next());
}
for (Iterator<Element> i2 = c2.iterator(); i.hasNext(); ) {
doSomethingElse(i2.next());
}
如果使用for循环,犯这种copy-paste 错误的可能性大大降低,因为通常没有必要在两个循环中使用不同的变量名。循环是完全独立的,所以重用元素(或者迭代器)变量的名称不会有任何危害。
使用for循环与使用while循环相比还有另外一个优势:更简短,从而增强了可读性。
2. for-each循环优先于传统的for循环
for-each 循环是在Java1.5发行版本之后才支持的,之前只能使用传统的for循环。相比于普通for 循环,for-each大大提高了代码可读性,由此也减少了低级BUG 出现的几率。
for-each循环不会有性能损失,甚至用于数组也一样。实际上,在某些情况下,比起普通的for循环,它还稍有些性能优势,因为它对数组索引的边界值只计算一次。
在对多个集合进行嵌套式迭代时,for-each循环相对于传统for循环的这种优势还会更加明显。下面就是人们在试图对两个集合进行嵌套迭代时常会犯的错误:
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()); //BUG, i被多次迭代
}
上面代码的BUG 是比较隐匿的,很多专家级的程序员也会偶尔犯类似的错误。问题在于,在迭代器上对外部的集合调用了太多次next方法。它应该从外部的循环进行调用,以便每种花色调用一次。下面我们来修复一下上面的代码:
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接口的对象。
遗憾的是,有三种常见的情况无法使用for-each循环:
(1)过滤:如果需要遍历集合,并删除选定的元素,就需要使用显式的迭代器,以便可以调用它的remove方法。
(2)转换:如果需要遍历列表或数组,并取代它部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值。
(3)平行迭代:如果需要并行的遍历多个集合,就需要显式的控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移。
3. 了解和使用类库
假设你希望产生位于0和某个上界之间的随机整数。面对这个任务,许多程序员会编写如下所示的方法:
private static final Random rnd = new Random();
static int random(int n){
return Math.abs(rnd.nextInt()) % n; //rnd.nextInt()返回值在-MaxValue到MaxValue
}
这个方法看起来不错,其实不然。它有三个缺点,第一个是如果n是一个比较小的2的乘方,经过一段相当短的周期之后,它产生的随机数序列会重复。第二个缺点是如果n不是2的乘方,那么平均起来,有些数会比其他的数出现得更为频繁。第三个缺点是它会返回一个落在指定范围之外的数,因为这个方法试图通过调用Math.abs,将rnd.nextInt()映射为一个非负整数int。如果nextInt()返回Integer.MIN_VALUES,那么Math.abs也会返回Integer.MIN_VALUE,假设n不是2的乘方,那么取模操作符%将返回一个负数。
幸运的是,我们并不需要自己来做这些工作,已经有现成的类库可以为你所用,Random.nextInt(int n),返回0到n的随机数,你无需关心实现细节。已经有很多高级算法工程师花了大量的时间来设计、实现和测试这个方法。
使用标准类库的第二个好处是,不必浪费时间为那些与工作不太相关的问题提供特别的解决方案。就像大多数程序员一样,应该把时间花在应用程序上,而不是底层的细节上。
使用标准类库的第三个好处是,它们的性能往往会随着时间的推移而不断提高,无需你做任何努力。因为提供这些标准类库的组织有强烈的动机要使它们运行得更快。
使用标准类库的再一个好处是,可以使自己的代码融入主流,这样的代码更易读,更易维护、更易被大多数的开发人员重用。
标准类库太庞大了,以至于不可能去学习所有的文档,但是每个程序员都应该熟悉java.lang、java.util,某种程序上还有java.io中的内容。
4. 如果需要精确的答案,请避免使用float和double
float 和double 类型主要是为了科学计算和工程计算而设计的。它们执行二进制浮点运算,这是为了在广泛的数值范围上提供较精确的快速近似计算而精心设计的。然而,它们并没有提供完全精确的结果,所以不应该被用于需要精确结果的场合,如货币计算等。
示例一:System.out.println(1.0.3 - .42); 它输出的结果却是0.6100000000000001。
示例二:
如果你手里有1 美元,超市货架上有一排糖果,它们的售价分别为10 美分、20 美分、30 美分,以此类推直到1美元。你打算从标价10 美分的开始买,每个糖果买1 颗,直到不能支付货架上下一中价格的糖果为止,那么你可以买多少糖果?还会找回多少零头呢?见如下代码:
public static void main(String[] args) {
double funds = 1.00;
int itemsBought = 0;
for (double price = .10; funds >= price; price += .10) {
funds -= price;
itemsBought++;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Change: $" + funds);
}
如果真正运行这个程序,你会发现你可以支付3颗糖果,并且还剩下$0.39999999999999。造成这一结果的主要原因就是double 类型
的精度问题。解决该问题的正确办法是使用BigDecimal、int 或者long 进行货币计算。如下:
public static void main(String[] args) {
final BigDecimal TEN_CENTS = new BigDecimal(".10");
int itemsBought = 0;
BigDecimal funds = new BigDecimal("1.00");
for (BigDecimal price = TEN_CENTS; funds.compareTo(price) >=0; price.add(TEN_CENTS)) {
itemsBought++;
funds = funds.substract(price);
}
System.out.println(itemsBought + " items bought.");
System.out.println("Money left over: $" + funds);
}
运行这个程序,就会发现你可以支付4颗糖果,还剩下$0.00。这才是正确的答案。
然而,使用BigDecimal有两个主要缺点:和使用基本运算类型相比,这样做很不方便,而且效率也低。除了该方法之外我们还可以使用int 或者long,至于使用哪种具体类型,需要视所涉及的数值大小而定。现在我们需要将计算单位转换为分,而不再是以元为单位,如下:
public static void main(String[] args) {
int itemsBougth = 0;
int funds = 100;
for (int price = 0; funds >= price; price += 10) {
itemsBought++;
fund -= price;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Money left over: $" + funds + " cents.");
}
总而言之,对于任何需要精确答案的计算任务,请不要使用float或者double。如果你想让系统来记录十进制小数点,并且不介意因为不使用基本类型而带来的不便,就请使用BigDecimal。使用BigDecimal还有一些额外的好处,它允许你完全控制舍入,每当一个操作涉及舍入的时候,它允许你从8种舍入模式中选择其一。如果你正通过法定要求的舍入行为进行业务计算,使用BigDecimal是非常方便的。如果性能非常关键,并且你又不介意自己记录十进制小数点,而且所涉及的数值又不太大,就可以使用int或者long。需要指出的是,如果数值所涉及的范围没有超过9 位十进制数字,就可以使用int,没有超过18 位可以使用long,一旦超过,则必须使用BigDecimal。
5. 基本类型优先于装箱基本类型
Java 的类型系统中主要包含两个部分,分别是基本类型,如int、double、long,还有就是引用类型,如String、List 等。其中每个基本类型都对应着一种引用类型,被称为装箱基本类型,如分别和int、double、long 对应的装箱类型Integer、Double 和Long等。
Java 在1.5 中新增了自动装箱的和自动拆箱的功能。这些特性仅仅是模糊了基本类型和装箱类型之间的区别,但是并没有完全消除他们之间的差异,而这些差别往往会给我们的程序带来一些潜在的问题。我们先看一下他们之间的主要区别:
(1)基本类型只有值,在进行比较时可以直接基于值进行比较,而装箱类型在进行同一性比较时和基本类型相比有着不同的逻辑,毕竟他们是对象,是Object 的子类,它们需要遵守Java 中类对象比较的默认规则。
(2)基本类型只有功能完备的值,而每个装箱类型除了它对应基本类型的所有功能之外,还有一个非功能值:null。
(3)基本类型通常比装箱类型更节省时间和空间。
如下示例:
public class MyTest {
private static int compare(Integer first,Integer second) {
return first < second ? -1 : (first == second ? 0 : 1);
}
public static void main(String[] args) {
Integer first = new Integer(42);
Integer second = new Integer(42);
System.out.println("Result of compare first and second is " + compare(first,second));
}
}
这段代码看起来非常简单,它的运行结果也非常容易得出,然而当我们真正运行它的时候却发现,实际输出的结果和我们的期望是完全不同的,分析: 对表达式first < second执行计算导致被first和secound引用的Integer实例被自动拆箱,将能够正常工作并得到正确的结果,即first < second 为false; 在进行相等性比较的时候问题出现了,如前所述,Integer 毕竟是对象,在进行对象之间的同一性比较时它将遵守对象的同一性比较规则,由于这两个参数对象的地址是不同的,因为我们是通过两次不同的new 方法构建出的这两个参数对象。结果可想而知,first == second 返回false;
下面我们看一下如何修正以上代码中存在的错误:
public class MyTest {
private static int compare(Integer first,Integer second) {
int f = first;
int s = second;
return f < s ? -1 : (f == s ? 0 : 1);
}
public static void main(String[] args) {
Integer first = new Integer(42);
Integer second = new Integer(42);
System.out.println("Result of compare first and second is " +
compare(first,second));
}
}
现在让我们再看一段代码片段:
public class Unbelievable {
static Integer i;
public static void main(String[] args) {
if (i == 42)
System.out.println("Unbelievable");
}
}
程序的运行结果并没有打印出"Unbelievable",而是抛出了空指针异常。这是因为装箱类型的i 变量并没有被初始化,即它本身为null,当程序计算表达式(i == 42)时,它会将Integer 与int进行比较。几乎在任何一种情况下,当在一项操作中混合使用基本类型和装箱基本类型时,装箱类型就会自动拆箱,这种情况无一例外。如果null对象引用被自动拆箱,就会得到一个NullPointerException。修正这一问题也非常简单,只需将i 的类型从Integer 变为int 即可。
再看一下最后一个代码示例:
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; ++i) {
sum += i;
}
System.out.println(sum);
}
这段代码虽然不像之前的两个示例那样有着明显的Bug,然而在运行时却存在着明显的性能问题。因为在执行for 循环时,会有不断的自动装箱和自动拆箱的操作发生。修改该代码也是非常容易的,只需将sum 的类型从Long 变为long 即可。
那么什么时候应该使用装箱基本类型呢?第一个是作为集合中的元素、键和值。你不能将基本类型放在集合中,因此必须使用装箱基本类型。在参数化类型中,必须使用装箱基本类型作为类型参数,因为java不允许使用基本类型(Java 泛型中的类型参数)。另外,在进行反射的方法调用时必须使用装箱基本类型。
总之,当可以选择的时候,基本类型要优先于装箱基本类型。基本类型更加简单,也更加快速。最后,当程序装箱基本类型值时,会导致高开销和不必要的对象创建。
6. 如果其他类型更适合,则尽量避免使用字符串208
字符串被用来表示文本,它在这方面也确实做得很好。因为字符串很通用,并且Java语言也支持得很好。下面讨论一些不应该使用字符串的情形:
(1)字符串不适合代替其他的值类型。如果它是数值,就应该被转换为适当的数值类型,如果它是“是或否”它就应该被转换为boolean类型。
(2)字符串不适合代替枚举类型。
(3)字符串不适合代替聚集类型。如果用来分隔域的字符也出现在某个域中,结果就会出现混乱。
(4)字符串也不适合代替能力表。
7. 当心字符串连接的性能
字符串连接操作(+)是把多个字符串合并为一个字符串的最为便利的途径。因此如果仅仅是对两个较小字符串进行一次连接并输出连接结果,这样是比较合适的。然而如果是为n 个字符串而重复地使用字符串连接操作符,则需要n 的平方级的时间。这是由于字符串对象本身是不可变的,在连接两个字符串时,需要copy 两个连接字符串的内容并形成新的连接后的字符串。见如下代码:
public String statement() {
String result = "";
for (int i = 0; i < numItems(); i++) {
result += lineForItem(i);
}
return result;
}
此时如果项目数量巨大,这个方法的执行时间将难以估量。为了获得可以接受的性能,请使用StringBuilder(非同步StringBuilder类代替了已经过时的StringBuffer类)替代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方法。另一种方法是,使用字符数组,或者每次只处理一个字符串,而不是将它们组合起来。
8. 通过接口引用对象
一般来讲,在函数参数、返回值、域变量等声明中,应该尽量使用接口而不是类作为它们的类型。只有当你利用构造器创建某个对象的时候,才真正需要引用这个对象的类,如:
List<Subscriber> subscribers = new Vector<Subscriber>();
而不是像下面这样的声明:
Vector<Subscriber> subscribers = new Vector<Subscriber>();
如果你养成了用接口作为类型的习惯,你的程序将更加灵活。对于上面的例子,在今后的改进中,如果不想使用Vector 作为实例化对象,我们只需在如下一出进行修改即可:
List<Subscriber> subscribers = new ArrayList<Subscriber>();
周围的所有代码都可以继续工作。周围的代码并不知道原来的实现类型,所有它们对于这种变化并不在意。但有一点值得注意:如果之前的实现提供了某种特殊的功能,而这种功能并不是这个接口的通用约定所要求的,并且周围的代码又依赖于这种功能,那么很关键的一点,新的实现也要提供同样的功能。例如:如果上面的第一个声明依赖于Vector的同步策略,在声明中用ArrayList代替Vector就是不正确的。如果依赖于实现的任何特殊属性,就要在声明变量的地方给这些需求建立相应的文档说明。
那么,为什么要改变实现呢?因为新的实现提供了更好的性能,或者因为它提供了期望得到的额外功能。
那么在哪些情况下不是使用接口而是使用实际类呢:
(1)没有合适的接口存在,如String 和BigInteger 等值对象,值类通常都是final的,很少有对应的接口。
(2)对象属于一个框架,而框架的基本类型是类,不是接口。如果对象属于这种基于类的框架,就应使用基类来引用该对象,如TimerTask。
(3)类实现了接口,但是它提供了接口中不存在的额外方法。如果程序此时依赖于这些额外的方法,这种类就应该只被用来引用他的实例。
简而言之,如果类实现了接口,就应该尽量使用其接口引用该类的引用对象,这样可以使程序更加灵活,如果不是,则使用类层次结构中提供了必要功能的最基础的类。
9. 接口优先于反射机制
核心反射机制提供了通过程序来访问关于已装载的类的信息的能力。给定一个Class 实例,你可以获取Constructor、Method和Field等实例,分别代表了该Class实例所表示的类的Constructor(构造器)、Method(方法)和Field(域)。这些对象提供了通过程序来访问类的成员名称、域类型、方法签名等信息的能力。
而且,Constructor、Method和Field实例可以使你能够通过反射机制操作它们的底层对等体:通过调用Constructor、Method和Field实例上的方法,可以构造底层类的实例、调用底层类的方法,并访问底层类中的域。反射机制允许一个类使用另一个类,即使当前者被编译的时候后者还根本不存在。然而这种能力是需要付出代价的:
(1)丧失了编译时类型检查的好处,包括异常检查。如果程序企图用反射方式调用不存在的或者不可访问的方法,在运行时它将会失败,除非采取了特别的预防措施。
(2)执行反射访问所需要的代码往往非常笨拙和冗长,编写和阅读起来都非常困难,通常而言,一个基于普
通方式的函数调用大约1,2 行,而基于反射方式,则可能需要十几行。
(3)性能损失,反射方法的调用比普通方法调用慢了许多。
核心反射机制最初是为了基于组件的应用创建工具而设计的。它们通常需要动态装载类,并且用反射功能找出它们支持哪些方法和构造器。这些工具允许用户交互式地构建出访问这些类的应用程序,但是所产生出来这些应用程序能够以正常的方式访问这些类,而不是以反射的方式。反射功能只是在设计时被用到,通常普通应用程序在运行时不应该以反射方式访问对象。
有一些复杂的应用程序需要使用反射机制。如类浏览器、对象监视器、代码分析工具、解释性的嵌入式系统等。
如果只是以非常有限的形式使用反射机制,虽然也要付出少许代价,但是可以获得许多好处。对于有些程序,它们必须用到编译时无法获取的类,但是在编译时却存在适当的接口或超类,通过它们可以引用这个类。如果是这样,就可以先通过反射方式创建实例,然后再通过它们的接口或超类,以正常的方式访问这些实例。如果适当的构造器不带参数,甚至根本不需要使用java.lang.reflect包,Class.newInstance方法就已经提供了所需的功能。如下:
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“,则它们就会按照字母顺序打印出来,因为TreeSet中的元素是排好序的。
尽管这个程序就像一个玩偶,但是它所演示的这种方法是非常强大的。这个玩偶程序可以很容易地变成一个通用的集合测试器,通过侵入式地操作一个或者多个集合实例,并检查是否遵守Set接口的约定,以此来验证指定的Set实现。同样地,它也可以变成一个通用的集合性能分析工具,实际上,它所演示的这种方法足以实现一个成熟的服务提供者框架。绝大多数情况下,使用反射机制时需要的也正是这种方法。
但是上面的代码中体现出了反射的两个缺点:一是这个例子有3 个运行时异常的错误,如果不使用反射方式实例化,这3 个错误都会成为编译时错误。第二是根据类名生成它的实例需要20 行冗长的代码,而调用构造器可以非常简洁的只使用一行代码。
另一个值得注意的附带问题是,这个程序使用了System.exit。很少有需要调用这个方法的时候,它会终止整个VM。但是它对于命令行有效性的非法终止是很合适的。
简而言之,反射机制是一种功能强大的机制,对于特定的复杂系统编程任务,它是非常必要的。如果你编写的程序必须要与编译时未知的类一起工作,如有可能,就应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类。
10. 谨慎地使用本地方法
Java Native Interface(JNI)允许Java应用程序可以调用本地方法,所谓本地方法是指用本地程序设计语言,如C/C++来编写的特殊方法。本地方法在本地语言中可以执行任意的计算任务,并返回到Java程序设计语言。
本地方法主要有三种用途,第一是提供了“访问特定于平台的机制”的能力,比如注册表、文件锁等。第二是它们还提供了访问遗留代码 库的能力,从而可以访问遗留数据。第三种是本地方法可以通过本地语言,编写应用程序中注重性能的部分,以提高系统的本能。
使用本地方法来访问特定于平台的机制是合法的,随着Java 平台的不断成熟,它提供了越来越多以前只有宿主平台才有的特性,如java.util.prefs包提供了注册表的功能,java.awt.SystemTray提供了访问桌面系统托盘区有能力。
使用本地方法有一些严重的缺点。因为本地语言不是安全的,所有,使用本地方法的应用程序也不再能免受内存毁坏错误的影响。因为本地语言是平台无关的,使用本地方法的应用程序也不再是可自由移植的。使用本地方法的应用程序也更难调试。在进入和退出本地代码时,需要相关的固定开销,如果本地代码只做少量的工作,本地方法就可能降底性能。最后本地方法编写起来单调乏味,并且难以阅读。
总而言之,极少数情况下会需要使用本地方法来提高性能。如果你必须要使用本地方法来访问底层资源,或者遗留代码库,也要尽可能少用本地代码,并且要全面测试,本地代码中的一个Bug就有可能破坏整个应用程序。
11. 谨慎地进行优化
有三条与优化有关的格言是每个人都应该知道的:
(1)很多计算上的过失都被归咎于效率(没有必要达到的效率),而不是任其他的原因——甚至包括盲目的做傻事。
(2)不要去计较效率上的一些小小的得失,在97%的情况下,不成熟的优化才是一切问题的根源。
(3)在优化方面我们应该遵守两条规则:一是不要进行优化,二是还是不要进行优化——也就是说,在你还没有绝对清晰的未优化方案之前,请不要进行优化。
不要因为性能而牺牲合理的结构,要努力编写好的程序而不是快的程序。如果好的程序不够快,它的结构将使它可以得到优化。
这并不意味着,在完成程序之前就可以忽略性能问题。实现上的问题可以通过后期的优化而得到修正,但是,遍布全局并且限制性能的结构缺陷几乎是不可能被改正的,除非重新编写系统。在系统完成之后再改变设计的某个基本方面,会导致系统的结构很不好,从而难以维护和改进。因此,必须在设计过程中考虑到性能问题。
努力避免那些性能的设计决策。当一个系统设计完成之后,其中最难更改的组件是那些指定了模块之间交互关系以及模块关系以及模块与外界交互关系的组件。在这些设计组件中,最主要的是API、线程层协议以及永久数据格式。这些设计组件不仅在事后难以甚至不可能改变,而且它们都有可能对系统本该达到的性能产生严重的限制。
要考虑API设计决策的性能后果。使公有的类型成为可变的,这可能会导致大量不必要的保护性拷贝。同样地,在适合使用复合模式的公有类中使用继承,会把这个类与它的超类永远地束缚在一起,从而人为地限制了子类的性能。还有在API中使用实现类型而不是接口,会把束缚在一个具体的实现上,即使将来出现更快的实现你也无法使用。
性能剖析工具有助于你决定应该把优化的重心放在哪里,这样的工具可以为你提供运行时的信息,比如每个方法大致上花费了多少时间、它被调用了多少次。除了确定优化的重点之外,它还可以警告你是否需要改变算法。如果一个平方级的算法潜藏在程序中,无论怎么调整和优化都很难解决问题。你必须用更有效的算法来替换原来的算法。
总之,不要费力去编写快速的程序——应该努力编写好的程序,速度自然会随之而来。在设计系统的时候,特别是在设计API、线路层协议和永久数据格式的时候,一定要考虑性能的因素。当构建完系统之后,要测量它的性能。如果它足够快,你的任务就完成了。如果不够快,则可以在性能剖析器的帮助下,找到问题的根源,然后设法优化系统中相关的部分。第一个步骤是检查所选择的算法:再多的低层优化也无法弥补算法的选择不当。必要时重复这个过程,在每次改变之后都要测量性能,直到满意为止。
12. 遵守普遍接受的命名惯例
Java平台建立一套很好的命名惯例,其中有许多命名惯例包含在了《The Java Language Specification》。
如果API违反了这些惯例,它使用起来可能会很困难。如果实现违反了它们,它可能会难以维护。这两种情况下,违反惯例都会潜在地给使用这些代码的其他程序员带来困惑和苦恼,并且使他们做出错误的假设,造成程序出错。
包的名称应该是层次状的,其名称都应该以组织的Internet域名开头,并且顶级域名放在最前面。其余部分应该包括一个或者多个描述该包的组成部分。
类和接口的名称,包括枚举和注解类型的名称,都应该包括一个或者多个单词,每个单词的首字母大写。
方法和域的名称,也应该包括一个或者多个单词,每个单词的首字母小写。
上述规则的唯一例外是"常量域",它的名称应该包含一个或者多个大写的单词,中间用下划线隔开。
类型参数名称通常由单个字母组成:T表示任意的类型,E表示集合的元素类型,K和V表示映射的键和值类型。
发表评论
-
EffectiveJava--序列化
2014-12-03 11:43 1516本章内容: 1. 谨慎地实 ... -
EffectiveJava--并发
2014-12-01 21:53 1742本章内容: 1. 同步访问共享的可变数据 2. 避免过度同步 ... -
EffectiveJava--异常
2014-11-23 21:47 1078本章内容: 1. 只针对异 ... -
EffectiveJava--方法
2014-11-16 17:44 1341本章内容: 1. 检查参数的有效性 2. 必要时进行保护性拷贝 ... -
EffectiveJava--枚举和注解
2014-11-15 21:23 1636本章内容: 1. 用enum代 ... -
EffectiveJava--泛型
2014-11-08 00:22 1208本章内容: 1. 请不要在 ... -
EffectiveJava--类和接口
2014-11-05 00:30 1383本章内容: 1. 使类和成 ... -
EffectiveJava--对象通用方法
2014-10-27 22:49 918本章内容: 1. 覆盖equals ... -
EffectiveJava--创建和销毁对象
2014-10-22 23:38 958本章内容: 1. 考虑用静 ... -
Java多线程
2014-04-28 18:56 1035一、多线程简介 对于Java而言,可以在一个程序中并发地启 ... -
Java异常处理
2014-04-27 00:49 1437一、异常简介 在程序运行过程中,如果环境检测出一个不可以执 ... -
Java集合框架
2014-04-24 23:56 1860一、集合框架简介 数据结构是以某种形式将数据组织在一起的集 ... -
Java编程规范整理
2014-03-29 21:36 1560一、排版1. 代码采用缩进风格编写,缩进空格数为4,不允许 ...
相关推荐
《Effective Java》是Java开发领域的一本经典著作,由知名程序员Joshua Bloch撰写。这本书深入探讨了如何编写高效、可维护的Java代码,并提出了许多实用的编程实践和设计原则。通过对书中源代码的分析,我们可以更...
目录:一、创建和销毁对象 (1 ~ 7)二、对于所有对象都通用的方法 (8 ~ 12)三、类和接口 (13 ~ 22)四、泛型 (23 ~ 29)五、枚举和注解 (30 ~ 37)六、方法 (38 ~ 44)七、通用程序设计 (45 ~ 56)八、异常 ...
### Java并发程序设计教程知识点概览 #### 一、使用线程的经验:设置名称、响应中断、使用ThreadLocal 在Java并发编程中,正确管理线程对于构建稳定、高效的多线程应用程序至关重要。命名线程是一项基本但重要的...
### C++程序设计基础 #### 1. C++语言概述 - **定义与历史**:C++是一种通用的、面向对象的编程语言,由Bjarne Stroustrup在1979年开始设计并实现。它是在C语言的基础上发展起来的,并添加了类、模板等特性。 - **...
### 程序设计经典书目推荐 #### 一、编程语言 对于游戏开发者来说,掌握一门或多门编程语言是至关重要的。当前游戏行业的主流编程语言主要包括 C/C++,此外,针对移动平台(如智能手机)的游戏开发则常用 J2ME...
根据提供的信息,“Effective Java 中文版 第二版”这本书主要关注的是Java编程语言的最佳实践、设计模式以及如何编写高效、可维护的Java代码。虽然给定的部分内容并未提供实际的文字内容,但从标题和描述来看,我们...
- Java编程书籍: 如《Thinking in Java》、《Effective Java》等。 - 在线教程和博客: 许多开发者分享的实践经验和技术文章。 综上所述,Java绘图板项目的开发不仅是一项实际的编程任务,更是深入学习Java语言...
### 《Effective Java》知识点概览 #### 一、引言 《Effective Java》是一本由Joshua Bloch编写的经典书籍,首次出版于2001年,书号为ISBN:0-201-31005-8,共包含272页。该书旨在帮助读者更好地理解Java编程语言,...
- 《Effective Java》 - 《Thinking in Java》 - 《Java编程思想》 - 《Java并发编程实战》 2. **解析XML的主要方法有哪些?各有什么区别?** XML解析主要有以下几种方法: - **DOM (Document Object Model)...
- **《Effective Java》**: 关于Java编程的最佳实践。 - **《Clean Code》**: 关于编写清晰简洁代码的原则和技术。 - **《Design Patterns: Elements of Reusable Object-Oriented Software》**: 介绍设计模式的经典...
- 讨论了经典书籍《Effective Java》第二版中文翻译的术语选择,以促进中文读者更好地理解和应用书中的编程原则。 14. **AOP缓存再讨论** - 面向切面编程(AOP)中的缓存机制是提高性能的重要手段,讨论可能涉及...
综上所述,《Java核心技术》、《Effective Java》、《Java并发编程实战》以及《深入理解Java虚拟机》四本书分别从不同的角度全面覆盖了Java开发中的关键技术点。无论是初学者还是有一定经验的开发者都能从中获得宝贵...
最后,遵循Java社区认可的最佳实践,如Google Java Style Guide、Effective Java等书籍提供的指导原则,可以帮助开发者写出高质量、高效率的代码。 总之,Java开发不仅仅是掌握语言本身,更是理解其背后的生态系统...
- **建议**:可以通过阅读《C程序设计语言》等书籍来打下坚实的C语言基础。 #### 2. 核心参考书籍 - 推荐学习《Thinking in C++》,这本书对于理解面向对象编程非常有帮助。 - **建议**:同时参考《The C++ ...
- **C# 程序设计**:Charles Petzold 著,被誉为 Windows 编程领域的经典之作,也是学习 C# 语言的重要参考书目之一。 以上书籍都是各自领域的经典之作,对于初学者和有一定经验的开发者来说都非常有价值。通过...
这种灵活性和复用性使得泛型编程成为现代C++程序设计的重要组成部分。 **特点:** - **通用性**:通过使用模板(templates),程序员可以编写能够处理多种数据类型的函数或类。 - **类型安全**:编译器会在编译时对...
Java是一种跨平台应用软件的面向对象的可编程语言,是由Sun Microsystems公司于1995年推出的Java程序设计语言和Java平台(即JavaSE,JavaEE,JavaME)的总称。Java技术广泛应用于个人电脑、互联网、数据中心、移动...