1. 程式人生 > >什麼樣的程式碼是好程式碼?

什麼樣的程式碼是好程式碼?

 

關於什麼是好程式碼,軟體行業爛大街的名詞一大堆,什麼高內聚、低耦合、可複用、可擴充套件、健壯性等等。也有所謂設計6原則—SOLID:

即Single Responsibility (單一職責),Open Close(開閉),Liskov Substitution(里氏替換),Interface Segregation(介面隔離),Dependency Inversion(依賴反轉)

詳情可參考: https://www.cnblogs.com/huangenai/p/6219475.html

 

不喜歡這些抽象名詞,我們搞點簡單明瞭的。一匹跑得快(執行速度快),少生病(健壯),可以馱載各類貨物(可擴充套件),容易辨識(容易看懂),病好治(bug好發現),高大英俊的千里汗血馬是也

 

什麼是好程式碼,不好定義,但是關於什麼是程式碼裡的"壞味道",比較容易搞清楚,避免程式碼裡的“壞味道",離好的程式碼就不遠了,壞味道一二三及推薦做法:

  • 程式碼重複
  • 函式太長

如果太長(一般不宜超過200行,但不絕對),你自己都不太容易讀懂,請不要猶豫,拆成小函式吧。筆者剛畢業,參與一個大型複雜的金融軟體,核心業務類,函式1000行算小case,5000多行的不在少數,我的內心是哇涼哇涼的,還好大致邏輯比較清晰

  • 類太大

一般不宜操過1000行,同樣不絕對,jdk原始碼過千行的不少嘛。還是那個大型複雜的金融軟體,核心的幾個Algo C++檔案,2萬到3萬行,我的心在滴血

  • 資料泥團

即很多地方有相同的三四項,兩個類中有相同的欄位、許多函式簽名中有相同的引數。把這些應該捆綁在一起的資料項,弄到一個新的類裡吧。這樣,函式引數列表會變短不少

  • 函式引數列表太長

工作中有7個引數的函式呼叫,搞清楚每個引數的業務含意,和順序有點頭暈。儘管可能有預設函式引數,不小心的時候確實範過錯誤,後面直接引入一個線上bug,緊張

  • 變數名、函式名稱、類名、介面等命名含義不清晰

             圖02 程式設計師最頭疼的事

苦命的天朝程式設計師,還要把中文翻譯為英文,我也很頭大鴨。函式名能讓人望名知義,看名字就知道函式的功能是啥,以至於幾乎不需要多少comments最好

通常DAO層函式的命令規範是:操作+物件+通過+啥,如:updateUserById, insertQuarter,delteteUserByName

  • 太多的if else
  • 在迴圈裡定義大量耗資源的變數

大物件,如果可以放在迴圈外,被共享,推薦這麼搞

  • try 塊程式碼太長

try塊只包住真的可能發生異常的語句,最小原則,同樣因為try包起來的程式碼要有額外開銷

  • 不用的資源未及時清理掉,流及時關閉

如IO控制代碼,資料庫連線,網路連線等。不清理掉,後果很嚴重,你若不信,軟體就死給你看

  • try-finally醜陋,明顯更愛try-with-resources

  1)醜陋的

    static String firstLineOfFile(String path) throws IOException{
        BufferedReader br = new BufferedReader(new FileReader(path));
        try {
            return br.readLine();
        } finally {
            br.close();
        }
    }

  2)漂亮的小姐姐

static String firstLineOfFile(String path) throws IOException{
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }
}

 

  • 迴圈裡字串的拼接不要用”+“

有改過一個OutOfMemery的bug,字串拼接用”+“,產生了一百多萬的字串變數。用Visual VM看程式佔用記憶體空間比較多,數量最大的,通常都是String,所以用StringBuilder的append吧。

用Java VisualVM擷取的一個dump,如下圖:

從中可以看出,字元char和字串String 例項數記憶體大小佔比都比較高。

  •  太巨量的迴圈,看情況用乘除法和移位運算

移位運算吧,速度略微快於乘除法。測試程式碼如下:

        int temp;
        long before = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            temp = 2 * 8;
            temp = 16 / 8;
        }
        long after = System.currentTimeMillis();
        System.out.println(after - before);


        before = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            temp = 2 << 3;
            temp = 16 >> 3;
        }
        after = System.currentTimeMillis();
        System.out.println(after - before);

 

執行結果,分別為{269,279 }、 {258, 317} milliseconds,驚不驚喜,意不意外,乘除法比移位運算更快。看了下stackoverflow,具體得看處理器,現代處理器好多對於乘除已作優化

參看redis rehashing.c hash key計算的程式碼片段,因為hash key的計算會高頻度用到

看下redis-benchmark基準測試的資料,寫Set = 47801/Second,12年的老電腦(Intel i5-2450M, 2.50GHz),速度很可觀,應該是程式碼寫的牛逼加C本身執行效率較高

 

參考: https://stackoverflow.com/questions/6357038/is-multiplication-and-division-using-shift-operators-in-c-actually-faster

  • 避免執行時大量的反射

不知道Java社群為什麼不太關注反射耗時的問題,以前寫C#都會謹慎使用,C#社群有專門的討論

  • 基本型別優於裝箱基本型別

基本型別更快,更省空間。避免不經意引起自動裝箱和拆箱。是否相等的比較,"裝箱基本型別"可能回出錯。下面的程式碼顯示了無意識的裝箱:

    private static long sum() {
        Long sum = 0L;
        for (long i = 0;i <= Integer.MAX_VALUE; i++) {
            sum += i;
        }
        return sum;
    }

我的電腦測出來,執行時間為11906 milliseconds;將"Long sum" 改為" long sum"後,執行時間降低為2027 milliseconds

  • 避免建立不必要的物件

String  s = new String("bikini"),每次執行該語句都會建立一個新的String例項,如果在迴圈或者頻繁呼叫的方法裡,將建立成千上萬多餘的String例項。應改為 String s = "bikini"

 又如有些物件的建立成本比其他物件搞得多,又有地方重複需要此“昂貴的物件",建議快取之然後重用,例如羅馬數字的判斷:

1)醜陋的

    static boolean isRomanNumeral(String s) {
        return s.matches("^(?=[MDCLXVI])M*(C[MD]|D?C{0,3})"
                + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
    }

 

2)漂亮的小姐姐

    public class RomanNumeral {
        public static final Pattern ROMAN = Pattern.compile("^(?=[MDCLXVI])M*(C[MD]|D?C{0,3})"
                + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");

        static boolean isRomanNumeral(String s) {
            return ROMAN.matcher(s).matches();
        }
    }

 

  • 未作引數有效性檢查

不搞這個,NullPointerException等妥妥地

  • 延遲初始化和懶載入

這個的確是一種優化,即需要用到它的值時,才初始化。如果永不用到,就永遠不會被初始化。但要 

  • LinkedHashMap、HashMap、ArrayList、HashSet、HashTable等集合類,沒有初始化容量

如果大致知道業務場景下這些集合類的數量,初始化吧。如ArrayList預設 DEFAULT_CAPACITY = 10,resize程式碼如下:

newCapacity = oldCapacity + (oldCapacity >> 1);

如最終存放100個數據,則最後的容量 = ((10 + 10 * 2) * 2 + 30)) * 2 + 90 = 270, 會有4次重新分配記憶體和拷貝,費時間啊,我也懶,想耍啊
  • 方法和類如果確實有業務場景需求不會被覆蓋、不會被繼承,用final修飾吧

final method在某些處理器下得到優化,跑得更快

參考: https://stackoverflow.com/questions/5547663/java-final-method-what-does-it-promise

  • 合理資料庫連線池和執行緒池

一個減少資料庫連線的建立和斷開(耗時),一個減少執行緒的建立和銷燬,動態根據請求分配資源,提高資源利用率

  • 多用buffer等緩衝提高輸入輸出IO效率及FileChannel.transferTo、FileChannel.transferFrom和FileChannnel.map

   1)   諸如 BufferedReader 、BufferedWriter、BufferedInputStream和BufferedOutputStream等

         在杭電ACM online judge平臺上,對於大資料量的輸入和輸出,BufferedReaderPrintWriter的效能遠高於Scannerprintlin

       參考:http://acm.hdu.edu.cn/faq.php?topic=java

   2)   FileChannel.transferXXX減少資料從核心到使用者空間的複製,資料直接在核心空間中移動

    FileChannel.map按照檔案的一定大小塊對映為記憶體區域,也不用從核心空間向用戶空間拷貝資料 ,只適用於大檔案的讀操作

  • synchronized修飾符最小作用域

synchronized要耗費效能,因此synchronized程式碼塊優於synchronized方法,最小原則

  • enum代替int列舉模式

int列舉模式不具有型別安全性,也沒有啥子描述性,比較也會出問題

1)醜陋的

    public static final int APPLE_FRUIT         = 0;
    public static final int APPLE_PIPPIN        = 1;
    public static final int APPLE_GRANNY_SMITH  = 2;

    public static final int ORANGE_NAVEL         = 0;
    public static final int ORANGE_TEMPLE        = 1;
    public static final int ORANGE_BLOOD         = 2;

 

2)漂亮的小姐姐

    public enum Apple { FRUIT, PIPPIN, GRANNY_SMITH }
    public enum Orange { NAVEL, TEMPLE, BLOOD }

 

  • 合理使用靜態工廠方法代替構造器

如Boolean基本類

    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }

 

靜態工廠方法,不必在每次呼叫時都建立一個新的物件;而且相較於構造器,它有名稱,便於閱讀和理解;同時可以返回原型別的任意子型別;也可以根據引數不同,返回不同的類物件,如EnumSet

    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }

 

  • 組合優於繼承

因為繼承打破了封裝性,overriding可能導致安全漏洞

  • 異常只能用於處理錯誤,不能用來控制業務流程

 

未完待續,困了

 

注:

參考《Effective java》《重構 —— 改善既有程式碼的設計》《深入分析JAVA web技術內幕》

 

*****************************************************************************************************

精力有限,想法太多,專注做好一件事就行

  • 我只是一個程式猿。5年內把程式碼寫好,技術部落格字字推敲,堅持零拷貝和原創
  • 寫部落格的意義在於鍛鍊邏輯條理性,加深對知識的系統性理解,鍛鍊文筆,如果恰好又對別人有點幫助,那真是一件令人開心的事

*****************************************************************************************************