之前一直使用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是類型系統的一部分,一個不拋出異常的函數和一個會拋出異常的函數,它們的類型是不相同的。這就導致了Java的Lambda泛用性大大減少而且不是很好用。以對List的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。