首页 > 代码库 > [Effective Java]第八章 通用程序设计
[Effective Java]第八章 通用程序设计
第八章 通用程序设计
45、 将局部变量的作用域最小化
将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。
要使用局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方才声明,不要过早的声明。
局部变量的作用域从它被声明的点开始扩展,一直到外围块的结束外。如果变量是在“使用它的块”之外被声明有,当程序退出该块之后,该变量仍是可见的,如果它在目标使用区之前或之后意外使用,将可能引发意外错误。
几乎每个局部变量的声明都应该包含一个初始化表达式,如果你还没有足够信息来对象一个变量进行有意义的初始化,就应该推迟这个声明,直到可初始化为止。但这条规则有个例外的情况与try-catch语句有关。如果一个变量被一个方法初始化,而这个方法可能会抛出一个检测性异常,该变量就必须在try块内部被初始化。如果变量的值必须在try块的外部使用到,它就必须在try块之前被声明,但是在try块之前,它还不能“有意义地初始化”,请参照第53条中的异常实例。
循环中提供了特殊的机会来将变量的作用域最小化。如果在循环终止之后不再需要使用循环变量的内容,for循环就优先于while循环。例如,下面是一种遍历集合的首选做法:
for(Element e: c){
doSomething(e);
}
在1.5前,首先做法如下:
for(Iterator i = c.iterator();i.hasNext();){
doSomething((Element) i.next());
}
为了弄清为什么这个for循环比while循环更好,请看下面代码:
Iterator<Element> i = c.iterator();
while(i.hasNext()){
doSomething(i.next());
}
…
Iterator<Element> i2 = c2.iterator();
while(i.hasNext()){//Bug!
doSomething(i2.next());
}
CP过来的代码未修改完,结果导致for循环编译通不过。
最后一种“将局部变量的作用域最小化”的方法是使方法小而集中。
----------------------
补充,不能在while条件中声明变量,这与for循环不一样,也不能像下面这样在while体中声明一个变量:
while(true)
int i = 1;
只可以这样:
while(true){
int i = 1;
}
46、 for-each循环优先于传统的for循环
1.5前,遍历集合的首选做法如下:
for(Iterator i = c.iterator(); i.hasNext();){
doSomething((Element)i.next());
}
遍历数组的首选做法如下:
for(int i =0; i < a.length;i++){
doSomething(a[i]);
}
虽然这些做法比while循环理好,但并不完美,因为迭代器和索引变量在每个循环中出现三次,其中有两次让你出错。
1.5中完全隐藏迭代器或者索引变量,避免了混乱和出错的可能,下面这种模式同样适合于集合与数组:
for(Element e : elements){
doSomething(e);
}
集合内嵌迭代时问题:
Collection<Suit> suits = ...;
Collection<Rank> ranks = ...;
List<Card> deck = ...;
for (Iterator<Suit> i = suits.iterator(); i.hasNext();)
for (Iterator<Rank> j = ranks.iterator(); j.hasNext();)
deck.add(new Card(i.next(), j.next()));//i.next在内部循环中多次调用,会出现问题
将i.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循环不仅让你遍历集合数组,还让你遍历任何实现Iteralble接口的对象。这个简单的接口接口由单个方法组成,与for-each循环同时被增加到Java平台中,这个接口如下:
public interface Iterable<E>{
Iterator<E> iterator();
}
总之,for-each循环在简洁性和预防Bug方面有着传统的for循环无法比拟的优势,并且没有性能损失。应该尽可能地使用for-each循环,遗憾的是,有些情况是不适用的,比如需要显示地得到索上或迭代器然后进行其他操作,或者是内部循环的条件与外部有直接关系的(比如内层循环的起始值依赖于外层循环的条件值)。
47、 了解和使用类库
假如你希望产生位于0和某个上界之间的随机整数,你可以会这么做:
privae static final Random rnd = new Random();
static int random(int n){
return Math.abs(rnd.nextInt())%n;
}
上面程序产生的0到n间的整数是不均的,使用类库中的Random类的nextInt(Int)可以解决,这些方法是经过专家们设计,并经过多次测试和使用过的方法,比我们自己实现可靠得多。
通过使用标准类库,可以充分利用这些编写标准类库的专家的知识,以及在你之前的其他人的使用经验。
使用标准类库中的第二个好处是,不必关心底层细节上,把时间应花在应用程序上。
使用标准类库中的第三个好处是,它们的性能往往会随着时间的推移而不断提高,无需你做任何努力。
本条目不可能总结类库中所有便利工具,但是有两种工具值得特别一提。一个是1.2发行版本中的集合框架,二是1.5版本中,在java.util.concurrent包中增加了一组并发实用工具,这个包既包含高级的并发工具来简化多线程的编程任务,还包含低级别的并发基本类型,允许专家们自己编写更高级的并发抽象,java.util.concurrent的高级部分,也应该是每个程序员基本工具箱中的一部分。
总之,不要重新发明轮子,已存在的我们就直接使用,只有不能满足我们需求时,才需自己开发,总的来说,多了解类库是有好处的,特别是为库中的工具包。
48、 如果需要精确的答案,请避免使用float和double
float和double类型主要是用来为科学计算和工程计算而设计的。它们执行二进制浮点运算,这是为了在广泛的数值满园上提供较为精确的快速近似计算而精心设计的,然而,它们并没有提供完全精确的结果,所以不应该被用于需要精确结果的场合。float和double类型尤其不适合用于货币的计算,因为要让一个float和double精确地表示0.1(或者10的任何其他负数次方值)是不可能的。
System.out.println(1.0-.9);// 0.09999999999999998
请使用BigDecimal、int或long(int与long以货币最小单位计算)进行货币计算。
使用BigDecimal时请还请使用BigDecimal(String),而不要使用BigDecimal(float或double),因为后者在传递的过程中会丢失精度:
new BigDecimal(0.1)// 0.1000000000000000055511151231257827021181583404541015625
new BigDecimal("0.1")//0.1
使用BigDecimal有两个缺点:与使用基本运算类型相比,这样做很不方便,而且很慢。
如果性能非常关键,请使用int和long,如果数值范围没有超过9位十进制数字,就可以使用int;如果不超过18位数字,就可以使用long,如果数字可能超过18位数字,就必须使用BigDecimal。
49、 基本类型优先于包装基本类型
Comparator<Integer> naturalOrder = new Comparator<Integer>() {
public int compare(Integer first, Integer second) {
/* 因为
* first < second 运行时会自动拆箱
* first == second 运行时不会自动拆箱
*/
return first < second ? -1 : (first == second ? 0 : 1);
}
};
//比较两个值相等的Integer
int result = naturalOrder.compare(new Integer(42), new Integer(42));
System.out.println(result);//所以结果为 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); // 按基本类型比较
}
};
int result = naturalOrder.compare(new Integer(42), new Integer(42));
System.out.println(result);//0
接下来,考虑这个小程序:
public class Unbelievable {
static Integer i;
public static void main(String[] args) {
if (i == 42)// !! 抛空指针异常
System.out.println("Unbelievable");
}
}
当一个项操作中混合使用基本类型和包装基本类型时,装箱基本类型就会自动拆箱,上面就是这样,如果null对象引用被自动拆箱,就会得到NullPointerException异常。修正这个问题很简单,声明i是个int而不是Integer就可以了。
最后考虑这个程序:
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.5中,也不会自动拆箱后比较,所以不管是1.5前还是以后,==都是比较的地址。
50、 如果其他类型更适合,则尽量避免使用字符串
字符串不适合代替其他的值类型。数组经过文件、网络,或键盘输出设置进入到程序中之后,它通常是以字符形式存在,但我们应该尽量将他们转换为确切的类型。
如果可以使用更加合适的数据类型,或者可以编写更加适当的数据类型,就应该避免用字符串来表示对象。若使用不当,字符串会比其他类型更加笨拙、更不灵活、速度慢,也更容易出错。经常被错误地用字符串来代替的类型包括基本类型、枚举类型和聚集类型。
51、 当心字符串连接的性能
由于字符串是不可变的,连接操作会产生新的字符串对象。所以不适合运用在大规模的场景中。
考虑下面的方法,它通过反复连接每个项目行,构造出一个代表该账单的字符串:
// Inappropriate use of string concatenation - Performs horribly!
public String statement() {
String result = "";
for (int i = 0; i < numItems(); i++)
result += lineForItem(i); // String concatenation
return result;
}
如果项目数量巨大,这个方法执行的时间就难以估算。为了获得可以接受的性能,请使用StringBuilder替代String(1.5中增加了非同步的StringBuilder类,代替了现在已经过时的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();
}
上述两种做法的性能差别非常大,第一种做法的开销随着项目数量而呈平方级增加,第二种做法是线性增加,所以项目越大,性能的差别会越显著。但要注意的是,第二种做法预先分配了一个StringBuilder,使它大到足以容纳结果字符串,即使使用默认大小(16)的StringBuilder,它仍比第一种快。
原则很简单:不要使用字符串连接操作符“+”来合并多个字符串,除非性能无关紧要。相反,应该使用StringBuilder的append方法。另一种方法是,使用字符数组,或者每次只处理一个字符串,而不是将它们组合起来。
——end
以上是effective java的建议,但说的不够精准,这里需要补充一下。
在JDK1.5中:String s = "a" + "b" + "c"; 在编译时,编译器会自动引入java.lang.StringBuilder类,并使用StringBuilder.append方法来连接。虽然我们在源码中并没有使用StringBuilder类,但是编译器自动地使用了它,因为它更高效。在1.4时或之前使用的是StringBuffer连接的。
虽然在JDK1.5或以上版本中使用“+”连接字符串时为避免产生过多的字符串对象,编译器会自加使用StringBuilder类来优化,但是如果连接操作在循环里,编译器会为每次循环都创建一个StringBuilder对象,所以在循环里一般我们不要直接使用“+”连接字符串,而是自己在循环外显示的创建一个StringBuilder对象,用它来构造最终的结果。但是在使用StringBuilder类时也要注意,不要这样使用:StringBuilder.append(a + ":" + c); ,如果这样,那编译器就会掉入陷井,从而为你另外创建一个StringBuilder对象处理括号内的字符串连接操作。
上面第一个例子中,是一个涉及到循环的字符串连接,由于循环次数是不确定的,我们无法将整个连接过程用单个表达式描述,所以编译器不得不隐式地为每一个表达式创建一个 StringBuffer 的对象,这才是导致运行效率低下的原因。也正是在这种前提下,显式地使用一个 StringBuffer 来进行字符串连接才能提高运行效率。
所以,如果我们最终要得到的字符串是可以通过一个表达式就连接而成的话(如String s = "a" + "b" + "c";),那么无论是用“+”还是 StringBuffer 在编译后的运行效率是完全一样的。相比之下,使用“+”的可读性恐怕还要更好些,因为1.4或之前的代码中使用“+”的在现在1.5中编译时会使用StringBuilder,但那些已经显式使用了 StringBuffer 的代码就不得不靠手工维护了,所以使用“+”在有时(可以通过一个表达式就能搞定的话)可能会更好一些。
总之,在大多数情况下,我们应该尽可能将整个字符串的连接集中在一个表达式里描述,然后让编译器来替我们使用 StringBuffer/StringBuilder ,只有当字符串的连接不得不涉及到多条语句的时候,才有必要显式的使用 StringBuffer/StringBuilder。
下来看一下例子:
void f() {
String a = "a";
String b = "b";
String c = "c";
//这里只产生一个StringBuilder
String s1 = a + b + c;
//从字节码中可以看现,下面会产生两个StringBuilder
String s2 = a + b;//这里会产生一个StringBuilder
s2 = s2 + c;//这里还会产生一个StringBuilder
}
从上面可以看出,如果使用“+”接连拉,要尽量将连接操作在一个表达式中完成,而不要在多个表达式中进行连接,因为一个表达式会产生一个StringBuilder,多个连接表达式就会产生多个StringBuilder。
52、 通过接口引用对象(针对接口编程)
如果有适合的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明。只有当你利用构造器创建某个对象的时候,才真正需要引用这个对象的类。
// 应该这样:
List<Foo> foo = new Vector<Foo>();
// !! 不应该这样:
Vector <Foo> foo = new Vector<Foo>();
如果你养成了用接口作为类型的习惯,你的程序将会更加灵活。当你决定更换实例时,所要做的就是只要改变构造器中类的名称(或者使用一个不同的静态工厂),例如,第一个声明可以改成:
List<Foo> foo = new ArrayList<Foo>();
周围的所有代码都可以继承工作,因为它们不知道原来的实现类型,所以不关注这种变化。
有一点值得注意:如果原来的实现提供了某特殊的功能,而这种功能并不是这个接口的通用约定所要求的,并且周围的代码又依赖于这种功能,那么关键的一点是,新的实现也要提供同样的功能。例如,如果代码依赖于Vector的同步功能,在声明中用ArrayList代替Vector就不正确了。如果依赖于实现的任何特殊属性,就要在声明变量的地方给这些需求建立相应的文档说明。
如果没有合适的接口存在,完全可以用类而不是接口来引用对象。例如,考虑值类,比如String和BigInteger。值类很少会用多个实现编写。它们通常是final的,并且很少有对应的接口。使用这种值类作为参数、变量、域或者返回类型是再合适不过了。
如果没有接口,但存在抽象类时,我也要优先考虑使用这个最基础的类来定义的类型。
不存在适当接口类型的最后一种情形是,类实现了接口,但是它提供了接口中不存在的额外方法——例如LinkedHashMap。如果程序依赖于这些额外的方法,这种类就应该只被用来引用它的实例,它很少应该被用作参数类型(第40条)。
53、 接口优先于反射机制(使用接口类型引用反射创建实例)
反射的代价:
1、 丧失了编译时类型检查的好处,包括异常检查。如反射的东西不存在时,在运行时将会失败。
2、 执行反射访问所需要的代码非常笨拙和冗长。
3、 性能损失。反射方法调用比普通方法调用慢了许多。
核心反射机制最初是为了基本组件的应用创建工具而设计的,普通应用程序在运行时不应该以反射方式访问对象。
如果只是以非常有限的形式使用反射机制,虽然也要付出小许代价,但是可以获得许多好处。对于有些程序,它们必须用到编译时无法获取的类,但是在编译时存在适当的接口或者是超类,通过它们可以引用这个类。如果是这种情况,就可以使用反射方式创建实例,然后通过它们的接口或者超类,以正常的方式访问这些实例。如果调用的构造器不带参数,我们根本不需要使用java.lang.reflect中的Constructor来反射出构造器对象,而是可以直接使用Class.newInstance方法就已经提供了所需要的功能。
类对于在运行时可能不存在的其他类、方法或者域的依赖性,用反射法进行管理,这种用法是合理的,但是很少使用。
总之,反射机制是一种功能强大的机制,对于特定的复杂系统编程任务,它是非常必要的,但它也是有一些缺点。如果你编写的程序必须要与编译时未知的类一起工作,如有可能,应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类。
54、 谨慎使用本地方法
Java Native Interface(JNI)允许Java应用程序可以调用本地方法,所谓本地方法是指用本地程序设计的语言(如C或者C++)来编写的特殊的方法。它可以在本地语言中执行任意的计算任务后返回到Java语言。
本地方法主要有三种用途。它们提供了“访问特定于平台的机制”的能力,比如访问注册表和文件锁。它们还提供了访问遗留代码库的能力,从而可以访问遗留数据。最后,本地方法可以通过本地语言,编写应用程序中注重性能的部分,以提高系统的性能。
使用本地方法来访问特定于平台的机制与访问遗留代码是合法的。但使用本地方法来提高性能的做法不值得提倡,因为VM在逐渐的更优更快了,如1.1中BigInteger是在一个用C编写的快速多精度运行库的基础上实现的,但在1.3中,则完全用Java重写了,并进行了精心的性能调优,比原来的版本甚至更快一些。
使用本地方法有一些严重的缺点。因为本地语言不是安全的、不可移植、难调试,而且在进行本地代码时,需要相关的固定开销,所以如果本地代码只是做少量的工作,本地方法就可能降低性能。
总之,本地方法极少数情况下会需要使用本地方法来提高性能。如果你必须要使用本地方法访问底层的资源,或者遗留代码,也要尽可能少的使用本地代码。
55、 谨慎地进行优化
有三条与优化有关的格言是每个人都应该知道的:
1、 很多计算上的过失都被归咎于效率(没有必要达到的效率),而不是任何其他原因——甚至包括盲目地做傻事。
2、 不要去计较效率上的一些小小的得失,在97%的情况下,不成熟的优化才是一切问题的根源。
3、 在优化方面,我们应该两条规则:
规则1:不要进行优化。
规则2(仅针对专家):还是不要进行优化——也就是说,在你还没有绝对清晰的未优化方案之前,请不要进行优化。
所有这些格言都比Java程序设计语言的出现早了20年,它们讲述了一个关于优化的深刻真理:优化的弊小于利,特别是不成熟的优化。在优化过程中,产生软件可能既不快速,也不正确,而且还不容易修正。
不要因为性能而牺牲合理的结构。要努力编写好的程序而不是快的程序。好的程序体现了信息隐藏的原则:只要有可能,它们就会设计决策集中在单个模块里,因此,可以改变单个的决策而不会影响到系统的其他部分。
必须在设计过程中考虑到性能问题。遍布全局并且限制性能的结构缺陷几乎是不可能被改正的,除非重新编写系统。
要考虑API设计决策的性能后果。使公有的类型成为可变的,这可能会导致大量不必要的保护性拷贝。同样,在适合使用复合模式的公有类中使用继承,会把这个类与它的超类永远地束缚在一起,从而人为地限制了子类的性能。最后一个例子,在API中使用实现的类型而不是接口,会把你束缚在一个具体的实现上,即使将来出现更快的实现你也无法使用。
一旦谨慎地设计了程序并且产生了一个清晰、简明、结构良好的实现,那么就到了该考虑优化的时候了,假定此时你对程序的性能还是不满意。
总之,不要费力去编写快速的程序——应该努力编写好的程序,速度自然会随之而来。在设计系统的时候,特别是在设计API、线路层协议和永久数据库格式的时候(模块之间的交互与模块与外界的交互一旦定下来后是不可能更改的),一定要考虑性能的因素。当构建完系统之后,要测量它的性能。如果它足够快,你的任务就完了。如果不够快,则可以在性能剖析器的帮助下,找到问题的根源,然后设法优化系统中相关的部分。第一个步骤是检查所选择的算法:再多的低层优化也无法弥补算法的选择不当。
>>>《Practical Java》性能拾遗<<<
实践28:先要把焦点放在设计、数据结构和算法身上,不要因要求提高程序执行速度度,而放弃了良好、可靠的设计,转而是追求不可能达到或甚小的性能改良。
产生运行快的代码的一个规则是,只优化必要的部分。花费时间将代码优化,却未能给程序带来实质性的性能影响,就是在浪费时间。如果80%-90%的程序执行的时间花费在10%-20%的代码上面(80-20法则),那你最好找出这需要改善的10%-20%代码然后进行优化。
请记住,高效代码与良好的设计、明智地选择数据结构和明智地选择算法三者的密切程度,远大于与实现语言的关系。
实践29:不要依赖于编译期的优化技术。比如在编译时使用 javac -o,-o选项不一定能够为运行期产生优化代码。以前可能做过一些内联函数的优化,但后来这个优化选项被取消了(因为这个选项产生的代码并不会比你自己撰写的更好),不过那些能内联的函数的确在“运行期”由JIT进行了内联动作。在Java中这样的函数可以内联:如果函数体小而且可以由编译器静态决议,它就可以被视为内联的候选者。所谓“可被静态决议”的函数,就是“不能被覆写”的函数,不能覆写的函数是private、static或final函数。
程序员必须明白,“常见”的Java编译器几乎做不了什么优化工作,面对这种情况,我们只有三个选择:
1、 手工优化Java源码,以求获得更好的性能。
2、 使用某个第三方优化编译器,将源码编译为优化的bytecode。
3、 依靠诸如JIT或Hotspot这新的“运行期优化技术”。
实践30:理解运行期代码优化技术。JIT的目的在于将bytecode于运行期转换为本机二进制码,它是一种运行期代码优化技术,有大部分桌面系统和企业系统的JVMS伴随有JIT。JIT优化时需先运转起来,这也是需要消耗时间的,它是针对相对较少的运行时间而设计,因为它们是存在是为了加速代码,如果JIT做的工作越多,它运行的时间也就越长,如果这样你的程序运行时间就会更长。
实践31:使用StringBuffer或StringBuilder进行字符连接要优于String。非并发环境下优先使用非线程安全集合类。
实践32:将对象的创建成本降到最低。创建一个轻对象就比创建一个重型对象快得多。所谓轻型对象是指:既不具有长继承链,也不含有其他引用域。重型对象恰恰相反。创建一个对象要经过heap的分配、内存清零、初始化最深层父类的域、调用最深层父类构造函数、沿着继承树向下到本类初始化域与执行构造器一系列的动作,所以创建重型对象将可能会比较慢。
实践33:对象的创建成本是非常昂贵的,绝对不要创建一些不必要的对象:
public int[] addArrays1(int[] arr1, int[] arr2) {//优先前
//下面创建的两个变量过早,可能用不着
int[] result = new int[ArraySize];
IllegalArgumentException exc = new IllegalArgumentException(
"arrays are invalid");
if (arr1 != null && arr2 != null && arr1.length == ArraySize
&& arr2.length == ArraySize) {
for (int i = 0; i < ArraySize; i++)
result[i] = arr1[i] + arr2[i];
} else
throw exc;
return result;
}
public int[] addArrays2(int[] arr1, int[] arr2) {//优先后
if (arr1 != null && arr2 != null && arr1.length == ArraySize
&& arr2.length == ArraySize) {
//只有在必要时才创建对象
int[] result = new int[ArraySize];
for (int i = 0; i < ArraySize; i++)
result[i] = arr1[i] + arr2[i];
return result;
}
//只有在必要时才创建对象
throw new IllegalArgumentException("arrays are invalid");
}
实践34:将同步化降至最低。
public synchronized int top1(){
return intArr[0];
}
public int top2(){
synchronized(this){
return intArr[0];
}
}
我们可以通过生成的字节码可以看出top2比top1体积大且还慢。因为top2中它要进行和异常的处理。记住,top1虽然带有synchronized修饰符,这并不会生成额外代码(性能还是会打折扣的),但top2会。如果你在函数体中使用synchronized,就会产生操作码monitorenter和moniterexit的bytecode,以及为了处理异常而附加的代码,之所以这样是为了在出现异常后确保退出前释放锁,所以top1比top2性能略高一些。总之,不管是同步方法还是使用同步块,都会大大降低性能,但如果整个函数都需要被同步化,则为了产生体积较小的且执行速度较快的代码,请优先使用函数修饰符,而不是在函数内使用synchronized块。
如果我们不需要synchronized方法,但我们又无法修改被调用的类,我们可以继承这个类,来重写那个被同步了的方法,不过样会增加代码维护量,不建议这么做,只是说可以这样实现。
当然,同步方法是可能会导致长时间的占用锁而导致并发性降低,一般只有短的方法才使用,如果方法体很长,而且有些代码行是不需要同步访问的,这时我们使用同步块为了高并发性可能会好些。
实践35:尽可能使用stack(局部)变量。访问stack变量要快于静态的或实例域,因为VM所做的相应工作远少于访问static变量或instance变量所做的工作。考虑下面代码:
class StackVars {
private int instVar;
private static int staticVar;
// 访问局部stack变量
void stackAccess(int val) {
int j = 0;
for (int i = 0; i < val; i++)
j += 1;
}
//访问实例域变量
void instanceAccess(int val) {
for (int i = 0; i < val; i++)
instVar += 1;
}
// 访问静态域变量
void staticAccess(int val) {
for (int i = 0; i < val; i++)
staticVar += 1;
}
}
经过测试发现stackAccess快于其它两个方法,不过可以优先它们,经过下面的优化后性能与stackAccess不相上下:
// 访问实例域变量
void instanceAccess(int val) {
int j = instVar;
for (int i = 0; i < val; i++)
j += 1;
}
// 访问静态域变量
void staticAccess(int val) {
int j = staticVar;
for (int i = 0; i < val; i++)
j += 1;
}
实践36:使用static、final和private函数以促成inlining(内联),此类函数可以在编译期被静态决议,让这此函数成为inlining的候选者,因为以函数本体替换函数调用会使代码执行更快。通常inlined函数包括class中常见而小巧的取值函数和设值函数,这些函数往往只有一两行代码。这些函数不会被目前市面上的大部Java编译器“inline化”,不过它们将被目前市面上大部分的JITs于运行期“inline化”,并导致性能显著的提升。另外,inlined函数只有在“被多次调用”的情况下,才会获得令人满意的性能提升。
实践37:instance变量初始化一次就好。不要在构造器内重复对实例域赋类初始默认值,也不要在定义时给实例域重新赋类初始默认值,这都是画蛇添足的做法,反而会影响性能,因为在调用构造器时或之前都会再一次执行赋值操作,而赋的这些值都是在对象内存分配时VM就已经将实例域初始化为相应的缺省值了。下面是错误的做法:
class Foo {
private int count;
private boolean done = false;//多余
private Vector vec;
public Foo() {
count = 0;//多余
vec = new Vector(10);
}
}
具有优化能力的编译器理应消除这些多余的赋值操作,不幸的是许多编译器没有这种能力。
但要记住,local变量没有缺省值,因此你必须将它明确地初始化,否则编译不通过。
实践38:使用基本类型使代码更快更小。基本类型体积(所需存储空间)小,访问快;而包装类型体积大,访问慢。考虑以下函数,stack之中有一个基本类型的int变量与一个reference指向的Integer对象:XXXXXXXXXXXXXXXXXX
实践39:不使用使用迭代器来遍历ArrayList、Vector,而是直接使用数组索引在通过for循环遍历。
实践40:使用System.arraycopy()来复制数组(在1.6中可以使用Arrays.copyOf方法更简洁),不需要使用for循环来循环拷贝每个元素,System.arraycopy()是本地函数,它可以直接、高效地移动“原array”的内存内容到“目标array”,这个动作之快中以消减本机函数的调用代价,固然调用函数需要代价,但那怕是在通过for循环直接面方法体里,也不及调用一下本地方法。
实践41:优先使用数组,然后才考虑ArryaList和Vector。如果不需同步,也请先使用ArrayList。
实践42:尽可地复用已存在的对象。
实践43:延迟加载相关的域。对有些不必马上初始化,而是等到使用时再初始化,不必要一下将整个对象都初始化完整。
实践44:以手工方将代码优化。
1、 删除空白函数
2、 删除无用代码
3、 使用复合赋值操作:i = i + x 改为 i += x;
4、 如果果能,尽量将变量声明成final,以便于在编译时期就能执行计算。
5、 对于方法里重复出现的表达式(比如i+j出重出现)或重复调用的有结果的方法(如果ArrayList的size())时,一定要将它们先赋值给一个临时变量后再使用这个临时变量来代替使用。
6、 对于小循环(次数少),我们可以将它展开,不使用循环,从而产生更快的执行效果,因为展开后相于变相的inline。
7、 简化代数:
int a = 1 ,b =2, c = 3;
int x = f*a + f*b + f*c;
可以做如下优化:
int x = f*(a + b + c);
8、 使用临时变量代替循环体内的固定表达式或返回结果不变的方法。
void fuc(int a, int b, List list) {
for (int i = 0; i < list.size(); i++) {
list.add(Integer.valueOf(i + a + b));
}
}
上面可做如下优化:
void fuc(int a, int b, List list) {
int x = a + b;
int size = list.size();
for (int i = 0; i < size; i++) {
list.add(Integer.valueOf(i + x));
}
}
实践45:编译为本机代码。缺点是失去了跨平台性。
实践46:使用包装类型时,如果要构造的包装类型是-128到127间时,请使用valueOf静态工厂方法来构造,不要使用new,因为这样会使用缓存中的包装对象(包装类型都是不可变的,所以缓存具有意义)。
56、 遵守普遍接受的命名惯例
任何将在你的组织之外使用的包,其名称都应该以你的组织的Internet域名形状,并且顶级域名放在前面,例如com.sum。标准类库和一些可选的包,其名称以java和javax开头,这属于例外,用户创建的包名绝不能以java和javax开头。包名称的其余部分应该包括一个或者多个描述该包的组成部分,这些组成部分应该比较简短,通常不超过8个字符,使用单词缩写或字母缩写。
类和接口的名称,包括枚举和注解类型的名称,都应该使用一个或者多个单词,每个单词的首字母大写,例如Timer和TimerTask。应该尽量避免用缩写,除非是一些首字母写和一些通用的缩写,比如max和min。对于首字母缩写,强烈建义采用仅首字母大写的形式,如HttpUrl就比HTTPURL好。
方法和域的名称与类和接口的名称一样,都遵守相同的字面惯例,只不过方法或者域的名称的第一个字母应该小写,哪怕第一个单词全由缩写组成。
上述规则的唯一例外的是“常量域”,它的名称应该包含一个或者多个大写的单词,中间用下划线符号隔开,例如VALUES域NEGATIVE_INFINITY。注意,常量域是唯一推荐使用下划线的情形。
局部变量名称的字面命名惯例与成员名称类似,只不过它也允许缩写。
类型变量名称通常单个字母组成,这个字母通常是以下一种上类型之一:T表示任意类型,E表示集合元素类型,K和V表示映射的键和值类型,X表示异常。如果同时有多个类型参数,则可以是T、U、V或T1、T2、T3。
语法命名惯例比字面惯例更加灵活,也更有争议。对于包而言,没有语法命名惯例。类包括枚举类型,通常用一个名词或者名词短言命名,如Timer、BufferedWriter。接口的命名与类相似,例如Collection或Comparator,或者使用一个以“-able”或者“-ible”结尾的形容词,例如Runnable、Iterable或者Accessible。由于注解类型有很多用处,因此没有单独安排词类。名词、动词、介词、形容词都常用。
执行某个动作的方法通常用动词或者动词短语来命名,例如append或drawImage。对于返回boolean值的方法,其名称往往以单词“is”开头,很少使用has,后面跟名词或名词短语,或者任何具有形容词功能的单词或短语,如isDigit、isEmpty、isProbablePrime、isEnabled或者hasSiblings。
如果方法返回被调用对象的一个非boolean的函数或者属性,它通常用名词、名词短语,或者以动词“get”开资源丰富的动词短语来命名,例如size、hashCode或者getTime。但如果方法所在的类是个Bean,就要强制使用以“get”开头的形式。另外,如果这个类包含一个方法用于设置同样的属性,则强烈建议采用这种形式,在这种情况下,这两个方法应该被命名为getAttribute和setAttribute。
有些方法的名称需专门提出来说。转换对象类型的方法、返回不同类型的独立对象的方法,通常被称为toType,如toString和toArray。返回视图(视图的类型不同于接收对象的类型)的方法通常被称为asType,例如asList。返回一个与被调用对象同值的基本类型方法,通常被称为typeValue,如intValue。静态工厂的常用名称为valueOf、of、getInstance、newInstance、getType和newType(见第1条)。
域名称的语法惯例没有很好地建立起来,也没有类、接口和方法的惯例那么重要,因为域会很少暴露出来的。boolean类型的域与返回boolean类型的访问方法很类似,但是省去了开头的“is”,例如initialized和composite。其它类型的域通常用名词或名词短语来命名,比如height、digits或bodyStyle。局部变量的方法惯例类似于域的语法惯例。但是更弱一些。
[Effective Java]第八章 通用程序设计