Joshua Bloch#

Google 的首席 Java 架構師,曾任 Sun Microsystems 的 Distinguished Engineer,領導了 Java Collections Framework 的設計與實作,並參與了 Java 5 多項語言功能的設計。擁有 Columbia University 的學士學位和 Carnegie-Mellon University 的博士學位。

他是 2001 年 Jolt Award 得獎書籍 Effective Java 的作者,也是 Java PuzzlersJava 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 應該是能處理所有你關心的使用案例的最簡單的東西。

設計步驟#

  1. 確定 use cases(使用案例)——這是最關鍵的步驟
  2. 寫一個非常簡短的骨架 API(skeletal API)——通常能放在一頁紙上
  3. 用 use cases 來編寫使用 API 的客戶端程式碼——在實作 API 之前
  4. 根據客戶端程式碼的回饋調整 API
  5. 當對 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 認為,幾乎所有事物——只要做得好——都能讓你成為更好的程式設計師。想法從各處遷移。保持眼睛開放,願意重用來自不同領域的想法。