TechTalk 專訪 Episode 26 逐字稿 - Java 8(上)


這一集很高興能夠邀請知名 Java 講師良葛格來接受我們的訪問,他將跟聽眾分享 Java 8 的新功能以及使用新的 Lambda/DateTime API 需要注意的地方。

HC – Hao Cheng
MY – Mao Yang
C: Catepillar

HC:大家好,歡迎收聽 Techtalk@Taiwan 第26集,今天是8月12號星期二,我是Hao Cheng。
MY:我是Mao Yang。

MY:我們今天很高興能夠邀請知名的 Java 講師良葛格來接受我們的訪問,如果有參加過 Java TWO 或是參加過教育訓練,大家應該都聽過他的演講,對他應該不陌生才是。

HC:哈囉,良葛格你好。

C:Hao Cheng、Mao Yang 你們好。

HC:很高興你能接受我們的訪問,雖然我想大家應該蠻多人都知道,不過還是請你做一個簡單的自我介紹好不好?

C:我早期曾經做過一些教育訓練,大家可能比較知道的,是幫昇陽教育訓練中心做教育訓練。目前的話,我是自由工作者,大家都看過我寫過不少的文件,其實技術寫作算是我的專長,我也有做一些翻譯,當然也有是會接一些教育訓練的工作。
C:我很喜歡研究各種程式語言,因為我很喜歡從不同的程式語言去瞭解,開發者在寫程式的時候是從什麼角度去切入與思考。所以你可以在我的網站上面,看到蠻多不同程式語言的一些文件,目前的話還蠻想歡寫一寫 Haskell 的東西,不過還在規劃當中。

MY:為什麼會想要寫 Haskell?

C:前兩天還在想的,因為我有幫忙 CodeData 寫專欄,我一直在思考的是,CodeData 裡面我要寫什麼,如果像我自己的網站的那些文件,那重疊性質很高,因此我在 CodeData 專欄的話是將之定位為一些比較有挑戰性或者是說經過再整理的內容。比如說我自己的網站筆記,在經過一些再整理或其中一些較有思考性的東西,我會想將之整理放在 CodeData 上。
C:Haskell 這個部份的話,跟我們待會要討論的東西有點關係,就是 Java 裡面導入蠻多 Functional Programming 的元素,雖然你可以不理 Functional Programming,但是瞭解了其實真的會對 Lambda 表達式與 Stream API 等比較也能夠知道怎麼運用,那學一門 Functional Programming 的真正語言,其實真的是會更有幫助。
C:我學過的是 Haskell,一般大家覺得它比較有學術性,然後我前幾天就突發奇想,如果我試著來寫 Haskell 的文件會如何?想說一般大家都覺得 Haskell 難懂。我的題目還沒定,不過想到一個很有趣的題目叫做〈地球人的 Haskell〉,盡量不要寫成火星文這樣子,那不過還在醞釀當。

MY:ok、ok,瞭解,那良葛格我看你花蠻多時間在經營你的網站,也在寫書還有在做教育訓練。

C:是。

MY:那我想聽眾都蠻好奇說,那你在時間的分配大概是怎麼樣?

C:因為我從退伍之後就只有過一個正職工作,之後都不是正職,包括以前跟教育訓練中心合作,也都是以 free lancer 的方式進行。因此,我的時間運用上確實蠻瑣碎的,這使得我得利用每個瑣碎的時間片斷,也許說 5 分鐘 10 分鐘那樣的時間,對大多數人來講就是很片斷很瑣碎,但是對我來講,都是我可以利用的時間。
C:其實以前,我的工作狀態還蠻常被打斷的,應該說,有點被逼著練就那種瑣碎時間的運用,也就是積少成多。我幾乎隨時都在想事情,包括走路也在想事情,坐車也在想事情,對!我覺得也沒什麼訣竅,就只是積少成多。
C:當然自由工作者真的自律性要比較高一點,我幾乎沒有什麼在看電視,漫畫倒是常看啦!那是生活中的人家說的小確幸,偶爾放鬆一下。

HC:之前在教育訓練那個時候,也不算專職的講師就是?

C:從來沒有擔任過專職的講師。

HC:ok、ok,瞭解。

C:正確來說,是曾經跟教育訓練中心簽過一年左右的短約,不過那時不是做講師的工作,那算是…找優秀的講師與講者,我的立場是,自己再跳過去做,等於分掉你去發掘優秀講師的那個心思與力氣,有時自己不得不跳過去做時,就會想,這是我本身沒有能力找到優秀的講師與講者,非不得以,我就是盡量找到優秀的老師來做這件事情,如果真的我跳過去做時,我就會檢討一下,自己的工作沒做好!

MY:瞭解!好,不然,我們今天來進入我們今天要談的主題 Java 8。

C:是。

MY:那請問良葛格,Java 8 算是從Java 5 以來改變最大的一個版本,可以請你介紹一下 Java 8,多了哪些你覺得比較重要的功能嗎?

C:當然大家從大部份網路上的文件,或者是演講當中聽到、看到、首先接觸到的就是 Lambda 的部份,因為那個對 Java 的開發者來講,算是語法上最明顯的一個變更,Lambda 語法的部份其實本身要去瞭解它不困難,也就是說從匿名類別的角度去思考的話,去掉類別名稱以及方法名稱,從這個角度去思考認識 Lambda 語法是比較容易切入的。
C:大家大部份把時間著重在 Lambda 語法的話,會失去對 Java 8 瞭解更多。例如,界面上可以有預設方法的實作,如果有寫過 Scala 話應該不難理解這個特性,也就是說,它其實相當於 Scala 裡面的 Trait。預設方法這個東西,可以把一些類別與類別間共用的一些實作,寫在interface 裡面,這個東西我覺得蠻好用的。在 Ruby 這門語言裡面類似的東西是 module,這被視為重大的特性。
C:例如,你可以設計一些比較方法,其實他只有一個抽象的方法要實作,你只要跟他說兩個元素哪個大哪個小,然後你實作這個有預設方法的介面之後,就可以自動擁有那些大於等於,或是等於、或者是小於等於的判斷方法,我覺得這是蠻不錯的一個特性。
C:當然你要講的話,他是一種多重繼承,只要引進多重繼承,就會有很多狀況需要判斷,當然,相對的也引進複雜性,我覺得這個是必要之惡,畢竟他對於程式碼在一些共用、共同維護的部份,確實是有幫助。
C:方法參考這個部份,其實大部份人對他有點誤解,其實方法參考不是那麼難理解,他的一個概念就是:因為現在有 Lambda 語法非常的方便,你可以到處去寫下 Lambda 表示式,那你在 A 處寫下 Lambda 表示式,在 B 處也寫下 Lambda 表示式,在 C 處也寫下 Lambda 表示式的話,很顯然地,萬一這些 Lambda 表示式其實都是做同一件事,程式碼內容幾乎一模一樣,也許只是變數不同,而這些 Lambda 表示式散落在四處,那我們也知道散落、重複就是一種不好的訊號,所以這個時候,其實就可以把它們提取出來定義成方法,依情況設計成靜態方法或是建構流程都可以,那你就可以在需要 Lambda 語法的部份,直接參考你已經定義出來的那個方法,將來你要維護的話,就只需要修改定義的方法就好。
C:在今年 Java TWO 研討會的時候,我曾經介紹過一本書《Java 8 Lambdas》,那個是 Oreilly 出的書,裡面也有提到類似的概念,而且書中說,其實這有利於你測試 Lambda 表示式,你把 Lambda 表示式直接寫在方法流程裡,你沒辦法測,但把它提取出來,第一,你就可以用方法參考去參考它,第二,就可以去測試它,所以從這個角度,方法參考這個部份就不難理解。
C:可以用一句話來涵蓋方法參考的作用:你有了 Lambda 表示式,不代表你要到處寫下 Lambda 表示式。
C:這句話我覺得還蠻適合的,當然,方法參考如果在命名上花點心思,就可以跟原來的那些 Functional API 去做一些結合,讓整個程式碼變得比較容易讀。例如說 filter、map 的時候,你就可以直接用那個方法參考名稱,就可樣知道 filter、map 做了什麼事,像是 filter(Number::greaterThan) 之類的名稱,就會很明確地知道這一句程式碼是在做什麼用。
C:當然,我們還會認識到很多 Consumer 之類的介面,以及那一些平行化的東西,這些其實我是都歸類在所謂的 Functional API,雖然它們可能散落在不同的 package 裡面,但是其實都是一些 Functional API,也就是說,他們的概念是來自於函數式程式設計,這就又接到我剛剛講到的 Functional Programming。這個東西其實大家都會覺得不太能理解,尤其是 Java 這個領域,畢竟它是命令式的一個語言。你要他一下子不要去想迴圈,要想 filter、map、reduce,他們可能就會覺得難以改變。
C:難道為了要用這些 API,一定得去認識 Functional Programming 嗎?那也不一定,我在Java TWO 上,其實就用了一個方式,也就是重構。其實重構這個東西蠻好用的,其實後來我在研究 Functional Programming 的過程當中,覺得它最後的目的,跟你重構的目的,其實是一模一樣的。
C:你說 Functional Programming 變數不可變動,ok,你把一個程式片段重構到細微的時候,就不需要用到會變動的變數,也就是說,你不用重新去指定一個變數值,因而,重構與 Functional Programming 的概念其實是一樣的。
C:你說迴圈怎麼重構?其實迴圈你去重構它,就把那個迴圈當作做一件事,然後把它重構到一個方法,只不過在那個方法裡面,如果不想用遞迴,那就是保持用迴圈,但是記得那個迴圈本體只做一件事。
C:所以說,整個 Lambda 專案,包括了 Lambda 表示式、預設方法、參考方法,它們雖然源自於函數式,但是其實在 Java TWO 我也有講過,不見得真的就要去理解函數式,如果從 Java 開發者的角度,也就是比較實務的角度來看,你想導入這些元素,那重構就對了!
C:整體來講,如果要我給 Lambda 一個結論,這個功能對我們 Java 開發者的作用是什麼,那就是:它是一種讓你用來重構的工具。當你重構到最後,可以導入那些 Functional API、那些 Lambda 元素,習慣這個過程之後,在碰到需要迴圈處理的東西,自然就會去用那些高階的Stream API。
C:也就是說,Lambda 可以當做一個重構的工具,在重構的過程中,訓練自己去習慣另一種角度,去思考程式碼的流程是怎樣,那你說最後是不是在寫 Functional Programming ,是不是很純粹的 Functional Programming,已經不是那麼重要了。

HC:我覺得其實 Java 本來就是一開始,就是一個以比較 OO,然後像 imperative 這種方式來寫的,的確對要接受 Functional 這個東西,好像還是有一點的門檻嗎?

C:是有一點門檻,所以我在準備 Java TWO Keynote 的時候,在要講什麼內容上,真的思考了蠻久,因為其實大家也可以在我的網站上看到,或者是以前其他的 Conference 上,都看過我講過 Functional Programming,也有直接帶大家認識各種語言的 Lambda 寫法是怎麼樣、為什麼 Java 會演進到這個 Lambda 語法,所以等於說我之前已經花過兩次或三次的研討會講過這類元素。所以,今天如果我還要再講 Functional API,是不是要把那些東西再提一遍?後來在我自己的專頁,我寫了一句話:「我能不能假設大家已經聽過我前兩場演講,再來聽這一場 Keynote?」
C:結果一個網友 pcbill寫了一句話:「我終於把前兩場良葛格寫的文件給看完了,我終於有資格去聽那一場 Keynote」,我就想說這樣不行,因為他一個蠻資深的開發者,如果連他都必須做這個動作,那我不敢假設大家都看過,所以我只好去思考,從另外一個角度去讓大家認識它。
C:所以,我想到一個是重構,因為講 Functional Programming 大家不熟,Java 畢竟也不是用來寫 Functional Programming 的專門語言,它是物件導向、命令式的語言。Java 中大家最熟的是 1999 年出版的那一本書《重構》,這是聖經本,後來還有很多演譯本,就跟設計模式一樣,大家對於重構這個東西太熟了,所以我就用這個角度去切入,剛好我也發現這其實跟 Functional Programming 的目的最後是一模一樣。
C:Java TWO 我在演講的過程中去重構一個程式,然後重構到最後,我還出了一個習題給各位,請大家回去看《重構》的第一章,把第一章重新完成,最後你再用 JDK8 的東西,將那個 Customer 類別再重構,我想這個方式大家會比較能接受,大家就暫且不要去理 Functional Programming 這個東西了,大家就是用 JAVA 的角度去認識他。
C:你一下子要去接受 Functional Programming 真的很難,其實我開始研究 Functional Programming 大概也是三、四年前的事了,驟然看到 Lambda 裡面有這些元素,還真的蠻訝異的,因為某些程度來講,真的拉進來許多概念,那只能說我真的是撿到一個大便宜,同時我會去想,身為一個 Java 開發者,要怎麼樣去把這個部份給熟悉,而不是因為其他語言有函數式的這些東西,那我們只好把它硬放進 Java,之後就不知道怎麼去用它。
C:網路上有很多文件去切入這個部份,當然也有從純函數式的方式去切入這個部份。我覺得大家不用想太多,就是重構,重構到最後,你能夠看出程式碼的意圖,然後再用 Functional API 去把它取代掉,這樣子大家就不會不熟悉,Functional Programming 大家就暫時忘了他沒有關係。

MY:ok,瞭解。

C:講到 DateTime 的話,這其實也是蠻激賞的一個部份,以我自己的經驗來講好了,過去就是 Date、Calendar 對不對?其實過去自己用的也還蠻漫不經心的,其實自己本身並沒有真的很常接觸,或者是說很嚴肅、很認真地要去使用這個日期跟時間的 API,所以大部份寫時間的程式,就是用一個 Date,然後用 Calendar 隨便加個兩下就讓他顯示日期這樣子,從來都不知道日期有這麼多議題要考量。
C:我們知道 DateTime JSR 的 Leader 是 Steven,他其實也是 Joda-Time 的 Creator,所以,我當然會去想要先認識一下,他當初寫 Joda-Time 的時候是怎麼寫的,一接觸之下不得了,因為其實他把 Joda-Time 的時間概念分為很多種,有 Instant、Period、Duration 等概念,然後他還很慎重地介紹了年曆系統,這些在 Joda-Time 官方網站都有文件。那個時候我第一個想到的就是:「我只是要一個日期,我需要瞭解這麼多概念嗎?」後來認真地去把他每一份文件都讀完後,還真的是需要瞭解那麼多。
C:因為 Joda-Time 很成功,大部份人會說 Joda-Time 很好用,是因為它用 Fluent API 去寫,寫起來會很爽、很簡單,大部份的人著重在這一點,但是我後來覺得真正重要的是,它那些 API 揭露的觀點,你在讀 Joda-Time 的文件時,你不得不去進一步細看 API 文件裡面的那些說明,因為它的 API 文件裡面每一個時間概念說明會更仔細。
C:ok,後來來到了JDK 8 的 DateTime API,其中當然很多觀念是來自於 Joda-Time,有人問 Steven,你為什麼不把 Joda-Time 稍微規格變更一下,讓它像個 JSR的規格、像個 Java 的規格,然後直接放進去就好,Steven 自己有寫過一個部落格文章解釋了為什麼,基本上,DateTime 是把 Joda-Time 裡面一些比較不好的設計元素,以及 Steven 經驗上比較不佳的時間概念給去除掉了,所以是比 Joda-Time 更好用一些,觀念上是大同小異,但是 API level 的話,你要去擴展 JDK8 DateTime的話會比較好擴展,當然大部份開發者不需要去擴展,不需要去創造一個新的年曆系統,但是它在 API level 思考如何操作時間的這個部份,我覺得還蠻值得研究一下的。
C:DateTime 如我剛剛所講的,開發者應該去試著去使用它,然後在使用它之前,你會被迫去瞭解一些時間觀念,那你就會知道,過去對時間大家有多麼輕忽。舉個例子來講,我最常舉的一個例子,也是在教育訓練當中人家問過我的一個例子,源由來自於大家一年的毫秒數計算方式多半是錯的,大家都覺得一秒就是 1000 毫秒,然後去乘以 365 天、60分鐘、60秒,來當作一年的毫秒數,大部份人都是這麼單純地認為,但是事實上這個真的是還錯的蠻離譜的一個觀念。如果你去用 DateTime API 的話,你會被迫,或者說會比較不會用自以為是的時間觀念去處理日期跟時間。

HC:ok,好,所以剛剛我們花蠻多時間講 Lambda ,因為這也算是這次 Java 8,大家覺得改最多的東西,因為從一開始,就是 N 年前大概 05 還 06 年那時候,在吵說到底要不要加 Lambda 到 Java 裡面,然後那個時候,我記得應該是 James Goling 出來,好像說他覺得太複雜不需要,然後到了某一年,應該是 Mark Reinhold 就跑出來說,我們決定要把 Lambda 加進去了,但是從他決定要加進去到真的出來,我記得也隔了三、四年有吧!還蠻多年的,從一開始討論語法到這個要不要,那個時候還有很多人提出有好幾個 spec,也就是 Lambda 的語法要像怎麼樣,那我看最後出來的語法也是還蠻接近 C# 跟 Scala的,那我想再來聊一下,你覺得最後定案的這個 Java 8 Lambda 語法還 ok 嗎?你有什麼看法?

C:我之前也曾經在這方面探討過,在 2010 年 JCD,也是所謂的 Java 認證日上講過 Lambda/Closure。

HC:ok。

C:對對對!那個時候其實就已經比較過,就是你剛剛講到的那個 Lambda 語法演進部份。

MY:那時候好像只有四個是不是?

C:對。

MY:還是三、四個?

C:對,我探討了其他語言的 Lambda 是怎樣,Java 裡面又是怎麼樣,功能上其他語言可以做到什麼,Java 上當時的 spec 又是做到什麼樣子,當時討論蠻多種的,結論之一就是 Closure 的問題,大家在講 Lambda 都會提到 Closure,但是在 Java 裡面來說是沒有 Closure 的,嚴格來講是半套Closure,因為在捕捉變數的時候,變數畢竟必須是 final,雖然你不用加 final 關鍵字,但要求它必須等效 final,在 Lambda 中如果說去更動它,Compiler 也不會讓你過,這個部份的話我覺得會造成一些不方便,但是我覺得其實還是有一些方式可以work around,也就是你還是可以去做到類似的效果,像是把它包在一個物件,然後讓那個物件被 Lambda 語法捕捉,然後還是可以試著去改變那個物件的狀態,在 Lambda 處理完之後,還是可以取到被變更的部份,事實上這樣子會多一層麻煩,有這一層麻煩的意思,其實就是在跟你說這件事情盡量不要做。
C:這邊還是要稍微牽扯到一些 Functional 的概念,就是說這個捕捉的動作,在Lambda 裡面去做變數的變更的問題,其實在 Functional Programming 裡本身就沒有變數這個概念,所以在 Java 裡面不這麼做,其實還是可以理解為什麼。當然很多人會覺得這樣有些地方不方便,但我覺得不方便是個還不錯的代價,因為其實在平行化的時候,你在 Lambda 裡面捕捉的變數,你去變更他的狀態,對於你將來要做平行化會有阻礙,不能變動變數這個部份是特意這麼做,我認為是做的還蠻不錯的。在 Java 8 的 Lambda 專案裡面,嚴格來說並沒有 Closure 的特性。
C:至於另外一個值得討論的重點就是,大家用過 Scala 的話,都知道其實它的 Lambda 目標形態是根據參數跟傳回值之型態而定義出來,這個形態的話你把他寫在方法參數或者是傳回值上,還蠻冗長的,而且這樣子會變成在 Java 中要再導入一個新的型態系統,Java 是物件導向,如果在大家已經熟悉這個物件導向設計的時候,再去導入另外一個型態語法,即使是個語法蜜糖,本身還是多了一個型態系統的語法,我們得被迫去學這樣一套東西。
C:後來他們討論用那個 Functional interface,我覺得蠻不錯,因為不用再去學一個新的型態系統,因為介面大家很熟悉,既有的 API 幾乎都能運用,就像我剛剛有講到,Lambda 裡面可以做方法參考,方法參考是一個參考既有 API 非常好的方式;Functional Interface 也是,在現有的 Java 標準 API 中充斥,那就表示說,符合Functional Interface 規範的既有API,都可以直接用 Lambda 來重構,在這個部份,我是很喜歡 Functional Interface 這樣的一個設計。
C:不過,Lambda 有一個很重大的問題,也就是 Generics,其實如果採用 Scala 那種作法,用參數跟傳回形態來做目標形態,那泛型的問題會更嚴重。有時候你在一些 Functional API、 Stream API 上看到那些泛型,你會根本看不清楚它到底要傳什麼參數進去、傳什麼值回來。
C:其實真正的問題也不在 Generics,如果有看過《Programming In Scala》這本書,裡面探討了蠻多泛型的議題,那你就會深深體會到,我記得它裡面也有講過,真正複雜的不是 Generics,真正複雜的是物件導向。什麼叫做 Generics?就是它是一種參數多型,你可以去指定一個形態參數真正的型態,而其實這是一種廣義的型態擴充,物件導向本身已經能夠擴充型態了,你又在泛型上能夠去做擴充型態,那這個動作那就是複雜加上複雜。如果對於一個不是物件導向的語言,譬如說我剛剛講過 Haskell,它也有泛型,但是它泛型一點都不複雜,為什麼?因為它不是物件導向,他的泛型就是純粹規範它的所謂的代數資料形態,代數資料形態不是物件導向,因此它的泛型很簡單。
C:我們回到 Java,如果你是寫 API,可以運用 Lambda 的 API,當然就是比較痛苦,好處是什麼?你的痛苦會換來你的 Client 使用上的方便,比如說,我現在對於那個 Stream API很熟悉了,我根本不用去看泛型,寫熟時根本就不用看,因為我們知道 Lambda 可以不用寫型態,也就是一個變數名稱可以不用寫出型態是什麼,就那個變數在 Lambda 本體要做什麼,我就寫出來,Client 其實是不用去思考泛型,痛苦是在一開始看 API 文件的時候,當你弄懂 API 文件上的泛型怎麼用之後,接下來你一次兩次的使用,久了就不會去想,我這個時候應該是什麼型態,所以在這個時候,泛型反而是個幫手。
C:因為其實泛型是什麼?泛型是不是寫給人看,是寫給機器,也就是寫給編譯器看的,當編譯器看得懂之後,你就不用再去理他了,這個其實是我在 Lambda 跟泛型的一個想法:泛型是對 Client 蠻大的幫手。大家可以想像,如果 Lambda 的相關 API沒有使用泛型,每個 Lambda 中都要把每個形態仔細地寫出來,那程式碼能看嗎?程式碼不能看,真的會還蠻醜的,更不要說可讀性了,根本就不會有人想去用他。
C:Lambda 語法上當然還包括了方法參考,我剛剛講到,方法參考其實就是避免你到處寫下同樣的 Lambda 表示式,對可維護性、可讀性,甚至說因為 Lambda 抽取出來變成一個方法了,你還可以對它做測試,方法參考嚴格來講也是 Lambda 語法的一部份,我還蠻喜歡方法參考這個特性,在 Java TWO 上其實你可以看到,後面其實我的範例幾乎都是用方法參考,因為跟Lambda 語法相比,方法參考還是比較好讀一點,唯一比較小小的缺憾就是,它的前面還是得有一個前置名稱,例如說一個類別後兩個冒號,這個部份我覺得如果再可以更簡潔一點會比較好,因為有時候我不需要那兩個冒號前面的前置名稱,比如說 filter(getName) 總是會比 filter(Customer::getName) 來得好讀一點,每次在打兩個冒號前面的前置名稱,我總是會想到這個缺憾。