Joshua Bloch#
Google 的首席 Java 架構師,曾任 Sun Microsystems 的 Distinguished Engineer,領導了 Java Collections Framework 的設計與實作,並參與了 Java 5 多項語言功能的設計。擁有 Columbia University 的學士學位和 Carnegie-Mellon University 的博士學位。
他是 2001 年 Jolt Award 得獎書籍 Effective Java 的作者,也是 Java Puzzlers 和 Java Concurrency in Practice 的共同作者。
學習程式設計的歷程#
童年啟蒙#
- 父親是 Brookhaven National Lab 的化學家,在 Bloch 四年級時上了一門程式設計課
- 當時(約 1971 年)電腦是大型主機,需要把打孔卡片交給操作員
- 雖然不是親手操作,但「電子計算機能為你做事」的概念讓他著迷
- 從父親那裡學了一點 Fortran
高中時期#
- 1973-1976 年在 Long Island 的 DECsystem-10 分時系統上使用 BASIC 程式設計
- 這台機器在 Suffolk County 的所有學校間共享
- 1977 年 7 月 4 日寫了一個「animals」遊戲——經典的 Twenty Questions 遊戲
- 使用 binary tree 儲存在磁碟上,程式會隨時間「學習」變得更聰明
- 高中時為 Long Island Math Fair 開發了「inter-job communication programs」(即時通訊程式的前身)
Bloch 回顧自己最早期的程式碼,發現他追求程式可讀性(readability)的風格從未改變。正如 Knuth 所說,程式本質上是一篇文學作品。
大學與研究所#
- 在 Columbia University 就讀,每門課使用不同語言:數值課用 Fortran、程式設計課用 Pascal/SAIL/Simula、AI 課用 Lisp
- 到了很晚才接觸物件導向程式設計——Java 是他認真使用的第一個 OO 語言
- 在 Carnegie-Mellon University 攻讀博士,研究 Camelot 分散式交易處理系統
- 博士論文分析了一個分散式資料結構——replicated sparse memory
程式設計風格的演變#
可讀性至上#
- 早期受限於單字元變數名,但一直努力讓程式可讀
- 隨著經驗增長,更加注重變數命名——會花數小時在 identifier names 上
- 如果使用識別符號讀起來像英文句子,程式更可能正確且易於維護
不重複原則#
- 早年對 copy-and-paste 比較寬容,現在嚴格遵守 DRY 原則(Don’t Repeat Yourself)
- 如果發現自己在複製貼上,就會思考:「這個設計哪裡有問題?怎麼修?」
- 隨年齡增長,對自己的程式碼標準越來越嚴格
對 BASIC 的看法#
- 不同意 Dijkstra「BASIC 會損害學生的思維」的觀點
- 認識很多從 BASIC 起步的優秀程式設計師——那是當時可用的語言
- 但同意應該學習多種語言
Bloch 強調:年齡越大越意識到,程式設計不只是讓程式碼能運行,而是要產出一個可讀的、可維護的、高效的成品。通常越乾淨的程式,執行速度反而越快。
推薦書籍#
必讀清單#
- Design Patterns(Gang of Four)——雖然有些過時,但提供了通用詞彙和好想法
- Elements of Style(Strunk & White)——軟體工程師的工作很大一部分是寫散文(specs、文件),且書中的大部分思想也適用於程式設計
- Hacker’s Delight(Hank Warren)——bit-twiddling 的聖經,對寫函式庫、編譯器、低階圖形或密碼學不可或缺
- The Art of Computer Programming(Knuth)——當需要研究特定演算法時查閱
- The Elements of Programming Style(Kernighan & Plauger)——雖然範例用 Fortran IV 和 PL/I,但理念完全不過時
- The Mythical Man Month(Frederick Brooks)——40 年後依然適用
- Java Concurrency in Practice——雖然書名有 Java,但大部分內容超越特定語言
- Merriam-Webster’s Collegiate Dictionary——命名識別符號需要好字典
軟體設計流程#
API 優先的設計方法#
- 在 OOPSLA 發表了 “How to Design a Good API and Why It Matters”
- 最重要的是先了解你要解決的問題——需求分析不可過度重視也不可忽視
- 客戶通常不會告訴你問題,而是告訴你解決方案——你需要反覆追問「為什麼?」來找出真正的需求
Bloch 的 API 設計根本定理:有疑問時,就不要加進去(when in doubt, leave it out)。API 應該是能處理所有你關心的使用案例的最簡單的東西。
設計步驟#
- 確定 use cases(使用案例)——這是最關鍵的步驟
- 寫一個非常簡短的骨架 API(skeletal API)——通常能放在一頁紙上
- 用 use cases 來編寫使用 API 的客戶端程式碼——在實作 API 之前
- 根據客戶端程式碼的回饋調整 API
- 當對 API 有信心後,才開始實作
Test-First 的 API 設計#
- 這是一種應用於 API 層面的 test-first programming
- 先寫使用 API 的程式碼,再寫實作 API 的程式碼
- 無法在真正編碼使用 API 之前就確保 API 是對的
- 如果在實作 API 底層之前就發現設計問題,那是巨大的節省
簡單性的價值#
- 「寫最簡單的能用的東西,然後無情地重構」不意味著寫草率的程式碼
- Martin Fowler 也是在寫程式碼之前思考系統的合理結構和形狀
- 與 Martin Fowler 的分歧:Bloch 不認為測試能替代文件——精確的 specs 仍然必要
閱讀程式碼#
閱讀方法#
- 真心希望程式是良好撰寫的——緊密耦合的系統幾乎無法閱讀
- 希望找到「10,000 英尺高度」的系統描述——知道重要的模組是什麼
- 先讀高階描述,偶爾深入低階模組來加深理解
- 雖然程式碼是線性書寫的,但執行不是線性的——需要工具快速定位被呼叫的方法和類別
閱讀的偏好#
- 閱讀小模組、獨立理解它們
- 會將所有相關程式碼印出來,坐在地板上被印刷品圍繞,寫筆記
Stepping Through 程式碼#
- 這仍然是他最主要的除錯方式
- 特別是並行程式碼——狀態太多,無法全部列舉
- 在心裡 step through:盯著程式碼,思考在什麼時間點哪些不變量必須成立
除錯方法與經驗#
最難的 Bug:壞掉的 Mutex#
- 在 Transarc 公司(Pittsburgh,1990 年代初)實作 transactional shared-memory
- 寫了一個「basher」——大量嵌套交易並行操作共享陣列的壓力測試
- 高並行度下偶爾出現一致性檢查失敗——不可重現
- 花了一週寫詳盡的單元測試,最終發現問題不在自己的程式碼
- 底層 mutex 實作有 Bug——負責 mutex 程式碼的工程師在組合語言中交換了 lock 和 try-lock 的標籤
- 結果整個公司的 mutex 其實都沒有正常工作好幾週,卻沒人發現
這個故事的教訓:讓 Bug 難以追蹤的因素包括 (1) 涉及並行程式、(2) 完全不可重現、(3) 一個核心假設(mutex 是正確的)其實是錯的。Bloch 引用 Knuth 的話:在寫測試時要「讓自己進入最卑鄙、最惡毒的心態」。
Thread ID 的 Bug#
- 在 Transarc 或 CMU 的 Camelot 系統中遇到
- Trace 套件會標記發出事件的 thread ID,但偶爾 ID 不正確
- 原因:threading package 用 stack 變數的高位元來推算 thread ID,假設每個 thread 的 stack 大小是固定的 2 的冪次
- 但當時的標準做法是在 stack 上建立大型物件(100 元素 x 4k 的陣列),導致 stack overflow 進入另一個 thread 的 stack 區域
- 這不只是 trace 的小問題——thread 會冒充另一個 thread,存取它的 thread-local 變數
除錯工具#
- 主要依靠眼睛和大腦——印出所有相關程式碼仔細閱讀
- 使用
print語句和 breakpoint - 偶爾使用 debugger,但不覺得沒有 debugger 就不行
- 使用 assertions 來維護複雜的不變量——不變量被破壞時立即知道
對 Assertions 與不變量的看法#
- 是將 assertions 加入 Java 語言的第一人——但 assertions 從未真正成為 Java 文化的一部分
- 只有少數 Java 程式設計師使用 assertions
- 寫程式時會問自己:「此時什麼必須為真?」然後將答案放入 assertion 或至少放入註解
- 理解 invariants(不變量)需要數學中的精確思維——理解「證明」的概念
Bloch 認為數學思維對程式設計至關重要。歸納證明(induction proofs)與遞迴程式設計密不可分——你可能不知道 base case 和 induction hypothesis 這些術語,但你必須理解這些概念才能寫出正確的遞迴程式。
對 Java 的看法#
Java 的優勢#
- 認為 Java 擁有目前最好的多執行緒建構模組(multithreaded building blocks)
- Java 的
java.util.concurrent提供從低階(AtomicInteger)到中階(CyclicBarrier)到高階(ConcurrentHashMap、ThreadPoolExecutor)的完整並行工具 - 在大多數情況下,程式設計師的時間比電腦時間更有價值——使用更安全的現代語言更有效率
- Google 正在越來越多地使用 Java(雖然核心搜尋管道仍是 C++)
Java 的複雜度問題#
- Java 5 增加了比預期更多的複雜度
- Generics(泛型)特別是 wildcards(萬用字元)增加的複雜度遠超預期
- 如果 generics 早期(不含 variance/wildcards)就加入,可能會有更簡單的語言
- 型別系統的修改是微妙的,可能產生深遠且不可預測的影響
Bloch 從 generics 的經驗中得到的教訓:不應該在不完全理解某功能對概念表面積的影響之前就加入語言。功能之間的複雜度至少是二次方(quadratic)的關係——新功能與所有現有功能都可能交互作用。
語言複雜度的臨界點#
- C++ 已經被推到遠超其複雜度閾值的地步——幾乎每個使用 C++ 的團隊都會子集化語言
- 這導致「程式設計師可移植性」(programmer portability)的喪失——無法輕易閱讀別人的 C++ 程式碼
- 語言加入過多複雜度不會讓語言消失,但會迫使人們子集化它
Generics 的反思#
- 仍然喜歡 generics——能找出程式碼中的 Bug,把原本只能放在註解中的東西變成編譯器可檢查的
- 但承認 generics 的設計不夠成熟就加入了
- 加入新功能的動機部分來自「這很酷且正確」(而非解決真實使用者的問題)
- James Gosling 說過:「在加入任何東西之前,你需要三個真實的使用案例」
選擇程式語言的哲學#
語言如同酒吧#
- 選擇語言不只是選擇技術權衡——更是選擇一個社群
- 就像選擇酒吧:你想去一家調酒好的酒吧,但更重要的是那裡有什麼人、他們談什麼
- 隨著社群的成長,語言周圍會建立起工具、函式庫等軟體資產
- 這就是為什麼有些在紙面上更好的語言輸給了社群更好的語言
語言的網路效應#
- 如同 Metcalfe’s law:語言的價值與使用者數量的平方成正比
- 即使 Java 不是完美的語言,但其生態系統(Eclipse、FindBugs、Guice)帶來巨大的附帶利益
學習新語言#
- 就像學習口語——會的語言越多,學新語言越容易
- 學新語言時要帶著已有的知識,但保持開放心態
- 某些語言會讓人形成「這就是所有程式都該這麼寫」的想法——這會妨礙學習新語言的精髓
開發工具#
- 自稱「老古董」——Emacs 的快捷鍵已經深植腦中
- 使用 IntelliJ 但不太熟練
- 承認現代 IDE 對大規模重構非常有價值——Brian Goetz 指出人們因為有工具支援而寫出更乾淨的程式碼
- 坦承自己不擅長工具——build 和 source-control 工具變化太快
- 認為工程師有各自擅長的領域——有些人擅長 API 設計(需要「empathy gene」同理心基因),有些人擅長效能優化
對軟體工程的整體觀點#
正確性的困難#
- 從「Nearly All Binary Searches and Mergesorts Are Broken」部落格文章中學到:即使是小程式,要寫正確都是難以置信地困難
- 我們只是在欺騙自己,以為程式是正確的——實際上大多數程式只是「足夠接近正確」
- 因此他相信靜態型別和靜態分析——任何能從我們盤子上移除一類 Bug 的東西都是好事
簡單性的信念#
- Tony Hoare 的名言:「設計系統有兩種方法。一種是使它簡單到明顯沒有缺陷;另一種是使它複雜到沒有明顯缺陷。」
- 他的「複雜度計量器」(complexity meter)進入紅區時,就是重新設計的時候
- 如果事情開始變得複雜,那可能是有什麼地方出了問題
API 文件的重要性#
- Javadoc 是 Java 平台成功的重要但被低估的原因之一
- 好的 API 文件從第一天就是 Java 文化的一部分
對程式設計教育的看法#
- 學生應該學低階語言、組合語言,甚至晶片架構
- 但不應該用 C 作為第一門語言——不應該讓學生一開始就處理 buffer overrun 和手動記憶體管理
- 理解底層讓你成為更好的高階語言程式設計師
公司的 DNA#
- 公司很早就建立其 DNA,之後很難改變
- IBM 在 1982 年仍被批次處理文化主導
- DEC 從未逃脫分時處理的心態
- Google 面對的問題是能否超越「在網路上賣廣告」的定位
對數學與程式設計的關係#
數學的重要性#
- 數學思維對程式設計至關重要——歸納證明、離散數學
- 離散數學比微積分更重要(對程式設計師而言)
- 但微積分中「讓思維繞行無限」的能力也很有價值
- 博士論文中用到的關鍵想法來自化學課——rate-balance equation(速率平衡方程式)
跨領域的思維#
- 生活中看到的很多事物——建築、語言溝通的方式——都可以重新應用到電腦科學
- 數學和程式設計非常相似——保持眼睛開放、願意重用想法是好事
程式設計的多學科本質#
- 程式設計是多學科的匯合(confluence of disciplines)
- 寫函式庫和編譯器需要更多數學
- 寫 Web 應用需要更多溝通能力(verbal 和 visual)
- 即使是函式庫和框架也需要可讀性和可維護性——如果你不是稱職的寫作者,很難達成這個目標
對並行程式設計的看法#
- Java 是第一個提供內建多執行緒機制的主流語言
- 認為 Java 的並行方法是目前所有語言中最好的
- 對 Software Transactional Memory (STM) 持觀望態度——尚未在主流語言中以實用形式存在
- Erlang 的 actor model 可以在很多語言中實作(Scala 已經做到了)
- 不確定 actors 是否最適合 multicore parallelism,但如果是的話,很快就會有人在 Java 中實作
享受程式設計#
- 仍然享受程式設計,雖然方式不同於年輕時
- 年輕時程式設計是逃避現實的避風港——年輕時有無窮的精力可以連續工作數小時
- 但那種寫出幾行漂亮程式碼、看著拼圖拼在一起的無可否認的高潮感(undeniable high)仍然存在
- 隨著經驗增長會有逃避行為(avoidance behaviors)——開始是最困難的部分
- 但提醒自己:「你已經做這件事三十年了,幾乎每次結果都不錯」
Bloch 認為,幾乎所有事物——只要做得好——都能讓你成為更好的程式設計師。想法從各處遷移。保持眼睛開放,願意重用來自不同領域的想法。