什麼樣的程式碼是好程式碼?
關於什麼是好程式碼,軟體行業爛大街的名詞一大堆,什麼高內聚、低耦合、可複用、可擴充套件、健壯性等等。也有所謂設計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平臺上,對於大資料量的輸入和輸出,BufferedReader和PrintWriter的效能遠高於Scanner和printlin
參考: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年內把程式碼寫好,技術部落格字字推敲,堅持零拷貝和原創
- 寫部落格的意義在於鍛鍊邏輯條理性,加深對知識的系統性理解,鍛鍊文筆,如果恰好又對別人有點幫助,那真是一件令人開心的事
*****************************************************************************************************