Company Ghost Story 公司鬼故事 1

contents

  1. 1. Java
    1. 1.1. Unique
    2. 1.2. toString in Comparison
    3. 1.3. toString() in Key
    4. 1.4. It is not in C++
    5. 1.5. Numeric Comparison
    6. 1.6. this in Constructor
    7. 1.7. Initialization
    8. 1.8. Find the Minimum/Maximum Element
    9. 1.9. Computed Getter
    10. 1.10. Computed If-Else
    11. 1.11. List Getter
    12. 1.12. Immutable Methods
    13. 1.13. Method Reference
    14. 1.14. Boxing
    15. 1.15. Exception is NOT a Return Value
    16. 1.16. Lightweight Exit
    17. 1.17. If-Elif-Else
    18. 1.18. Dead instanceof
    19. 1.19. Observer Pattern
    20. 1.20. Remove Observer
    21. 1.21. Opposite Behavior
    22. 1.22. Global Garbage
    23. 1.23. Useless Argument
    24. 1.24. Count
    25. 1.25. Tooltip
    26. 1.26. Convert Set to Array
    27. 1.27. Error Message
    28. 1.28. New Option

以下出現的問題,都是在公司遇到的事件,以類比的方式來描述遇到的狀況。

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) {...}; // possible O(n)
}

結果就是變得超慢。

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; // ?????, Objects.equals(a, b)
}

抱歉,您使用的是 Java,並沒有 operator== 的語法,所有的 == 都是比較相同物件,並不會實作對應的 equals()

Numeric Comparison

1
2
static final Comparator<Point> compareX =
(a, b) -> a.getX() - b.getX(); // X, Long.compare(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; // X, this(0, 0);
mY = 0;
}
}

請盡量使用建構子,各自獨立容易漏掉一些共同的檢查。

Initialization

1
2
3
4
5
6
Point copy() {
Point pt = new Point();
pt.setX(getX());
pt.setY(getY());
return pt; // new Point(getX(), getY());
}

能使用建構子完成的事情就盡量使用,分次使用可能會造成過多額外的檢查或調整,效能就會往下掉。

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); // Collections.max(testList);
}
Point getMin(List<Point> testList) {
return testList.stream().sorted().getFirst().orElse(null);
// testList.stream().min().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();
... // X, return ca.getCPt().compareTo(cb.getCPt());
}

請不要這麼寫,排版好看行數很多並不是好藉口,這呼叫了好幾次 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);
...
}
}

別鬧了,這會出大事。效能殺手就是你。LinkedListget(index) 可是 $O(n)$ 的。

Immutable Methods

1
2
3
4
void parse(String elem) {
elem.trim(); // ??????, 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<>()); // X, Point::prepare
}
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() { // X, double
double r = ...;
return r;
}
boolean getMirror() {...};
Boolean isMirror = getMirror(); // X, boolean

既然有預設值也確保不會發生 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) {
// ?????, unsearchable
}
}

凡事有先後,請注意繼承關係。

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);
}
// where is your db.removeListener
}

別忘記移除掛載到資料庫的觀察者,這樣往往覆覆操作,越來越慢真的不能怪人。

Opposite Behavior

1
2
assert pathA.endsWith(pathB) ==
pathB.isEndOf(pathA); // fail ??????

英文函數命名上,操作對稱就該對稱。英文不好要先說,邏輯不好也先說一聲,大家可以幫忙的。

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); // new one
}

別因為不想改動原本的,建立一個一模一樣名字的函數,而且第二個參數並沒有使用到,overloading 不是這樣子設計的。

Count

1
2
3
4
int getPointCount() {
return mPoints.parallelStream().count();
// return mPoints.size();
}

可以覺得慢,但不要總是開平行解決事情。這種計數問題,通常都有相關的方法可以呼叫。如果沒有,請聯絡相關人士。

Tooltip

1
2
3
String tooltip = ""; // should StringBuilder
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; // return new ArrayList<>(set);
}

建構子更方便,別這麼辛苦,效能至少慢了兩倍。

演算法還是有它的極限在的,常數的確不是很重要,但是數量一大的時候,均攤造成額外操作造成的垃圾,在 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