以下出現的問題,都是在公司遇到的事件,以類比的方式來描述遇到的狀況。
Java
Unique
1 2 3 4 5 6
| class DbObj { @Override public boolean equals(Object obj) { return this.compareTo((DbObj) obj) == 0; } }
|
對於唯一存在的物件,別用 compareTo()
來實作 equals()
,它們可以直接用 ==
判定。而且大部分的 compareTo
是比較慢的線性實作。如下述的物件
1 2 3 4
| class MyString { @Override public boolean equals(Object obj) {...}; }
|
結果就是變得超慢。
toString
in Comparison
1 2 3 4 5 6
| class CompositeClass implements Comparable<CompositeClass> { @Override public int compareTo(CompositeClass other) { return toString().compareTo(other.toString()); } }
|
千萬別這麼幹,當我們對類別增加成員時,同時修改 toString()
後,運行結果也不同。這也會帶來嚴重的效能問題,試想著排序的時候,產生一大堆的字串用於比較,那麼垃圾回收就會佔有大部分的時間。
toString()
in Key
當需要對 enum
或者其他相關物件進行反序列時,避開 toString()
,因為這個函數很容易被其他人覆寫。
1 2 3 4 5 6 7 8
| enum SomeType { @Override public String toString() { return super.toString() + "..."; } } SomeType.valueOf(type.toString());
|
有人會說這個函數不該被覆寫,但能操作就可能出事。
It is not in C++
1 2 3
| boolean sameLocation(Point a, Point b) { return a == b; }
|
抱歉,您使用的是 Java,並沒有 operator==
的語法,所有的 ==
都是比較相同物件,並不會實作對應的 equals()
。
Numeric Comparison
1 2
| static final Comparator<Point> compareX = (a, b) -> a.getX() - b.getX();
|
這種容易發生 overflow/underflow 的寫法,並不建議模仿 C/C++ 的習慣,請多用內建的函數比較。
this
in Constructor
1 2 3 4 5 6 7 8 9 10
| public Point { Point(long x, long y) { mX = x; mY = y; } Point() { mX = 0; mY = 0; } }
|
請盡量使用建構子,各自獨立容易漏掉一些共同的檢查。
Initialization
1 2 3 4 5 6
| Point copy() { Point pt = new Point(); pt.setX(getX()); pt.setY(getY()); return pt; }
|
能使用建構子完成的事情就盡量使用,分次使用可能會造成過多額外的檢查或調整,效能就會往下掉。
Find the Minimum/Maximum Element
1 2 3 4 5 6 7 8
| Point getMax(List<Point> testList) { Collections.sort(testList); return testList.get(testList.size() - 1); } Point getMin(List<Point> testList) { return testList.stream().sorted().getFirst().orElse(null); }
|
排序很簡單,但也不能亂寫。
排序複雜度大部分為 $O(n \log n)$,事實上找最小值只需要 $O(n)$。當 $n = 10^6$ 時,效率就可能差到 20 倍。
Computed Getter
1 2 3 4 5 6 7 8
| static final Comparator<CPoint> compateLocation = (CPoint ca, CPoint cb) -> { long p0x = ca.getCPt().getX(); long p1x = cb.getCPt().getX(); long p0y = ca.getCPt().getY(); long p1y = cb.getCPt().getY(); ... }
|
請不要這麼寫,排版好看行數很多並不是好藉口,這呼叫了好幾次 getCPt()
,如果牽涉到好幾步複雜運算,整個效率就慢上了好幾倍。
Computed If-Else
1 2 3 4 5
| if (getA() != null && getA().getB() != null && getA().getB().getC() != null) { ... }
|
寫在同一個 if-statement 很方便,卻造成效能嚴重退化。不如蠢一點的寫法,或者透過 Optional<>
來描述回傳型態。
1 2 3 4 5 6 7 8 9
| A a = getA(); if (a != null) { B b = a.getB(); ... } getA().ifPresent(a -> { a.getB().ifPresent(...) });
|
List Getter
1 2 3 4 5 6 7 8
| LinkedList<Long> X; void doSomething(List<Long> X) { for (int i = 0; i < X.size(); i++) { long x = X.get(i); ... } }
|
別鬧了,這會出大事。效能殺手就是你。LinkedList
的 get(index)
可是 $O(n)$ 的。
Immutable Methods
1 2 3 4
| void parse(String elem) { elem.trim(); ... }
|
記得把回傳值接起來,請不要陡增效能問題。
Method Reference
1 2 3 4 5 6 7 8 9 10
| Map<Long, List<Point>> map; void add(Point a) { List<Point> list = map.computeIfAbsent(a.mX, key -> new LinkedList<>()); } static List<Point> prepare(Long key) { return new LinkedList<>(); }
|
盡量使用 method reference,防止 memory leak,也降低 lambda metafactory desugar 的花費。
Boxing
1 2 3 4 5 6 7
| Double getRotate() { double r = ...; return r; } boolean getMirror() {...}; Boolean isMirror = getMirror();
|
既然有預設值也確保不會發生 null
,請不要過度包裝。拆拆裝裝的狀況會造成垃圾過多。
Exception is NOT a Return Value
1 2 3 4 5 6 7 8
| public String getFileExt(File f) { try { String token = parseExt(f); return "." + token; } catch (NullPointerException e) { return null; } }
|
請別接收這麼奇怪的 runtime exception。在 Java 中,exception 很昂貴的,造價就是整個 call stack。
也別這樣追蹤物件的建立,大量物件會造成內存不足。
1 2 3
| class Point { private Throwable trace = new Throwable(); }
|
Lightweight Exit
1 2 3 4 5
| void process(Point p) { List<Long> array = new ArrayList<>(10000); if (p == null) return; }
|
別急著建立一堆用不著垃圾,有可能在後續判斷中直接返回。盡量縮小變數的生命週期,快用到的時候再準備計算資源。
If-Elif-Else
1 2 3 4 5 6 7 8 9
| if (token.equals("a")) { } else if (token.equals("b")) { } if (token.equals("c")) { }
|
別說排版不重要,那一定是沒看過有人不小心漏了什麼。在大部分情況不太會出事,只是多判斷了幾次造成效能退化。
Dead instanceof
1 2 3 4 5 6 7 8 9 10
| public abstract class Shape; public class Poly extends Shape; public class Rect extends Poly; void write(Shape shape) { if (shape instanceof Poly) { } else if (shape instanceof Rect) { } }
|
凡事有先後,請注意繼承關係。
Observer Pattern
1 2
| private List<ObsListener> list = new LinkedList<>(); private Set<ObsListener> list = new HashSet<>();
|
當訂閱者數量不少,且容易進進出出,請不要用線性的 LinkedList
。當你需要嚴格地再現 BUG,就不要使用不穩定順序的 HashSet
,請使用 LinkedHashSet
是一種保守的選擇。
Remove Observer
1 2 3 4 5 6
| class CreateUI extends Dialog { public CreateUI() { db.addListener(mChangeListener); } }
|
別忘記移除掛載到資料庫的觀察者,這樣往往覆覆操作,越來越慢真的不能怪人。
Opposite Behavior
1 2
| assert pathA.endsWith(pathB) == pathB.isEndOf(pathA);
|
英文函數命名上,操作對稱就該對稱。英文不好要先說,邏輯不好也先說一聲,大家可以幫忙的。
Global Garbage
1 2 3 4 5 6 7
| static List<DbObj> tmp; List<DbObj> getXXX() { tmp = new LinkedList<>(); parallel... return tmp; }
|
別擠壓到下一個人的生存空間,要是沒有使用 getXXX
,會看到一堆資料庫物件被卡在全區變數裡頭。
Useless Argument
1 2 3 4
| class Path { boolean startsWith(Path p); boolean startsWith(Path p, Object a); }
|
別因為不想改動原本的,建立一個一模一樣名字的函數,而且第二個參數並沒有使用到,overloading 不是這樣子設計的。
Count
1 2 3 4
| int getPointCount() { return mPoints.parallelStream().count(); }
|
可以覺得慢,但不要總是開平行解決事情。這種計數問題,通常都有相關的方法可以呼叫。如果沒有,請聯絡相關人士。
1 2 3
| String tooltip = ""; for (DbObj t : db.getObjects(XXX.class)) tooltip = tooltip + t.toString();
|
沒人說這樣不好,除了串接消耗 $O(n^2)$,一般也不會把所有東西拉出來。
Convert Set to Array
1 2 3 4 5 6
| List<Point> toArray(Set<Point> set) { List<Point> arr = new ArrayList<>(); for (Point e : set) arr.add(e); return arr; }
|
建構子更方便,別這麼辛苦,效能至少慢了兩倍。
演算法還是有它的極限在的,常數的確不是很重要,但是數量一大的時候,均攤造成額外操作造成的垃圾,在 Java 中會變得相當明顯。
Error Message
1 2 3 4 5 6 7 8 9 10 11
| Integer parseInteger(String x) { String errMsg = x + "is an invalid integer format..."; errMsg = attachCurrentLineMessage(errMsg); try { Integer n = Integer.parseInt(x); return n; } catch (Exception e) { System.err.println(errMsg); return null; } }
|
人家還沒出錯,先別急著準備錯誤訊息。提前準備的字串複雜度比處理的複雜度還高,這絕不允許。
New Option
1 2 3 4 5
| static void import(boolean a); static void import(boolean a, boolean b); static void import(boolean a, boolean b, int c); static void import(boolean a, boolean b, int c, String d); ...
|
別再為了新參數往上疊,請用更容易看出參數意義的 builder 的寫法。
越來越多的參數,造成容易寫錯傳參的順序的悲劇,這時候 BUG