關于什么是好代碼,軟體行業爛大街的名詞一大堆,什么高內聚、低耦合、可復用、可擴展、健壯性等等(作者【CoderBaby】),也有所謂設計6原則 — 迪米特法則(最少知道原則) + SOLID :
即Single Responsibility (單一職責),Open Close(開閉),Liskov Substitution(里氏替換),Interface Segregation(介面隔離),Dependency Inversion(依賴反轉)
詳情可參考: https://www.cnblogs.com/huangenai/p/6219475.html
不喜歡這些抽象名詞,我們搞點簡單明了的,一匹跑得快(運行速度快),少生病(健壯),可以馱載各類貨物(可擴展),容易辨識(容易看懂),病好治(bug好發現),高大英俊的千里汗血馬是也

什么是好代碼,不好定義,但是關于什么是代碼里的"壞味道",比較容易搞清楚,避免代碼里的“壞味道",離好的代碼就不遠了,壞味道一二三及推薦做法:
轉載請注明出處: https://www.cnblogs.com/NaughtyCat/p/what-is-good-codes.html
- 代碼重復
- 函式太長
如果太長(一般不宜超過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 原始碼(5.05版本)之 “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#社區有專門討論反射優化,關于反射的不好的地方:
1) 編譯時沒法檢查了
2)反射的代碼冗長和丑陋
3)性能損耗
推薦做法:用反射的方式創建實體,然后通過介面或者其超類在來訪問這些實體
- 基本型別優于裝箱基本型別
基本型別更快,更省空間,避免不經意引起自動裝箱和拆箱,是否相等的比較,"裝箱基本型別"可能會出錯,下面的代碼顯示了無意識的裝箱:
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(); } }
- 未作引數有效性檢查
不搞這個,ArrayIndexOutOfBoundsException、NullPointerException等妥妥地,是否為空的檢查推薦JAVA8的Optional
- 延遲初始化和懶加載
這個的確是一種優化,即需要用到它的值時,才初始化,如果永遠不用到,就永遠不會被初始化,但要慎用,只有在初始化這個資料域開銷很大的時候才用,在大多數情況下,
正常的初始化要優于延遲初始化,
1)如果出于性能的考慮對靜態域使用延遲初始化,就需要使用 lazy initialization holder class 模式,示例代碼如下:
private static class FieldHodler { static final FieldType field = computeFieldValue(); } private static FieldType getField() { return FileHodler.field; }
2)如果出于性能的考慮對實體域使用延遲初始化,就需要使用雙重檢查模式(double check idiom) 模式,示例代碼如下:
private volatile FieldType field; private FieldType getField() { FieldType result = field; if (result == null) { synchronized (this) { if (field == null) field = result = computeFieldValue(); } } return result; }
- 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和println
參考: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可能導致安全漏洞
- 例外只能用于處理錯誤,不能用來控制業務流程
- 精準的運算,如貨幣運算等不要用float 和 double
正確的做法,用BigDecimal、int和long
- ArrayList對于“隨機訪問較多的場景”性能較高,LinkedListd對于“洗掉和插入較多的場景”性能更高
- 使用范圍最小的資料型別,redis原始碼里大量使用unsigned int 和 unsigned long,時間和空間效率高于int 和 long
部分原始碼截圖如下:

- 將區域變數最小化
推薦在第一次使用區域變數的地方宣告它,不然隔太遠,容易分散注意力,閱讀代碼的人忘記它的型別和初始值了,需要再去找
幾乎每個區域變數的宣告都應該包含一個初始化運算式
- 并發的資料結構可以降低高并發下的CPU時間,但要評估記憶體消耗
并發的資料結構如ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteHashSet等,可以在讀和寫的時候,不用加鎖,因而提高了高并發下的處理效率,但是其復雜的資料結構和鎖優化,代碼了額外的記憶體消耗
未完待續,困了
注:
參考《Effective java》《重構 —— 改善既有代碼的設計》《深入分析JAVA web技術內幕》
本文著作權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利,
*********************************************************************************
精力有限,想法太多,專注做好一件事就行
- 我只是一個程式猿,5年內把代碼寫好,技術博客字字推敲,堅持零拷貝和原創
- 寫博客的意義在于鍛煉邏輯條理性,加深對知識的系統性理解,鍛煉文筆,如果恰好又對別人有點幫助,那真是一件令人開心的事
*********************************************************************************

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/48403.html
標籤:架構設計
