嘗試Java,從入門到Kotlin(上)
之前一直使用C#開發,最近由於眼饞Java生態環境,並藉著工作服務化改造的契機,直接將新專案的開發都轉到Java上去。積攢些Java開發經驗,應該對.NET開發也會有所啟發和益處。
從理論上說,Java和C#語言差別不大,畢竟難聽地說,C#就是抄Java出來的。程式語言簡史如是介紹這兩種語言:
然而隨著時間流逝語言發展,個人認為,C#在語言層面已經大大領先了Java。關於Java和C#的比較這幾篇文章http://blog.zhaojie.me/2010/04/why-java-sucks-and-csharp-rocks-1-thoughts-and-goals.html有著詳細的描述。下面我總結一下我在趟過的坑,以供轉型或學習的同學參考。
本文並非要比出這些語言誰優誰劣。有時候,好或壞是非常主觀的判斷,不同人有著不同的看法,強行斷定好壞只會引起無畏的爭論。這些語言有著各自的特點,有各自適合的場景。就像下面要談到的Checked Exception特性,這是個很好的特性,但是在一些情況下也會引起不少麻煩。
Checked Exception
Java是Checked Exception的。這就是說,如果你寫了一個方法,這個方法會丟擲一些異常,那麼你需要用throws
關鍵字標明這個方法會丟擲哪些異常。這個特性很難說是好還是不好。Checked Exception本質上是一種型別系統,它明確規定了一個方法除了返回值型別以外,還可能丟擲什麼異常。這樣呼叫方函式就能夠明確地知曉應該處理或者傳遞哪些異常。這個特性在用得好的人手裡,對正確處理各種邊邊角角的異常十分有用。然而,如果在你無法自己選隊友,無法控制開發人員的水平的情況下,你很可能會發現,所有的方法都被標記為throws Exception
Lambda,以及與Checked Exception產生的奇怪反應
Java的Lambda本質上仍然是一個物件。事實上,Java的Lambda函式是一個滿足Functional Interface介面的物件。比如下面程式碼,聲明瞭一個具有一個int
引數,返回一個int
引數的函式。
@FunctionalInterface
interface AFunction {
int invokeBalaBala(int a);
}
我們可以這樣定義一個這個函式的變數:AFunction f = x -> 2 * x;
。
Java的Lambda和Checked Exception結合在一起後,產生了一個非常棘手的問題。由於Checked Exception是型別系統的一部分,一個不丟擲異常的函式和一個會丟擲異常的函式,它們的型別是不相同的
map
操作為例,我們可以用如下程式碼將list
裡的每個元素翻倍:
list = list.stream().map(x -> 2 * x).collect(Collectors.toList());
這裡map
接收一個型別為輸入一個int
引數,返回一個int
值的函式。然而,如果我們需要給它的函式有可能丟擲異常,比如這個函式會去讀取檔案、訪問網路服務、或者做Json反序列化,則由於型別不同,Java編譯器將會報錯。
// 這個編譯器會報錯
list.stream().map(x -> JsonUtil.parse(x)).collect(Collectors.toList());
解決方案一種是在函式體中使用try cache處理異常。但是很多時候,異常沒辦法在這個時刻處理,必須要丟擲。那麼還有另一種方案:將異常轉換為RuntimeException
,RuntimeException
是所謂的Unchecked Exception,它不是型別系統的一部分,不需要用throws
標註,所以不會導致函式型別變化。另一方面,編譯器也無法檢測出是否可能會丟擲RuntimeException。無論採用哪種方案,都使得這個Lambda函式變得沒那麼好看。
泛型
Java的泛型原理和C#不同。C#是執行時泛型,在程式執行的時候仍然能獲取泛型的型別資訊。而Java的泛型是型別擦除(Type Erasure)式泛型。名稱聽起來很高大上,意思是Java的泛型僅僅用於編譯時型別檢查,型別檢查完成後,型別資訊就被編譯器擦除。在最後生成的位元組碼中中,泛型型別都被改為Object
型別。
比如這句:
HashMap<TK, TV> map = new HashMap<TK, TV>();
編譯後變成:
HashMap map = new HashMap();
Type Erasure方式的影響主要有兩個:
- 執行時無法判斷型別;
- 執行時無法動態生成泛型具現化的類的例項。
像下面兩句:
x instanceof T
new T()
在Java中都會編譯出錯。而這在C#中都是很常見的程式碼。在C#中,我們可以有這樣的Json反序列化方法:
T parse<T>(string jsonStr)
這個方法將jsonStr
反序列化為型別T
的一個物件。這種寫法看起來十分自然。然而在Java中無法實現。因為在parse
方法中需要在執行時例項化T
的一個物件,而Java在執行時這些泛型都已經被擦除,無法獲取型別T
的資訊,從而無法例項化。要在Java實現類似的方法,需要額外將一個Class
物件放到引數:
T parse(String jsonStr, Class<T> type)
這樣Java才能使用這個type
,在執行時使用反射的方式生成型別T
的例項。
Getter/Setter
在面向物件哲學中,欄位屬於實現細節,應該設為private
使它隱藏在類的內部。但是在實際中,有很多欄位需要直接訪問和修改。從功能實現上講,直接把欄位設為public
也是可以的。但是這樣做的壞處在於未來功能擴充套件時,這個欄位的含義、儲存方式可能發生變化,導致每個使用了這個欄位的程式碼都需要修改。因此,應該將欄位的訪問封裝的方法中,即使只是很簡單的訪問和設定,也應該實現getter方法和setter方法。
C#和Python有property特性支援快速定義和呼叫getter方法和setter方法。Ruby則依靠函式呼叫可以省略括號的特性,使getter方法看起來很像直接訪問欄位。Java沒有使用特性支援getter和setter方法,而是約定必須實現欄位名前加get
的getter方法(然而這裡有個不一致的地方,如果欄位是布林型別,則加is
)和欄位名前加set
的setter方法。這導致的一個問題是開發時需要編寫大量的getter方法和setter方法。為Java冗長的特點貢獻了一份力量。遵循這個規範很重要,以為在很多常用庫,比如Json序列化,會以getter方法作為欄位存在的依據。
為了減少開發工作量,可以使用IDE自動生成getter方法和setter方法。常見的Java IDE都支援自動生成getter方法和setter方法。另一個方案是使用Lombok,通過Data
,Getter
,Setter
等註解,讓編譯器在編譯時自動生成getter方法和setter。