Company Ghost Story 公司鬼故事 9

Java

BufferWriter/StringBuilder

1
2
3
4
5
private void writeLong(long v) {
StringBuilder sb = new StringBuilder(8192); // ?
sb.append(Long.toString(v));
writer.write(sb.toString());
}

每一次轉換,別浪費額外的空間 8192 的 StringBuilder。如果真的需要這種 IO 的緩衝區,可以直接宣告在 BufferWriter 給定預設大小的緩衝區。

BufferWriter/StringBuilder

1
2
3
private String writeTwo(String token0, String token1) {
writer.write(token0 + token1); // why not write separately without intermediate result.
}

既然都知道了字串串接會有中間結果,我們可以直接分開寫 writer.write(token0);writer.write(token1); 來達到相同效果。

StringBuilder Helper

1
2
3
4
5
6
7
8
9
10
private static StringBuilder append(StringBuilder sb, String... tokens) {
for (String t : tokens)
sb.append(t);
return sb;
}
append(sb, "a", "b", "c").append("d", "e");
// if performance it critical, please write all statements without sugar syntax
append(ab, "a" + "b", "c");
// ... why?

這種可變參數的寫法會額外產生一個陣列,對於效能來說會很傷。

Math Getter/Setter

1
2
3
4
5
// convert x' = a x + b
private void convert(Point p) {
p.setX(p.getX() * a);
p.setX(p.getX() + b); // why not p.setX(p.getX() * a + b);
}

別中毒於 Java 或者是 OOP 的寫法。

Lookup by Key

1
2
3
4
5
6
7
8
9
10
public T getSelected() {
for (T c : candidates) {
if (Objects.equals(c.toKey(), mSelected))
return c;
}
return null;
}
public void setSelected(T s) {
mSelected = s.toKey(); // why not store `s` directly?
}

在大多數的狀況下都可以直接存原本的物件。除非是在一些特定的序列化,或者是橫跨兩個不同 domain 的交互資料,這時候我們必須採用字串作為中介對象。

Default Value by Wrapper Class

1
2
3
4
5
6
7
8
9
10
11
private Boolean mirror = false; // why use wrapper class here?
public void setMirror(boolean m) {
mirror = m;
}
private void process() {
if (mirror != null) { // why?
// must do here
}
}

多餘的寫法。如果 wrapper class 有預設值,而且還不能被設置 null。那必然有些地方出錯。

Constant Value by Wrapper Class

1
2
3
4
5
private static final Double SIZE = 50L; // ...
public void exec() {
long v = SIZE.longValue(); // ...
}

這傢伙到底在宣告什麼?想用什麼?

Read More +

Java JNI Thread Error (EINVAL)

Keywords: JNI, Java, ProcessBuilder, Thread Error

Error message: [os,thread] Failed to start thread - pthread_create failed (EINVAL) for attributes: stacksize: 136k, guardsize: 0k, detached.

Solution: -Djdk.lang.processReaperUseDefaultStackSize=true

Description

From JNI process, it is fine to execute Java internal multi-threading, but can not use Runetime.exec or ProcessBuilder to do anything. Even through adjust java stack-related option

1
setenv _JAVA_OPTIONS "-ea -Xlog -XX:ThreadStackSize=31744 -XX:VMThreadStackSize=1024 -XX:CompilerThreadStackSize=1024"

cgroup (Linux control group) limition, and set_rlimit. Same error message will be displayed.

When you link large C/C++ program with large thread local storage (TLS), default minimum stacksize computation is not enough to create pthread in OS-level. Only to set -Djdk.lang.processReaperUseDefaultStackSize=true to apply default pthread arguments. Then, it will be working for you.

問題描述

無法透過 JNI 啟動的 JVM 運行外部程序,如 Runtime.exec 或者是 Process 都無法使用,卻可以使用內部執行緒,甚至達到了數百個執行緒。

首先,JVM 內部拆分了好幾種不同的執行緒使用策略,也有不同的資源需求判斷,去挖原始碼的時候會追到底層的 C 部分,就可以看出這些問題。但是錯誤訊息仍然不足,到了 JDK15 後,才能透過 -Xlog dump 更多的配置訊息,但有些配置訊息的翻譯仍然有錯,還是得看原始碼去得知實際情況。

追了好幾天,調適了許多不同的 stack 配置都沒有起色。自己寫的小短碼 JNI 都沒有事情,架在產品上就突然運行失敗。因此,只能去找有沒有現有的產品開發或使用上發生類似的問題,不該去找任何的教學文件。最後挖到幾篇安裝不同版本的產品失敗,據說更改 JDK 版本就能解決,然後有人分析說因為 TLS 的大小變動才導致新版本突然無法工作,才明白果然是 JNI 上的問題。

實作 JNI 介面的時候,若連接的 C++ 程式中存在大量的 thread_local 變數時,那麼在每一個執行緒中都必須給予相對應的大小進行初始化。而對於 JVM 內部會透過一些預設 stack 配置進行最小化,透過 JNI 連接的時候,便會受到影響。

透過 -Djdk.lang.processReaperUseDefaultStackSize=true 取消最小化 stack 配置,直接使用預設的構造行為,這樣就能暫時解決 JNI 啟動的 JVM 無法運行外部程序的問題。很明顯地,每一條執行緒都會開相當大的 stack size,大部分的情況下預設最多為 4 MB,而實際工作的產品中預設就要 20 MB,這個得從其關聯函數庫去分析為什麼需要這麼大的堆疊,可能是寫錯,也或者是連結太多沒有必要的庫進來。

Read More +

Company Ghost Story 公司鬼故事 8

Java

Format String

1
2
String.format("ABC%s", "D"); // const string should be written directly
String.format("%s.%s", A.class.getName(), "C");

對於可以直接計算得到的,應該直接展開。

Math

1
2
3
4
5
6
7
8
9
10
11
12
13
14
double theta = dx != 0 ? Math.atan(dy/dx) : (dy < 0 ? ... : ...);
// or
if (dx < 0) {
if (dy < 0)
...
} else (dx == 0) {
if (dy < 0)
...
}
// why not be simple?
double theta = Math.atan2(dy, dx);

數學很重要,可以減少很多分支判斷。

Variable Name

1
2
List<T> set = new LinkedList<>(); // what does `set` means?
// perhaps, I need to ask what does `list` means for you.
1
2
TextField okBtn; // why name button for text field?
enableCancelButton(okBtn); // are you sure about it?

命名盡量貼切型態,不然有時候真的會有很奇怪。

Initialize Member

1
2
3
4
5
6
7
8
9
10
11
12
class MPanel {
private JButton mButton;
public MPanel() { // consturctor
...
mButton = new JButton("A"); // what?
panel.add(mButton);
mButton = new JButton("B"); // what?
panel.add(mButton);
// finally, which result you want to express mButton?
}
}

在建構子中,每一個成員變數盡量只存放一次,採用 final 對每個成員變數檢查只初始化一次。

Lambda with Final

1
2
3
4
5
6
7
void process(...) {
String s = "default";
if (isA())
s = "A";
final String _s = s; // shut up, don't explain
blackhole(() -> map(_s));
}

Lambda 函數抓取的時候都要抓取可推斷出 final 的變數。為了 lambda 而額外宣告一個變數作為 final 的話,應該要額外宣告一個 getter 函數去抓取,不然觀感上會覺得代碼有點多餘。

Method Name

1
2
3
4
void increment() { // why is noun?
i++;
i++; // why call it twice?
}

函數盡量使用動詞開頭,名詞有點奇怪。

Context

1
2
3
4
5
6
7
void enableA() {
a = true;
b = true; // why is b here?
}
void disableA() {
a = false; // wait ... where is B?
}

當名稱對應到成員變數時,就應該一件事情對應一個操作。

Read More +

Company Ghost Story 公司鬼故事 7

Java

try-catch-exception

1
2
3
4
5
6
7
8
9
10
11
Object getSomething() {
final Exception abort = new RuntimeException();
try {
// ...
} catch (Exception e) {
if (e == abort)
// fine
else
e.printStackTrace();
}
}

每次呼叫這個函數都要先建立一個例外,然後看裡面會不會丟出來,這個其實很浪費空間和時間建立。可以考慮建立一個特殊 AbortException 來取代預先建立的例外,或者建立一個全局的靜態例外物件來使用。

Double Brace Initialization

1
2
3
4
5
6
7
8
9
10
11
12
final Set<String> set = new HashSet<> {
{
add("A");
add("B");
add("C");
} // no more
};
// why not?
set.add("A");
set.add("B");
set.add("C");

這會額外建立匿名類別。如果宣告在一般的成員方法中,還會捕抓成員變數,造成內存洩漏而無法回收的狀況。

No More Inheritance

1
2
3
4
5
6
7
8
9
10
11
12
13
class Point extends Pair<Integer, Integer> {
Point(int x, int y) {
super(x, y);
}
@Override
public boolean equals(Object o) {
if (o instanceof Point) {
Point p = (Point) o;
return first == p.first && second == p.second; // wrong
}
return false;
}
}

別忘了,Java 中的 wrapper class 可是一個物件,光靠 == 是不能比較相同與否。雖然對於數值小於某個定值的時候,他們會在常數內存池中取得,讓你覺得一切都好像沒事。

Read More +

ADA 2020 Fall P3. ADA Party

Algorithm Design and Analysis (NTU CSIE, Fall 2020)

Problem

$N$ 個堆,每個堆有 $a_i$ 個糖果,現在邀請 $K$ 個人,現在問有多少種挑選區間的方法,滿足扣掉最大堆和最小堆後,區間內的糖果總數可以被 $K$ 整除。

Sample Input

1
2
10 2
6 9 3 4 5 6 1 7 8 3

Sample Output

1
25

Solution

由於沒辦法參與課程,就測測自己產的測試資料,正確性有待確認。

分治處理可行解的組合,每一次剖半計算,統計跨區間的答案個數。

討論項目分別為

  • 最大值、最小值嚴格都在左側
  • 最大值、最小值嚴格都在右側
  • 最大值在左側、最小值在右側
  • 最大值在右側、最小值在左側

最後兩項會有交集部分,則扣除 在左側的最大最小值接等於右側的最大最小值。對於每一項回答,搭配單調運行的滑動窗口解決。

時間複雜度 $\mathcal{O}(n \log n)$、空間複雜度 $\mathcal{O}(n)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#include <bits/stdc++.h>
using namespace std;
// Algorithm Design and Analysis (NTU CSIE, Fall 2020)
// Problem 3. ADA Party
const int MAXN = 500005;
const int32_t MIN = LONG_MIN;
const int32_t MAX = LONG_MAX;
int32_t a[MAXN];
int32_t lsum[MAXN], rsum[MAXN];
int32_t lmin[MAXN], lmax[MAXN];
int32_t rmin[MAXN], rmax[MAXN];
int cases = 0;
int mark[MAXN];
int counter[MAXN];
int n, k;
void inc(int32_t val) {
val = (val%k+k)%k;
if (mark[val] != cases)
mark[val] = cases, counter[val] = 0;
counter[val]++;
}
void dec(int32_t val) {
val = (val%k+k)%k;
if (mark[val] != cases)
mark[val] = cases, counter[val] = 0;
counter[val]--;
}
int get(int32_t val) {
val = (val%k+k)%k;
if (mark[val] != cases)
mark[val] = cases, counter[val] = 0;
return counter[val];
}
int64_t common(int l, int m, int r) {
int64_t ret = 0;
cases++; // max and min is same on both end
for (int i = m, j = m+1, jl = m+1; i >= l; i--) {
while (j <= r && (rmax[j] <= lmax[i] && rmin[j] >= lmin[i])) {
inc(rsum[j]-rmin[j]);
j++;
}
while (jl < j && (rmin[jl] > lmin[i] || rmax[jl] < lmax[i])) {
dec(rsum[jl]-rmin[jl]);
jl++;
}
if (j > m+1 && lmin[i] == rmin[j-1] && lmax[i] == rmax[j-1])
ret += get(k-(lsum[i]-lmax[i]));
}
return ret;
}
int64_t divide(int l, int r) {
if (l >= r)
return 0;
int m = (l+r)/2;
int32_t sum = 0;
int32_t mn = MAX, mx = MIN;
for (int i = m; i >= l; i--) {
sum += a[i], mn = min(mn, a[i]), mx = max(mx, a[i]);
if (sum >= k) sum %= k;
lsum[i] = sum, lmin[i] = mn, lmax[i] = mx;
}
sum = 0, mn = MAX, mx = MIN;
for (int i = m+1; i <= r; i++) {
sum += a[i], mn = min(mn, a[i]), mx = max(mx, a[i]);
if (sum >= k) sum %= k;
rsum[i] = sum, rmin[i] = mn, rmax[i] = mx;
}
int64_t c1 = 0, c2 = 0, c3 = 0, c4 = 0;
cases++; // min max on the left
for (int i = m, j = m+1; i >= l; i--) {
while (j <= r && lmin[i] < a[j] && a[j] < lmax[i]) {
inc(rsum[j]);
j++;
}
if (i < m)
c1 += get(k-(lsum[i]-lmin[i]-lmax[i]));
}
cases++; // min max on the right
for (int i = m+1, j = m; i <= r; i++) {
while (j >= l && rmin[i] < a[j] && a[j] < rmax[i]) {
inc(lsum[j]);
j--;
}
if (i > m+1)
c2 += get(k-(rsum[i]-rmin[i]-rmax[i]));
}
cases++; // min on the left, max on the right
for (int i = m, j = m+1, jl = m+1; i >= l; i--) {
while (j <= r && rmin[j] >= lmin[i]) {
inc(rsum[j]-rmax[j]);
j++;
}
while (jl < j && rmax[jl] < lmax[i]) {
dec(rsum[jl]-rmax[jl]);
jl++;
}
c3 += get(k-(lsum[i]-lmin[i]));
}
cases++; // min on the right, max on the left
for (int i = m+1, j = m, jl = m; i <= r; i++) {
while (j >= l && lmin[j] >= rmin[i]) {
inc(lsum[j]-lmax[j]);
j--;
}
while (jl > j && lmax[jl] < rmax[i]) {
dec(lsum[jl]-lmax[jl]);
jl--;
}
c4 += get(k-(rsum[i]-rmin[i]));
}
int64_t local = c1 + c2 + c3 + c4 - common(l, m, r);
return local + divide(l, m) + divide(m+1, r);
}
int main() {
while (scanf("%d %d", &n, &k) == 2) {
for (int i = 0; i < n; i++)
scanf("%d", &a[i]);
memset(counter, 0, sizeof(counter[0])*k);
int64_t ret = divide(0, n-1);
printf("%lld\n", ret);
}
return 0;
}
Read More +

ADA 2020 Fall P2. Bomb Game

Algorithm Design and Analysis (NTU CSIE, Fall 2020)

Problem

有數名玩家依序抵達遊戲,並且落在位置 $c_i$,並且具有防禦力 $d_i$,過程中會有炸彈發生於 $[l_i, r_i]$,對防禦力小於等於 $p_i$ 造成 $k_i$ 點傷害。

回報遊戲最後每一名玩家所受的傷害總額。

Sample Input

1
2
3
4
5
6
7
8
9
10
11
10 10
P 3 5
A 2 8 15 5
P 7 10
A 4 10 5 3
A 1 9 10 7
P 6 20
P 5 1
A 4 9 17 2
A 1 2 20 4
P 9 5

Sample Output

1
2
3
4
5
12
9
0
2
0

Solution

由於沒辦法參與課程,就測測自己產的測試資料,正確性有待確認。

如果這一題目強制在線對答,則需要一個樹套樹在 $\mathcal{O}(\log^2 n)$ 內回答每一個結果,需要一個動態開區間的實作方法。

如果採用離線處理,則可以透過逆序處理來回答,可以透過二維空間的 BIT 結構來完成,這時候空間上會是另一個問題,即使使用懶惰標記,預期可能會達到 $\mathcal{O}(C \; D)$,通常是不允許的。

從分治算法切入,預想防禦能力高影響不受到攻擊力低的炸彈影響,無論時間與否都不受到影響。接下來,對防禦能力和攻擊力統稱力量。在分治的時候,對力量低的累計出答案,在合併階段受時間順序的影響才能回答。最後:

  1. 對力量從小到大排序
  2. 分治算法
    1. 對左區間和右區間按照時間由大到小排序
    2. 對於每一個左區間的詢問,插入所有滿足的右區間

時間複雜度 $\mathcal{O}(n \log^2 n)$、空間複雜度 $\mathcal{O}(n)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#include <bits/stdc++.h>
using namespace std;
// Algorithm Design and Analysis (NTU CSIE, Fall 2020)
// Problem 2. Bomb Game
const int MAXN = 200005;
struct BIT {
int64_t a[MAXN];
int l[MAXN];
int cases = 0;
void add(int x, int val, int n) {
while (x <= n) {
if (l[x] != cases)
l[x] = cases, a[x] = 0;
a[x] += val, x += x&(-x);
}
}
int64_t sum(int x) {
int64_t ret = 0;
while (x) {
if (l[x] != cases)
l[x] = cases, a[x] = 0;
ret += a[x], x -= x&(-x);
}
return ret;
}
void reset(int n) {
cases++;
}
void add(int l, int r, int k, int n) {
add(l, k, n);
add(r+1, -k, n);
}
} bit;
int n;
struct PEvent {
int c, d, i;
};
struct AEvent {
int l, r, p, k;
};
struct Event {
int type; // 'P' 0 or 'A' 1
int time; // input order
union {
PEvent p;
AEvent a;
} data;
int power() {
if (type == 0)
return data.p.d;
else
return data.a.p;
}
void println() {
if (type == 0)
printf("P %d %d\n", data.p.c, data.p.d);
else
printf("A %d %d %d %d\n", data.a.l, data.a.r, data.a.p, data.a.k);
}
} events[MAXN];
static bool cmp_p(Event &a, Event &b) {
int pa = a.power();
int pb = b.power();
if (pa != pb)
return pa < pb;
return a.time < b.time;
}
static bool cmp_t(Event &a, Event &b) {
return a.time > b.time;
}
int ret[MAXN];
void resolve(int l, int m, int r) {
sort(events+l, events+m+1, cmp_t);
sort(events+m+1, events+r+1, cmp_t);
// printf("resolve %d %d =========\n", l, r);
// for (int i = l; i <= m; i++)
// events[i].println();
// puts("---");
// for (int i = m+1; i <= r; i++)
// events[i].println();
bit.reset(n);
int j = m+1;
for (int i = l; i <= m; i++) {
if (events[i].type)
continue;
int qtime = events[i].time;
while (j <= r && events[j].time > qtime) {
if (events[j].type) {
bit.add(events[j].data.a.l,
events[j].data.a.r,
events[j].data.a.k,
n);
// printf("add %d %d %d %d\n", events[j].data.a.l,
// events[j].data.a.r,
// events[j].data.a.p,
// events[j].data.a.k);
}
j++;
}
// printf("%d --- %d\n", events[i].data.p.i, bit.sum(events[i].data.p.c));
ret[events[i].data.p.i] += bit.sum(events[i].data.p.c);
}
}
void divide(int l, int r) {
if (l >= r)
return;
int m = (l+r)/2;
divide(l, m);
divide(m+1, r);
resolve(l, m, r);
}
int main() {
int m;
char s[2];
scanf("%d %d", &n, &m);
int id = 0;
for (int i = 0; i < m; i++) {
scanf("%s", s);
events[i].time = i;
if (s[0] == 'P') {
events[i].type = 0;
events[i].data.p.i = id++;
scanf("%d %d",
&events[i].data.p.c,
&events[i].data.p.d);
} else {
events[i].type = 1;
scanf("%d %d %d %d",
&events[i].data.a.l,
&events[i].data.a.r,
&events[i].data.a.p,
&events[i].data.a.k);
}
}
sort(events, events+m, cmp_p);
divide(0, m-1);
for (int i = 0; i < id; i++)
printf("%d\n", ret[i]);
return 0;
}
Read More +

資訊科學的偏科生 面試者篇

前言

從實習一年、正式工作三年,從一般的軟體工程職位軟體工程師 II (Software Enginner II) 開始爬,一年後升到資深工程師 (Senior Software Engineer),第三年升到首席工程師 (Principal Software Engineer),依照正常流程,這樣的躍遷是快的。

在薪水上可能會有點失望,必須了解到能力好與不好之間並沒有太大的差別,也就是說薪水不會有 30% 的差異,同時職位之間的差別也不到 20%。但工作量卻顯著地增加,這社會相當奇怪,這也難怪很多高科技的高手最後反而會想離開大公司的體制,自行創業或者轉行都是會發生的狀況。

從成功嶺出獄的第一天上班,頂著小平頭也參與面試流程,從那開始三年間面試了不少人,也與一些身歷百戰的朋友聊了一下狀況,而我卻沒站在白板上參與真的面試,大部分都是工作直接來找,看到需要繁瑣面試的公司都想直接忽略,畢竟身為一個偏科生,肯定是會被刷掉的,要找通才去找血統純正的學歷吧。

面試狀況

電話面試

最近受到疫情影響,會盡量先以 電話面試 為第一關,通常會從簡單的問答看出端倪,如基礎的時間複雜度分析、程式架構、開發經歷上著手提問。有時會配上線上共享文件撰寫簡單的程式,這對刷 leetcode 準備面試的人都不算難。

通常用解說定義為主,或者可以分享一些使用經驗,對於模糊不清的項目直接表達說知道但是不熟悉,一旦描述錯誤或者是甚至在使用完全錯誤的方向,就會凸顯沒有仔細了解,在錯誤理解下又有大量的錯誤衍生,下場就會非常慘。

雷點如下:

  • 不知道二元樹的定義 (到底是怎麼碩班畢業的?)
  • 不知道遞迴的實際流程 (到底是怎麼大學畢業的?)
  • 要求指定語言的工作條件,卻不知物件導向 (可以不滿足所有應徵條件,但請理解每一項大概是什麼內容再來。)

自我介紹

通過第一階段後,很多人就會來到這個環節,通常要準備三十分鐘自我介紹,我比較喜歡稱作武力展示,但不是像相親那樣的自我介紹,如學歷、出生年月、興趣嗜好都不需要。如果早已遞交履歷,那些訊息不需要提第二次,這些口頭描述就像流水帳,聽起來也很無趣。

對於碩博剛畢業的人而言,大部分都直接拿論文出來講,對於程式能力比較吃重的職位,偏好程度很低,實在是不想當論文口委,畢竟研究領域差太遠,也不能驗證對錯與否。如果用口述方式表達,專有名詞與公式都也無法理解是否具有該實力。

從以前讀過碩班的經驗,很多碩士都是老師放水過關的,也有可能完全不需要寫程式的經驗也能畢業。對於那些跨科系的碩士及學碩學歷差異大的,都需配上其他的實務經驗,才能讓面試官比較有信心。

履歷部分請參考相關文章

在過程中的確有幾個實際發生的項目

  1. 博士學位的面試者容易遇到本公司可能無法讓你發展所才,請另尋他路的勸導
    • 原本想要的,但實在是太高學歷了。
  2. 以 Microsoft Office Word/Excel 出現在技巧描述中
    • 因為大部分的工作工作都是之後再學,像這種被 GUI 固化的工具,很難拓展。
  3. 只有在課堂上寫過程式,完全沒有課堂以外的程式項目或參與專案
    • 課堂以內是可接受的,但需要體現足夠自我挖掘與學習能力
  4. 履歷上的縮排混雜著空白字元和跳脫字元

在其他未提及和重點項目,

  1. 履歷鏈結中出現 ?fbclid=XXX,作為一個從事資訊行業的人會直接出局。
  2. 強調的內容卻用淡色系的文字或其他方式呈現,脫離一般的思考模式。
  3. 個人網站
    • 能提高具有該實力的證明。
    • 展示出資訊整合的高度能力就是寫作,也可以從中看出溝通能力。
    • 因分享教學是最好的學習,請參閱「費曼學習法」。
  4. github/bitbucket 等代碼託管帳號
    • 自行開發的項目架構一覽無遺,直接進行武力展示。
    • 參與的開放項目,展現其求知慾與發現問題並解決的能力。
    • 參與的專案討論,展現其挖掘與辯論分析的能力。

在過程中可以透過白板直接介紹,也可以純口頭表達,亦可以採用簡報的方式,手法不受任何限制,但盡量貼合目標工作內容所需要的技能或是該公司缺少的項目為主軸。千萬別忘記體現自己所做過,拉回自己擅長的領域。

有時面試官突然問個太細節的問題,則需要準備概要和講給不同領域的人能理解的一句話,否則將容易錯失表現機會。畢竟那些項目細節上網搜尋即可,就像論文描述幾十頁,而摘要卻只有幾百個字一樣,唯有充分理解的人,才能做到的能力。千萬別花半個小時去描述需要解決的問題定義,適可而止!

筆試

好的公司是不會有筆試的。若有,則筆試應該只是參考依據。對沒有推薦人或認識的狀況下,筆試都是從嚴審核。反之,就不會佔有太大的比重。

不同類型的公司對筆試的考核方式皆不同。

對於著重實作細節的公司,如指定語言下的語法考試,甚至有一些未定義行為 (undefined behavior) 的項目需要注意,有的還會考以前大學教科書上附錄不想見的問題。這類型因為工作環境和需求,可能會讓新人非常受挫,對於新人的要求會比較高,才剛畢業的人通常要具備在沒有搜尋資源下,也能略知一二的能力。

對於跨領域的公司,會有一些其他領域題型,具有博學能力的人就會特別被選拔出來。像我們 EDA 公司,就有機會看到電子電路的題目,實際上答不出來也很正常,不用太沮喪,導致下一個階段表現不好。

對於應用方面,題目描述的手法也不一定像學校所學的非一即二的要求。若看到含糊不清的描述,那表示接下來的工作環境就是那種含糊不清的需求,則在答題上要自己以自身經驗去約束和討論問題,才能符合需求,原則上是以申論題的模式作答。

雷點如下:

  • 沒有檢查輸入、輸出的定義
    • 那個不是已知的條件,不能假設,常理上是絕對不行的啦。
  • 不完整的程式片段
    • 到底要回傳什麼,記得寫。
  • 不完整的上下文描述
  • 沒有計算過程的答案
    • 直接迸出一個數值。如果答錯,是完全沒有轉圜的餘地的。畢竟不知道你是不是費馬,還是把過程列出來吧。
  • 全然忘卻高中數學
    • 如為什麼 $\sin \theta$$\frac{a}{c}$
  • 相關題目的邏輯矛盾
    • 如在前一題答說 A 是缺點,卻在下一題答 A 是優點
  • 履歷中有提及此項目,卻無法以此作答
    • 履歷上的項目看起來只是掛名的對吧?像是上百人團隊的工作項目。
  • 論述內容呈現大學必修科目沒有學好
    • 如說雜湊的整體空間複雜度是 $\mathcal{O}(1)$
  • 在行為準則考題中,連演都不演的真性格
    • Q: 能不能與同事合作 A: 完全無法

同時,在撰寫的代碼中很容易展現出經驗差距,容易出錯的寫法雖然沒錯,卻間接告訴面試官有經驗不足的缺點。

白板題

解決只刷 leetcode 的假工程師的回合 (與考驗同事的時候)。

每題作答 30 分鐘,採用白板討論構思方向與實作細節。通常考三題,答題過快會有第四個防破台題目出現,題目對於以前打比賽的都是水題,基本上都會在十分鐘內解決。

類型通常都是教科書上每一章節的經典算法,答不出來或者構思錯誤,多半都會被刷掉。即使答出來,討論實務經驗時,也會從中觀察出是否有足夠的學習基礎,以及對所學的概念應用的能力。那些培訓班出來的假高手通常會在變化題型受挫,分析相關問題時無法應答,及缺少實務相關經驗。

後話

不太選用從短程培訓班出來的,風險相當大,因為太多沒有實戰或者過時技術的老師在外頭開補習班。試想過,為什麼能力好卻不從事開發工作,卻跑來當老師的原因嗎?再者,程式補習班老師多半沒有學過教育學程,跟教授一樣不需要,那麼教學品質很難有所保證。

現在又有大學入學程式考試 APCS (Advanced Placement Computer Science),自稱老師的越來越多,家長又瘋狂地看著老師的名號去選擇,而不知道科技業每一個職位與工作時間是有很大的差異,就像工作沒幾年就轉戰的 youtuber ,的確是高學歷也很聰明,但對於整體知識的養成與發展概念,通常都還沒沒達到融會貫通,只有問 A 答 B 的快速反應,缺少對 A 的定義及其他種非正規的可應用解的能力。

如何了解、觀察、解決、應用問題,適當地啟蒙學生才是老師要引導的。若像大學教授一樣教學生,九成的大學生畢業之後都不到八成內容。只是要知道問題的解決方法,可能還贏不了 Google,看看複製貼上的工程師就明白了。

有一些認識的學長,也開始從事家教和補習班老師,希望能遏止這種劣質的教學環境,就看看近期有沒有發展吧。

藉此,了解到不少學校的學生都找家教當槍手,學歷似乎也不能證明任何能力,家長資助和選對槍手才是當代的生存之道。面對欺瞞的文化,不行就說不行,請不要裝作很行,在面試過程中試出來,公司可是會記錄評價的。

Read More +

Company Ghost Story 公司鬼故事 6

Java

Observer vs. Inheritor

1
2
3
4
5
6
7
8
9
Tool tool = new Tool() {
@Override
protected void fireSomething(T t) {
super.fireSomething(t);
updateProcess(t);
}
}
void updateProcess(T t) {...}

當觀察者模式 (observer pattern) 的介面應優先考量 attach/addListener,不應該以繼承的方式覆寫 update/fireListener/notify 等函數。

WhatIf?

1
2
3
4
5
if (isA()) {
// something
} else if (isA() && isB()) {
// dead code
}

第二個 else-if 並不會執行到。

Equals vs. ==

1
2
3
if (a == b && a.equals(b)) {...}
if (a == b || a.equals(b)) {...}

對 Java 而言,有 Objects.equals 可以替代呼叫,又或者在 boolean equals(Object) 中包含 == 才對。

String.format

1
String.format("Move %s", fromObj, toObj);

對上參數數量是很重要的。

Mapper Function

1
2
3
4
5
6
7
8
static T a2b(String key) {
Map<String, T> mapper = new HashMap<>();
mapper.put("...", ...);
mapper.put("...", ...);
mapper.put("...", ...);
mapper.put("...", ...);
return mapper.get(key);
}

手動做 if-else 比較快的,建立 hash 可不是這麼簡單,記憶體宣告等因素需要考慮。而它也不屬於只宣告一次的靜態變數,因此並不適合這樣處理。

Comparable

1
2
3
4
5
@Override
int compareTo(T other) {
int[] c = {fa.compareTo(other.fa), fb.compareTo(other.fb) ...};
return firstNotZero(c);
}

if-else 不應該這樣被偷懶的,整個效能都爛了。

Argument vs. Method Name

1
2
3
4
5
void doExpand(boolean isExpand) {
if (!isExpand)
doCollapse();
...
}

好的命名決定品質。

Switch

1
2
3
4
5
6
7
8
static void enableAListener() {}
static void removeAListener() {}
static void addBListener() {}
static void stopBListener() {}
static void setCListener() {}
static void disableCListener() {
sListeners.remove(sAListener);
}

有一種被玩弄的感覺 …

Read More +

Company Ghost Story 公司鬼故事 5

Java

Duplicate Field

1
2
3
4
5
6
7
8
class C {
final A a;
final A b;
C(A a) {
this.a = a;
this.b = a; // why?
}
}

為什麼要做出兩個相同意義的欄位?

Strange Method Name

1
2
3
4
5
class UIComponent {
public void setDisable(boolean d);
public boolean isDisable();
public boolean isDisabled();
}

英文不好的我,一度慘死。

Strange Method Name II

1
2
3
4
5
class UIComponent {
public void setEnabled(boolean e);
public boolean isEnabled();
public boolean getEnabled();
}

拜託決定一種風格做一件事情。

Inefficient Listener

1
2
3
4
5
6
7
void init(Component c) {
c.addFocusListener(...);
c.addChangeListener(...);
c.addSelectListener(...);
c.setSelect(...); // trigger listener ...
c.setFocus(...); // trigger listener ...
}

初始化的優先順序很重要,多次觸發有可能發生,造成重複處理。

Override Method

1
2
3
4
5
6
7
8
9
@Override
public void uninstall() {
uninstallMe();
}
private uninstallMe() { // why not write into uninstall()?
// something ...
super.uninstall();
}

函數這麼簡單,為什麼 super.method 寫在原本的函數外部。出現不同於 @Override 的函數中實在少見。

Empty Listener

1
2
3
4
5
6
c.addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e) {
super.windowClosed(e);
}
});

不要註冊一堆空操作。

Override Empty

1
2
3
4
@Override
public void install() {
// please add comment here if remove parent processing
}

這不知道在寫什麼,也沒叫 super.install(),該觸發事件的都沒觸發,真的沒事嗎?應該是寫錯了吧

Read More +

Company Ghost Story 公司鬼故事 4

SVN

Commit File

1
2
3
4
5
6
7
$ svn status
.DS_Store
import.csv$
export.txt~
test_result.txt
golden.txt
? test_result.log
  • 別建立隱藏檔案格式,以小數點 . 開頭的在 Linux 可是隱藏檔案的意思
  • 別上傳 .DS_Store 那個是 Mac 平台上的隱藏檔案,並不需要
  • 別上傳 xxx.csv$那個是 Microsoft 平台上的 lock 檔案,並不需要
  • 別上傳 xxx.txt~ 那個是 emacs, nano, vim 等製造的備份檔案,並不需要
  • 別上傳測試結果,如 test_result.txt,正常來講會覆寫,傳上去結果沒產生怎麼辦,比對還會過呢。
  • 別上傳空資料夾

Commit Log

1
2
3
4
5
$ svn log golden.txt
------------------------------------------------------------------------
r32 | product_bot | 2003-01-13 00:43:13 -0600 (Mon, 13 Jan 2003) | 1 line
Update golden

拜託用自己的帳號上傳,不小心用到 product_bot 這種全自動的帳號,不曉得是誰修改的內容。

Read More +