背景
週五本該是愉快的,可是花了一個早上查問題,為什麼要花一個早上?我把原因總結為兩點:
- 日誌資訊嚴重丟失,茫茫程式碼毫無頭緒。
- 對泛型的認識不夠,導致程式碼出現了BUG。
第一個原因可以通過以後編碼謹慎的打日誌來解決,我們今天主要來一起回顧下 JAVA 泛型基礎。
一個小栗子
先看下面一個例子,test1例項化一個List容器的時候沒有指定泛型引數,那麼我們可以往這個容器裡面放入任何型別的物件,這樣是不是很爽?但是當我們從容器中取出容器中的物件的時候我們必須小心翼翼,因為容器中的物件具有執行時的型別資訊,這意味著你不能夠將一個帶有執行時型別資訊的物件賦值給另一個型別,否則ClassCastException。
1 2 3 4 5 6 7 8 9 10 |
@ Test public void test1() throws Exception { List list = new ArrayList(); list.add("float.lu"); list.add(1); String name = (String) list.get(0); int num = (Integer) list.get(1); System.out.println(String.format("name[%s], num[%s]", name, num)); } |
上面的程式碼沒問題,可以很好地編譯和執行通過,問題是我必須要事先很清楚地知道容器中的索引為0的物件是什麼型別,索引為1的物件是什麼型別,很顯然,這在實際應用中是不切實際的,也是一種很不靠譜的做法,那麼這個問題如何解決呢?泛型。
引入泛型
為了解決這個問題,我們引入泛型,下面程式碼可以看出與上面不同的是我們在例項化容器的時候加了<String>這個東西,這個東西的學名叫做泛型引數,就像普通方法帶有引數一樣,interface List<E>中的E為形式引數、而String為實參。
1 2 3 4 5 6 |
@ Test1 public void test2() throws Exception { List<String> list = new ArrayList<String>(); list.add("a"); list.add(1)//1 } |
引入泛型後,我們規定這個容器中只能存放型別為字串型別的物件,好的,編譯器可以識別泛型並幫我們檢查編譯錯誤,上面的程式碼中1處會出現編譯錯誤。注意:泛型資訊僅僅存在於編譯期間,編譯器可以通過泛型資訊來對程式碼是否存在違規行為(編譯錯誤)來進行檢查,當編譯器將程式碼編譯為位元組碼之後,泛型資訊將不復存在,然而物件的執行時資訊仍然是有的,這就解釋了為什麼會出現ClassCastException。
別高興太早
有了泛型我們可以讓程式碼安全地通過編譯,並且我們認為他是安全的了,嘿嘿,是否就真的安全了呢?是否就能和ClassCastException說拜拜了呢?答案是:NO。看看下面這段程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 |
@ Test public void test3() throws Exception { List<String> list = new ArrayList<String>(); list.add("a"); list.add("b"); List _list = list; List<Integer> integerList = _list; for (Integer item : integerList) { System.out.println(String.format("item[%s]", item)); } } |
上面這段程式碼編譯沒有問題,我們沒有直接將泛型引數為String的容器賦值給泛型引數為Integer的容器,而是花了點點小心思,我們現將list賦值給_list,_list生命為可以儲存任何型別,也就相當於無特定型別,而後我們又把_list賦值給integerList容器,integerList容器被宣告為只能儲存型別為Integer的物件。悲催的是這段程式碼在執行的時候報了ClassCastException,很明顯,我們知道在迭代integerList容器中的物件的時候,這些物件是有執行時型別資訊的,當帶有String型別資訊的物件賦值給Integer的時候顯然就報錯了。這一切看起來似乎沒問題,符合邏輯,但是有一個問題我們還沒有問:為什麼會沒有編譯錯誤?
泛型術語
在學習數學的時候我們往往會對一個證明題進行論證,而論證之前我們手上往往會有一些不需要證明的已知定理,下面這些“定理”將被用來直接回答上一節中遺留的問題。
- List<E>被稱作泛型型別。
- List<E>中的E被稱為型別變數或型別引數。
- List<String>被稱為引數化型別。
- List<String>中的String被稱為實際型別引數。
- List<E>中的<>年typeof。
- List被稱為原始型別。
- 引數化型別可以引用一個原始型別物件,編譯報告警告。
- 原始型別可以引用一個引數化型別物件,編譯報告警告。
由上可知,List<Integer> integerList = _list;可以通過編譯。
看清本質
經過上面的一些小波折,我們瞭解一些關於泛型的本質:泛型是給javac編譯器使用的,javac是JAVA的編譯器,而泛型可以讓程式碼在編譯期間確定型別安全,比如我們告訴編譯器某個容器只能儲存某種型別的物件,那麼編譯器會為我們好好地檢查,確保型別安全,但是安全是相對的,只要我們逃過編譯器,我們就有一百種方法讓程式碼ClassCastException(比如反射)。同時編譯之後引數化型別在執行時沒有任何泛型資訊,也就是為什麼List.class和List<String>.class是同一個東西。除了引數化型別之外,容器中的物件在執行的時候是有型別資訊的,也就是為什麼會ClassCastExcetion。關於泛型還有很多內容,這裡不做多講,文中有誤也歡迎留言討論。
為學日益,為道日損,損之又損,以至於無為。無為而無不為,取天下常事以無事;及其有事,不足以取天下。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式