通用程序設(shè)計

該篇文章是《Effective Java 2nd》的讀書總結(jié),關(guān)于此書自然不必多言。如果有需要pdf中文&英文的,可以下方留言。據(jù)說現(xiàn)在已經(jīng)有了第三版啦,點擊查看:Effective Java 第三版的相關(guān)介紹

第8章 通用程序設(shè)計

——以下內(nèi)容都是來自于該章節(jié)的總結(jié)


1. 將局部變量的作用域最小化

在第一次使用它的地方聲明,幾乎每個局部變量的聲明都應(yīng)該直接初始化
(如果沒有足夠的信息來對這個局部變量進行有意義的初始化,那么就應(yīng)該推遲這個聲明,直到可以初始化為止。 有例外情況,具體視情況而定

如果在循環(huán)終止之后不再需要變量的內(nèi)容,我們選擇for循環(huán)就優(yōu)先于while循環(huán)。

// 我們使用Iterator遍歷java集合
首選的做法:
for(Iterator i = c.iterator(); i.hasNext() ){
  doSomething((Element)i.next());
}

另外一種對局部變量的作用域進行最小化的循環(huán)做法:

// 此處的countLength()表示的是可能耗時需要計算的操作;
// 這樣寫可以避免每次迭代中執(zhí)行冗余計算的開銷
for(int i = 0 , length = countLength() ; i < lenght ; i ++){
  doSomething(i);
}

對于局部變量,晚聲明(初始化),早結(jié)束,即不可擴大局部變量的作用域


在實際開發(fā)中,對于該條規(guī)則肯定是深有體會的。尤其是我們擅長的Ctrl+C & Ctrl +V,異常就離程序不遠(yuǎn)了。

2. for-each循環(huán)優(yōu)先于傳統(tǒng)的for循環(huán)

前提條件——我們不需要借助于index來執(zhí)行操作

for - each循環(huán)適用于數(shù)組&集合及任何實現(xiàn)Iterator接口的對象,且無毒副作用(無性能損失),IAW,使用它百利而無一害。這一點在多個集合進行嵌套迭代時,它的優(yōu)勢將會更加明顯。

/**
* 假設(shè)2個骰子,有多少種組合
*/
enum Face {ONE,TWO,THREE,FOUR,FIVE,SiX}
Collection<Face> faces = Arrays.asList(Face.values());
//使用傳統(tǒng)的for循環(huán)
for(Iterator<Face> i = faces.iterator(); i.hasNext();){
  for(Iterator<Face> j = faces.iterator(); j.hasNext();){
    Sysout.out.println(i.next() +" , "+j.next());
  // 結(jié)果輸出的是:6個重復(fù)的單詞("ONE,ONE" 到 "SIX,SIX"),而不是我們期望的36種
  }
}

為了fix 上面的bug,必須在外部循環(huán)的作用添加一個變量來保存外部元素,即:

// 此處只貼出關(guān)鍵代碼
  for (Iterator<Face> i = faces.iterator(); i.hasNext(); ) {
            Face nextI = i.next();
            for (Iterator<Face> j = faces.iterator(); j.hasNext(); ) {
                System.out.println(nextI + "," + j.next());
            }
        }

按照傳統(tǒng)的for循環(huán)來做上述簡單功能,我們難道還需如履薄冰嗎?NO

// for- each循環(huán),想你之所想
  for (Face face : faces) {
            for (Face f : faces) {
                System.out.println(face+","+f);
            }
    }

IAW,for-each循環(huán)在簡潔性&預(yù)防Bug方面有著傳統(tǒng)for循環(huán)無可比擬的優(yōu)勢,并且沒有性能損失,So開發(fā)中,盡可能地使用for-each循環(huán)。

補充一句,如果牽涉到Index,我們就必須使用傳統(tǒng)的for循環(huán)啦。


3. 強烈建議使用標(biāo)準(zhǔn)類庫

“不要重復(fù)的造輪子”,能用標(biāo)準(zhǔn)庫實現(xiàn)那就別猶豫,因為我們不一樣。性能,代碼風(fēng)格都有保障。

在java中,每個程序員都應(yīng)該熟悉以下三種類庫:

  1. java.lang
  2. jang.util ( java.util.concurrent 很重要哦)
  3. java.io
    其他的類庫根據(jù)需要進行學(xué)習(xí)。

4. 如果需要精準(zhǔn)的答案,那就扔掉Float & Double

強行裝13一波,可以直接略過

float 和 double 類型主要是為了科學(xué)計算和工程計算而設(shè)計的。它們執(zhí)行二進制浮點運算,這是為了在廣泛的數(shù)值范圍上提供較為精準(zhǔn)的快速近似計算而精心設(shè)計的。然而它們并沒有提供完全精確的結(jié)果,所以不應(yīng)該被用于需要精準(zhǔn)結(jié)果的場合。

  // WTF,這咋不是我想要的呢?
  System.out.println(1.03 - 0.42);//0.6100000000000001
  System.out.println(1.0 - 0.9);//0.09999999999999998
  System.out.println(1.0 - 0.2);// 0.8
  System.out.println(1.0-0.1-0.2-0.3-0.4); // -5.551115123125783E-17

如何解決上述這種問題呢?
正確姿勢是:使用BigDecimal 或者想辦法轉(zhuǎn)化為int,long來計算;

  // 使用 BigDecimal計算
   public static void main(String[] args){
     // 其實BigDecimal構(gòu)造函數(shù)有很多,注意這里的BigDecimal的構(gòu)造函數(shù)里面是字符串
      BigDecimal bigDecimal = new BigDecimal("1.03");
      BigDecimal bigDecimal1 = new BigDecimal("0.42");
      BigDecimal result = bigDecimal.subtract(bigDecimal1);
      System.out.println("1.03 - 0.42 = "+result); // 1.03 - 0.42 = 0.61 perfect 

      BigDecimal bigDecimal2 = BigDecimal.valueOf(1.0);
      BigDecimal bigDecimal3 = BigDecimal.valueOf(0.9);
      BigDecimal subtract = bigDecimal2.subtract(bigDecimal3);
      System.out.println("1.0 - 0.9 = "+ subtract); // 1.0 - 0.9 = 0.1  nice
        
      // 想辦法湊成int 或者long再做
      System.out.println((1.03 * 100 - 0.42 * 100)/100); // 0.61
  }

構(gòu)造 BigDecimal 對象常用以下方法:
BigDecimal BigDecimal(double d); //不允許使用,得不到精確值
BigDecimal BigDecimal(String s); //常用,推薦使用
static BigDecimal valueOf(double d); //常用,推薦使用

其中,

  1. double 參數(shù)的構(gòu)造方法,不允許使用!!!!因為它不能精確的得到相應(yīng)的值;
  2. String 構(gòu)造方法是完全可預(yù)知的: 寫入 new BigDecimal("0.1") 將創(chuàng)建一個 BigDecimal,它正好等于預(yù)期的0.1; 因此,通常建議優(yōu)先使用 String 構(gòu)造方法;
  3. 靜態(tài)方法 valueOf(double val) 內(nèi)部實現(xiàn),仍是將 double 類型轉(zhuǎn)為 String 類型; 這通常是將 double(或float)轉(zhuǎn)化為 BigDecimal 的首選方法;

BigDecimal詳情,值得你收藏。

這里也要注意,BigDecaimal也是有缺點的:

你自己寫也能感覺出來 1. 不夠方便;2. 慢,影響程序性能低
根據(jù)實際情況取舍。

如果性能非常關(guān)鍵,你又不介意十進制的小數(shù)點,且數(shù)值不大,就可以考慮使用int 或者long.
如果數(shù)值范圍沒有超過9位十進制數(shù)值,就可以使用int;
如果數(shù)值范圍沒有超過18位十進制數(shù)值,就可以使用long;
否則,必須使用BigDecimal.


5.基本類型優(yōu)先于裝箱基本類型

Java的類型系統(tǒng)由兩部分組成:
1.基本類型(primitive); byte ,char, int ,long,float ,double , boolean
2.引用類型(reference); String, List...and so on.

裝箱基本類型(boxed primitive)就是 基本類型對應(yīng)的引用類型。
1.Byte
2.Character
3.Integer
4.Long
5.Float
6.Double
7.Boolean

Java 1.5版本引入了自動裝箱(autoboxing)自動拆箱(auto-unboxing)。 該特性模糊了但沒有完全抹去基本類型和裝箱基本類型之間的區(qū)別。我們在開發(fā)中要謹(jǐn)慎選擇。

基本類型和裝箱基本類型的區(qū)別:

1.基本類型只有值,而裝箱基本類型則具有與它們的值不同的同一性(IOW,兩個裝箱基本類型可以具體相同的值和不同的同一性)[這里的同一性: 其實就是對象的內(nèi)存地址];
2.每個裝箱基本類型都有一個值:null;
3.基本類型通常比裝箱基本類型更節(jié)省空間和時間,雖然裝箱基本類型也進行了部分的性能優(yōu)化(可以查看相關(guān)類的valueOf()方法);

// 裝箱基本類型
 Integer i1 = new Integer(1);
 Integer i2 = new Integer(1);
 System.out.println("i1 == i2? " + (i1 == i2)); // false
 System.out.println("i1.equals(i2)? " + (i1.equals(i2))); // true;
// 裝箱基本類型
Integer j1 = 1; //自動裝箱
Integer j2 = 1; //自動裝箱
System.out.println("j1 == j2? " + (j1 == j2)); // true;
System.out.println("j1.equals(j2)? " + (j1.equals(j2))); // true

再看個更神奇的栗子

        Integer i11 = 127;
        Integer i22 = 127;
        System.out.println("i11 == i11? " + (i11 == i22));  // true
        System.out.println("i11.equals(i22)? " + (Objects.equals(i11, i22)));// true
        Integer i33 = 128;
        Integer i44 = 128;
        /**
        * 在通過valueOf方法創(chuàng)建Integer對象的時候,如果數(shù)值在[-128,127]之間,
        * 便返回指向IntegerCache.cache中已經(jīng)存在的對象的引用;否則創(chuàng)建一個  
        * 新的Integer對象。
        */
        System.out.println("i33 == i44? " + (i33 == i44));// false
        System.out.println("i33.equals(i44)? " + (Objects.equals(i33, i44)));// true

        Double d1 = 200.0;
        Double d2 = 200.0;
        System.out.println("d1 == d2? "+(d1 == d2)); // true
        System.out.println("d1.equals(d2)? " + (Objects.equals(d1, d2))); // true

無形裝B,最為致命。

以后看見裝箱基本類型,比較的時候就直接用equals()就安全啦。實在不行,就裝個Alibaba的插件

注意:

重要的事情說3遍:
1.裝箱基本類型比較相等時用equals();
2.裝箱基本類型比較相等時用equals();
3.裝箱基本類型比較相等時用equals();

裝箱基本類型在開發(fā)中還容易引起以下錯誤,這里順便提一下:

public class TestAutoBox{
public static Integer num;
  public static void main(){
    // NullPointerException
      /**
      * 錯誤原因:
      * 當(dāng)使用基本類型和裝箱基本類型進行操作時,一般裝箱基本類型都會自動拆    
      * 箱。但是類似于這種情況,此時裝箱基本類型為null時,就得到這個
      * NullPointerException異常。
      */
    if(num == 42){
      System.out.println("You are so lucky as to Unbelievable");
    }
  }
}

另一種情況:

public class TestBox2{
   // 裝箱基本類型
    private Long sum = 0L;    
    public static void main(){
        for(long i = 0; i < Integer.MAX_VALUE; i++){
          /**
            * 程序編譯運行沒有問題,但是由于sum是裝箱基本類型,
            * 所以 += 操作將進行反復(fù)地裝箱和拆箱,導(dǎo)致明顯的性能下降
          */
            sum += i;
        }
      System.out.println(sum);
    }
}

到這里,你可能有疑惑,So Why 發(fā)明這個裝箱基本類型呢?存在即合理在某些場景下,我們不得不用裝箱基本類型。

使用裝箱基本類型的場景:
1.使用集合(Collections Framework)中;
2.在參數(shù)化類型中;
3.反射的方法調(diào)用中;

總結(jié)

1.包裝基本類型套路太多,盡量避免使用;
2.包裝類型進行操作時,請尊重人家對象的本質(zhì),按對象的規(guī)格接待它(equals(),null);
3.使用基本類型安全可靠;

另外,關(guān)于裝箱基本類型的詳細(xì)內(nèi)容,
可以參考深入理解Java中的包裝類與自動拆裝箱


6.選擇合適的類型,放字符串一條生路

這條規(guī)則其實是一些人在開發(fā)中為了方便而養(yǎng)成的壞習(xí)慣,坦白地講,以前團隊就有成員這么干,我也想不通為什么。


7.注意字符串連接的性能

String 字符串常量
StringBuffer 字符串變量(線程安全)
StringBuilder 字符串變量(非線程安全)
StringBuilder類是Java5.0新增加的,用于代替在非同步情況下的StringBuffer類。

String 類型和 StringBuffer(或者StringBuilder)類型的主要性能區(qū)別在于String是不可變對象。所以在每次對String類型進行改變的時候,其實就等同于生成了一個新的String對象,然后將指針指向新的String對象。到此體驗一把

在字符串連接的使用中,大部分情況下,性能上:

  • StringBuffer > String;
  • StringBuilder > StringBuffer;

需要注意的是:

// 這樣的拼接是沒有任何問題的,因為在JVM'Eyes,它
// 等價于 String string = "Hello,Good Morning Sir";
String string = "Hello" +" Good" + "Morning" +"Sir";

總而言之,經(jīng)常改變內(nèi)容的字符串最好不要用String,在非同步的情況下,直接選擇StringBuilder。因為每次生成對象都會系統(tǒng)性能產(chǎn)生影響。
String,StringBuffer與StringBuilder的區(qū)別
Difference between StringBuilder and StringBuffer

另外,身邊的同事轉(zhuǎn)化字符串時,總是直接(+"")來搞,令我十分反感。在這里懇求大家,在開發(fā)中,一定要優(yōu)雅地來處理String.valueOf(param):

int i = 10;
String s1 = i + "";// 不推薦,產(chǎn)生兩個對象
String s = String.valueOf(i);//推薦

8.通過接口引用對象

一般來講,應(yīng)該優(yōu)先使用接口而非類來引用對象。如果有合適的接口類型存在,那么對于參數(shù),返回值,變量和域來講,都應(yīng)該使用接口類型進行聲明。

ArrayList<Integer>  arrayList = new ArrayList<Integer>(10);//不推薦
正確姿勢:
List<Integer> list = new ArrayList<Integer>(10);// 推薦

Don't ask me WHY? 你想想,那天發(fā)現(xiàn)ArrayList不滿足需求,要換成LinkedList。采用第一種寫法,你會不會Crazy.

如果頂層不是接口,使用基類也是可以的啊


9.謹(jǐn)慎地進行優(yōu)化

關(guān)于優(yōu)化的格言:

1.很多計算上的過失都被歸咎于效率(沒有必要達到的效率),而不是其他的任何原因----甚至包括盲目地做傻事。
2.不要去計較效率上的一些小損失,在97%的情況下,不成熟的優(yōu)化才是一切問題的根源。
3.在優(yōu)化方面,我們應(yīng)該遵守兩條規(guī)則:
*規(guī)則1:不要進行優(yōu)化;
*規(guī)則2:(僅針對專家):還是不要優(yōu)化——That's to say,在你還沒有絕對清晰的未優(yōu)化方案前,請不要優(yōu)化。

要努力編寫好的程序而不是快的程序

過早地優(yōu)化乃萬惡之源

善用性能剖析工具來分析代碼。

IOW,不要費力去編寫快的程序——應(yīng)該努力編寫好的程序,速度自然會隨之而來。


10.遵守大眾的命名規(guī)范

這里只強調(diào)一點,

類型參數(shù):
T:表示任意的類型;
E:表示集合的元素;
K&V:表示映射的鍵和值的類型;
X:表示異常。
任何類型可以是T,U,V或者T1,T2,T3。

我之前的CTO曾對我說過一句話,我感覺很受用:
你的代碼能讓后來看的人不罵你就足夠啦


這里分享一個不錯的在線編輯網(wǎng)站:GeeksforGeeks

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容