1. 局部變量作用域最小化
-
問題
將局部變量的作用域最小化,可以增強代碼的可讀性和可維護性,并降低出錯的可能性。那么,常用的將局部變量作用域最小化的方式有哪幾個方面?
-
解決
- 為了避免局部變量擴大作用域,污染到其他作用域。局部變量的作用域應該最小化,即在第一次使用它的地方進行聲明,盡可能在聲明處進行初始化。典型的例子是,使用
for循環(huán),變量作用域在循環(huán)內,而不會擴散。所以,如果循環(huán)終止之后不再需要循環(huán)變量的內容,for循環(huán)就優(yōu)于while循環(huán); - 使方法小而集中。如果把兩個操作合并到同一個方法中,與其中一個操作相關的局部變量就有可能會出現在執(zhí)行另一個操作的代碼范圍之內。為了防止這種情況發(fā)生,只要把這個方法分成兩個,每個方法各自執(zhí)行一個操作;
- 幾乎每個局部變量的聲明都應該包含一個初始化表達式,如果沒有足夠信息來對一個變量進行有意義的初始化,就應該推遲這個聲明,直到可以初始化為止,例外情況是與try..catch有關。
- 為了避免局部變量擴大作用域,污染到其他作用域。局部變量的作用域應該最小化,即在第一次使用它的地方進行聲明,盡可能在聲明處進行初始化。典型的例子是,使用
-
結論
為了方法局部變量域污染到其他的作用域,需要將局部變量域盡可能縮小,這樣能夠增強代碼的可讀性和可維護性。
2. For-Each由于for循環(huán)
-
問題
在Java1.5之前,對集合的遍歷習慣用下面這種方式:
for (Iterator i = c.iterator(); i.hasNext(); ) { doSomething((Element) i.next()); // (No generics before 1.5) }對數組的遍歷,習慣采用下面這種方式:
for (int i = 0; i < a.length; i++) { doSomething(a[i]); }這些習慣用法要好于while循環(huán),但也不完美。代迭器與索引都有些混亂。而且,它們還可能引起錯誤。在上面的循環(huán)中迭代器與索引都出現了三次,其中有兩個地方可能帶來錯誤,如果的確出現了這種錯誤,卻無法保證編譯器能捕獲到這些錯誤。那么,在集合和數組的遍歷中優(yōu)先采用哪種方式?
-
解決
-
在集合和數組的遍歷時,優(yōu)先采用for-each遍歷的方式,比如下面這種示例代碼:
for (Element e : elements) { doSomething(e); } -
for-each循環(huán)的性能要優(yōu)于傳統(tǒng)的for循環(huán),并且能夠減少異常的發(fā)生,for-each循環(huán)不僅可以遍歷集合和數組,還可以遍歷任何實現Iterable接口的對象。同時,使用for-each循環(huán)也有以下局限性:- 過濾:如果需要在遍歷的過程中刪除特定的元素,就需要用顯式的迭代器,調用
remove方法進行刪除; - 轉換:如果在遍歷的過程中,需要轉換部分元素,就需要使用迭代器或者索引,一遍設定特定的元素;
- 平行迭代:如果需要并行的遍歷多個集合,就需要顯式的控制迭代器和索引變量,以便所有的迭代器和索引能夠同步前移。
- 過濾:如果需要在遍歷的過程中刪除特定的元素,就需要用顯式的迭代器,調用
-
-
結論
在大多數情況下都應該使用for-each循環(huán)的方式去遍歷集合和數組,只有在過濾、轉換、平行迭代的時候才需要采用傳統(tǒng)的迭代器和索引的方式進行遍歷。
3. 避免使用float
-
問題
float和double類型主要是為了科學計算與工程計算而設計的,它們并沒有提供完全精確的結果,所以不應該被用于需要精確結果的場合。比如:
System.out.println(1.0-0.42) //輸出 0.580000000000001那么,當需要精確結果的時候,應該怎樣處理?
-
解決
- 在需要精確結果的時候需要使用BigDecimal,BigDecimal所創(chuàng)建的是對象,我們不能使用傳統(tǒng)的+、-、*、/等算術運算符直接對其對象進行數學運算,而必須調用其相對應的方法,方法中的參數也必須是BigDecimal的對象;
-
任何需要精確答案的計算任務,都不要使用float和double,可以使用
int、long或者BigDecimal。使用BigDecimal可以很方便的選擇舍入的方式,它一共提供了8種方式; - 如果數值范圍沒有超過9為十進制數字,使用int;如果超過9位但不超過18位數字,可以使用long;如果有可能超過18位數字,就必須使用BigDecimal。
-
結論
總之,對于任何需要精確答案的計算任務,請不要使用float或者double,如果你想讓系統(tǒng)來記錄十進制小數點,并且不介意因為不使用基本類型帶來的不便,就可以使用BigDecimal。
4. 基本類型優(yōu)于裝箱類型
-
問題
Java有一個數據類型由兩部分組成,包含基本類型(primitive),如int、double和boolean,和引用類型(reference type),如String和List。 每個基本類型都有一個對應的引用類型,稱為裝箱基本類型(boxed primitive)。由于Java自動裝箱和拆箱機制,會使得在實際開發(fā)中基本類型和裝箱類型混用,那么它們之間有什么區(qū)別?
-
解決
-
基本類型和裝箱類型主要有3個主要區(qū)別:
- 基本類型只有值,而裝箱類型具有和它們值不同的統(tǒng)一性,即
new Integer(42)==new Integer(42),雖然這兩個裝箱類型都是表示數字42,但是同一性判斷會返回false; - 基本類型只具有具體功能值,如數值等,而裝箱類型還具有非功能值
null; - 基本類型通常要比裝箱類型更節(jié)省空間和運行時間。
注意:如果基本類型和裝箱類型混合使用,裝箱類型會拆箱為基本類型,這時,如果裝箱類型為null,就會容易報NullPointException。
- 基本類型只有值,而裝箱類型具有和它們值不同的統(tǒng)一性,即
-
什么時候用裝箱類型
- 在使用集合時,鍵、值都只能使用裝箱類型;
- 使用參數化類型時,如ThreadLocal類時,只能使用裝箱類型
-
-
結論
當可以選擇的時候,基本類型要優(yōu)先于裝箱類型?;绢愋透臃奖愫唵危阅芨?。如果沒辦法避免使用裝箱類型時,注意類型間同一性的比較,以及NullPointException。
5. 正確的使用字符串
-
問題
字符串在實際開發(fā)中被高頻使用,同樣也存在著被濫用的情況,那么,字符串不應該使用在哪些情形下呢?
-
解決
字符串不適合代替其他值的類型:當原始類型為int、float等其他類型時,就不要使用字符串替代;
字符串不適合代替枚舉類型:枚舉類型比字符串更適合用來表示枚舉的常量;
字符串不適合代替聚集類型:如果一個實體有多個組件,用字符串來表示這個實體通常不恰當,
String compundKey = className + "#" + i.next(),更好的做法是編寫一個類來描述這個數據集,通常是一個私有的靜態(tài)成員類;-
字符串不適合作為授權鍵:有時候,字符串被用于對某種功能進行授權訪問,考慮設計一個提供線程局部變量的機制,這個機制提供的變量在每個線程中都有自己的值,示例代碼:
public class ThreadLocal { private ThreadLocal() {} public static void set(String key, Object value); public static Object get(String key); }這種方法的問題在于,字符串鍵代表一個共享的全局命名空間,要使這種辦法可行,客戶端提供的字符串必須是唯一的,如果使用了相同的字符串,實際上就共享了這個變量。可以采用以下這種方式進行修正:
public class ThreadLocal { private ThreadLocal() {} public static class Key { Key() {} } public static Key getKey() { return new Key(); } public static void set(Key key, Object value); public static Object get(Key key); }這種采用對象實例作為授權鍵的話,就能夠保證全局唯一。事實上,ThreadLocal也是采用這種方式,將ThreadLocal實例作為了ThreadLocalMap的鍵。
-
結論
總之,如果可以使用更加合適的數據類型,或者可以編寫更加適當的數據類型,就應該避免使用字符串來表示對象。如果使用不當,字符串會比其他類型更加笨拙、速度更慢。字符串經常被錯誤的用來替代基本類型、枚舉類型。
6. 慎用反射機制
-
問題
反射機制提供了通過程序來訪問關于已裝載的類的信息的能力,給定一個Class實例,可以獲得Constructor、Method、Field實例,這些對象提供了類構造器、訪問類成員名稱、域類型、方法簽名等信息。反射機制很強大,但使用起來有哪些注意事項呢?
-
解決
在使用反射機制的時候需要注意如下幾點:
- 喪失了編譯時類型檢查的好處:如果程序企圖用反射訪問不存在的方法時,在運行時將會失?。?/li>
- 執(zhí)行反射訪問的方法代碼很冗長:由于使用反射,會有很多的Exception需要try catch;
- 性能損失:反射方法調用比普通方法調用要慢很多。
-
結論
反射很強大的功能機制不能否認,對于特定復雜系統(tǒng)編程任務,反射機制很有用途,但是他也有很多缺點,對于普通的方法調用,建議不采用反射機制。
7. 謹慎的進行優(yōu)化
-
問題
有三條與優(yōu)化有關的格言是每個人都應該知道的:
1. 很多計算上的過失都被歸咎于效率(沒有必要達到的效率),而不是任何其他的原因,——甚至包括盲目地做傻事。 ——William A.Wulf[Wulf72] 2. 不要去計校效率上的一些小小的得失,在97%的情況下。不成熟的優(yōu)化才是一切問題的根源。 ——Donald E.Knuth[Knuth74] 3. 在優(yōu)化方面,我們應該遵守兩條規(guī)則: 規(guī)則1:不要進行優(yōu)化。 規(guī)則2(僅針對專家):還是不要進行優(yōu)化一一也就是說,在你還沒有絕對清晰的未優(yōu)化方案之前,請不要進行優(yōu)化. ——M.A.Jackson[Jackson75]
在大多數情況下,不成熟的優(yōu)化都會造成更嚴重的問題,在進行性能優(yōu)化的時候應該著重關注于哪些方面?
-
答案
- 不要因為性能而犧牲合理的結構。要努力編寫好的程序而不足快的欄序。如果好的程序不夠快,它的結構將使它可以得到優(yōu)化。好的程序體現了信息隱藏 (information hiding)的原則:只要有可能,它們就會把設計決策集中在單個模塊中,因此,可以改變單個決策,而不會影響到系統(tǒng)的其他部分;
- 努力避免那些限制性能的設計決策。當一個系統(tǒng)設計完成之后,其中最難以更改的組件是那些指定了模塊之間交互關系以及模塊與外界交互關系的組件。在這些設計組件之中,最主要的是API協(xié)議以及永久數據格式。這些設計組件不僅在事后難以甚至不可能改變,而且它們都有可能對系統(tǒng)本該達到的性能產生嚴重的限制;
- 要考慮API設計決策的性能后果。使公有的類型成為可變的(mutable )。這可能會導致大量不必要的保護性拷貝(見第39條:必要時進行保護性拷貝 )。同樣地,在適合使用復合模式的公有類中使用繼承,會把這個類與它的超類永遠地束縛在一起,從而人為地限制了子類的性能
-
結論
總而言之,不要費力去編寫快速的程序——應該努力編寫好的程序,速度自然會隨之而來。在設計系統(tǒng)的時候,特別是在設計API、線路層協(xié)議和永久數據格式的時候,一定要考慮性能的因素。當構建完系統(tǒng)之后,要測量它的性能。如果它足夠快,你的任務就完成了。如果不夠快,則可以在性能剖析器的幫助下,找到問題的根源,然后設法優(yōu)化系統(tǒng)中相關的部分。第一個步驟是檢查所選擇的算法:再多的底層優(yōu)化也無法彌補算法的選擇不當。必要時重復這個過程,在每次改變之后都要測量性能,直到滿意為止。
8. 多使用抽象來進行聲明
-
問題
先來看一個反例:
Vector<User> list= new Vector<User>();大多數情況下,我們都喜歡用具體的類型來聲明變量,這里有一個弊端,如果將來想將Vector換成ArrayList的話,可能會影響其他的代碼。在聲明變量是最佳的實踐是什么?
-
解決
像上例中采用接口的方式聲明變量更加合適,如:
List<User> list= new ArrayList<User>();具有的優(yōu)點:程序更加靈活,如果其他代碼使用的是List接口中的方法,當你想改變具體實現類的時候,比如這里將Vector換成了ArrayList,只需要改變構造器就可以,對其他地方的代碼而言是無感知的,并不會影響其他地方的操作。
- 什么時候應該讓具體類去聲明變量呢?
- 如果沒有合適的接口存在,可以用類來引用對象。例如,考慮值類(String、BigInteger)很少用多個實現編寫,他們通常是final的,并且很少有對應的接口。使用這種值類作為參數、變量、域或者返回值類型就比較合適;
- 頂層類是抽象類的情況, 例如java.util.TimerTask抽象類,應該用相關的基類(往往是抽象類)來引用對象,而不是它的實現類;
- 代碼依賴于具體類的特殊屬性,比如上例中假設程序中需要利用Vector的線程安全的特性,如果采用接口聲明變量的話,無意間將實現類改成了ArrayList就會造成極大的錯誤。
- 什么時候應該讓具體類去聲明變量呢?
-
結論
如果有合適的接口類型存在,那么對于參數、返回值、變量和域來說,都應該使用接口類型進行聲明。這樣做可以讓程序變得更加靈活,如果改變接口的具體實現類,其他代碼都可以繼續(xù)工作。
