## 13 差異對比:集合在 Java 7 和 8 有何不同和改進
## 引導語
Java 8 在 Java 7 的基礎上,做了一些改進和優化,但我們在平時工作中,或者直接升級到 Java 8 的過程中,我們好像無需做任何兼容邏輯,那么 Java 8 底層是如何處理的呢,在改進的同時,是如何優雅兼容 Java 老版本,讓使用者無需感知,接下來我們通過對比 Java 7 和 8 的差異,來展示 Java 8 是如何優雅升級的。
#### 1 通用區別
#### 1.1 所有集合都新增了forEach 方法
List、Set、Map 在 Java 8 版本中都增加了 forEach 的方法,方法的入參是 Consumer,Consumer 是一個函數式接口,可以簡單理解成允許一個入參,但沒有返回值的函數式接口,我們以 ArrayList 的 forEach 的源碼為例,來看下方法是如何實現的 :
```
@Override public void forEach(Consumer<? super E> action) { // 判斷非空 Objects.requireNonNull(action); // modCount的原始值被拷貝 final int expectedModCount = modCount; final E[] elementData = (E[]) this.elementData; final int size = this.size; // 每次循環都會判斷數組有沒有被修改,一旦被修改,停止循環 for (int i=0; modCount == expectedModCount && i < size; i++) { // 執行循環內容,action 代表我們要干的事情 action.accept(elementData[i]); } // 數組如果被修改了,拋異常 if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } }
```
從這段源碼中,很容易產生兩個問題:
1、action.accept 到底是個啥?
action.accept 就是你在 for 循環中要干的事情,你可以進行任何事情,比如我們打印一句話,如下:
```
public void testForEach(){ List<Integer> list = new ArrayList<Integer>(){{ add(1); add(3); add(2); add(4); }}; // value 是每次循環的入參,就是 list 中的每個元素 list.forEach( value->log.info("當前值為:{}",value)); }
```
輸出為:
```
當前值為:1
當前值為:3
當前值為:2
當前值為:4
log.info(“當前值為:{}”,value) 就是我們要干的事情,就是 action。
```
2、forEach 方法上打了 @Override 注解,說明該方法是被繼承實現的,該方法是被定義在 Iterable 接口上的,Java 7 和 8 的 ArrayList 都實現了該接口,但我們在 Java 7 的 ArrayList 并沒有發現有實現該方法,編譯器也木有報錯,這個主要是因為 Iterable 接口的 forEach 方法被加上了 default 關鍵字,這個關鍵字只會出現在接口類中,被該關鍵字修飾的方法無需強制要求子類繼承,但需要自己實現默認實現,我們看下源碼:

不僅僅是 forEach 這一個方法是這么干的,List、Set、Map 接口中很多新增的方法都是這么干的,通過 default 關鍵字,可以讓 Java 7 的集合子類無需實現 Java 8 中新增的方法。
如果想在接口中新增一個方法,但又不想子類強制實現該方法時,可以給該方法加上 default 關鍵字,這個在實際工作中,也經常使用到,算是重構的小技巧吧。
#### 1.2 List 區別
#### 1.2.1 ArrayList
1. ArrayList 無參初始化時,Java 7 是直接初始化 10 的大小,Java 8 去掉了這個邏輯,初始化時是空數組,在第一次 add 時才開始按照 10 進行擴容,下圖是源碼的差異對比圖:

2. List 其它方面 java7 和 8 并沒有改動。
#### 1.3 Map 區別
#### 1.3.1 HashMap
1. 和 ArrayList 一樣,Java 8 中 HashMap 在無參構造器中,丟棄了 Java 7 中直接把數組初始化 16 的做法,而是采用在第一次新增的時候,才開始擴容數組大小;
2. hash 算法計算公式不同,Java 8 的 hash 算法更加簡單,代碼更加簡潔;
3. Java 8 的 HashMap 增加了紅黑樹的數據結構,這個是 Java 7 中沒有的,Java 7 只有數組 + 鏈表的結構,Java 8 中提出了數組 + 鏈表 + 紅黑樹的結構,一般 key 是 Java 的 API 時,比如說 String 這些 hashcode 實現很好的 API,很少出現鏈表轉化成紅黑樹的情況,因為 String這些API 的 hash 算法夠好了,只有當 key 是我們自定義的類,而且我們覆寫的 hashcode 算法非常糟糕時,才會真正使用到紅黑樹,提高我們的檢索速度。
*****
也是因為 Java 8 新增了紅黑樹,所以幾乎所有操作數組的方法的實現,都發生了變動,比如說 put、remove 等操作,可以說 Java 8 的 HashMap 幾乎重寫了一遍,所以 Java 7 的很多問題都被 Java 8 解決了,比如擴容時極小概率死鎖,丟失數據等等。
*****
4. 新增了一些好用的方法,比如 getOrDefault,我們看下源碼,非常簡單:
```
// 如果 key 對應的值不存在,返回期望的默認值 defaultValue public V getOrDefault(Object key, V defaultValue) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? defaultValue : e.value; }
```
還有 putIfAbsent(K key, V value) 方法,意思是,如果 map 中存在 key 了,那么 value 就不會覆蓋,如果不存在 key ,新增成功。
還有 compute 方法,意思是允許我們把 key 和 value 的值進行計算后,再 put 到 map 中,為防止 key 值不存在造成未知錯誤,map 還提供了 computeIfPresent 方法,表示只有在 key 存在的時候,才執行計算,demo 如下:
```
@Test public void compute(){ HashMap<Integer,Integer> map = Maps.newHashMap(); map.put(10,10); log.info("compute 之前值為:{}",map.get(10)); map.compute(10,(key,value) -> key * value); log.info("compute 之后值為:{}",map.get(10)); // 還原測試值 map.put(10,10); // 如果為 11 的 key 不存在的話,需要注意 value 為空的情況,下面這行代碼就會報空指針 // map.compute(11,(key,value) -> key * value);
// 為了防止 key 不存在時導致的未知異常,我們一般有兩種辦法 // 1:自己判斷空指針 map.compute(11,(key,value) -> null == value ? null : key * value); // 2:computeIfPresent 方法里面判斷 map.computeIfPresent(11,(key,value) -> key * value); log.info("computeIfPresent 之后值為:{}",map.get(11)); }
```
結果是:
```
compute 之前值為:10
compute 之后值為:100
computeIfPresent 之后值為:null(這個結果中,可以看出,使用 computeIfPresent 避免了空指針)
```
上述 Java 8 新增的幾種方法非常好用,在實際工作中,可以大大減少我們的代碼量,computeIfPresent 的源碼就不貼了,有興趣可以去 github 上面查看,主要的實現原理如下:
* 找到 key 對應的老值,會分別從數組、鏈表、紅黑樹中找;
* 根據 key 和老值進行計算,得到新值;
* 用新值替換掉老值,可能是普通替換、鏈表替換或紅黑樹替換。
#### 1.3.2 LinkedHashMap
由于 Java 8 的底層數據有變動,導致 HashMap 操作數據的方法幾乎重寫,也使 LinkedHashMap 的實現名稱上有所差異,原理上都相同,我們看下面的圖,左邊是 Java 7,右邊是 Java 8。

從圖中,我們發現 LinkedHashMap 的方法名有所修改,底層的實現邏輯其實都差不多的。
#### 1.4 其他區別
#### 1.4.1 Arrays 提供了很多 parallel 開頭的方法。
Java 8 的 Arrays 提供了一些 parallel 開頭的方法,這些方法支持并行的計算,在數據量大的時候,會充分利用 CPU ,提高計算效率,比如說 parallelSort 方法,方法底層有判斷,只有數據量大于 8192 時,才會真正走并行的實現,在實際的實驗中,并行計算的確能夠快速的提高計算速度。
#### 1.5 面試題
1. Java 8 在 List、Map 接口上新增了很多方法,為什么 Java 7 中這些接口的實現者不需要強制實現這些方法呢?
答:主要是因為這些新增的方法被 default 關鍵字修飾了,default 一旦修飾接口上的方法,我們需要在接口的方法中寫默認實現,并且子類無需強制實現這些方法,所以 Java 7 接口的實現者無需感知。
2. Java 8 中有新增很多實用的方法,你在平時工作中有使用過么?
答:有的,比如說 getOrDefault、putIfAbsent、computeIfPresent 方法等等,具體使用細節參考上文。
3. 說說 computeIfPresent 方法的使用姿勢?
答:computeIfPresent 是可以對 key 和 value 進行計算后,把計算的結果重新賦值給 key,并且如果 key 不存在時,不會報空指針,會返回 null 值。
4. Java 8 集合新增了 forEach 方法,和普通的 for 循環有啥不同?
答:新增的 forEach 方法的入參是函數式的接口,比如說 Consumer 和 BiConsumer,這樣子做的好處就是封裝了 for 循環的代碼,讓使用者只需關注實現每次循環的業務邏輯,簡化了重復的 for 循環代碼,使代碼更加簡潔,普通的 for 循環,每次都需要寫重復的 for 循環代碼,forEach 把這種重復的計算邏輯吃掉了,使用起來更加方便。
5. HashMap 8 和 7 有啥區別?
答:HashMap 8 和 7 的差別太大了,新增了紅黑樹,修改了底層數據邏輯,修改了 hash 算法,幾乎所有底層數組變動的方法都重寫了一遍,可以說 Java 8 的 HashMap 幾乎重新了一遍。
### 總結
總體來說,List 方面是小改動,HashMap 幾乎重寫了一套,所有的集合都新增了函數式的方法,比如說 forEach,也新增了很多好用的函數,比如說 getOrDefault,這些函數可以大大減少我們的代碼量,讓我們把關注點聚焦在業務邏輯的實現上,這其實是一種思想,把繁瑣重復的計算邏輯抽取出來,從計算邏輯中擴展出業務邏輯的口子,讓使用者只專心關注業務邏輯的實現即可。
想要了解更多差異,也可直接前往 JDK 8 新特性查看,地址為:http://openjdk.java.net/projects/jdk8/features#103。
- 前言
- 第1章 基礎
- 01 開篇詞:為什么學習本專欄
- 02 String、Long 源碼解析和面試題
- 03 Java 常用關鍵字理解
- 04 Arrays、Collections、Objects 常用方法源碼解析
- 第2章 集合
- 05 ArrayList 源碼解析和設計思路
- 06 LinkedList 源碼解析
- 07 List 源碼會問哪些面試題
- 08 HashMap 源碼解析
- 09 TreeMap 和 LinkedHashMap 核心源碼解析
- 10 Map源碼會問哪些面試題
- 11 HashSet、TreeSet 源碼解析
- 12 彰顯細節:看集合源碼對我們實際工作的幫助和應用
- 13 差異對比:集合在 Java 7 和 8 有何不同和改進
- 14 簡化工作:Guava Lists Maps 實際工作運用和源碼
- 第3章 并發集合類
- 15 CopyOnWriteArrayList 源碼解析和設計思路
- 16 ConcurrentHashMap 源碼解析和設計思路
- 17 并發 List、Map源碼面試題
- 18 場景集合:并發 List、Map的應用場景
- 第4章 隊列
- 19 LinkedBlockingQueue 源碼解析
- 20 SynchronousQueue 源碼解析
- 21 DelayQueue 源碼解析
- 22 ArrayBlockingQueue 源碼解析
- 23 隊列在源碼方面的面試題
- 24 舉一反三:隊列在 Java 其它源碼中的應用
- 25 整體設計:隊列設計思想、工作中使用場景
- 26 驚嘆面試官:由淺入深手寫隊列
- 第5章 線程
- 27 Thread 源碼解析
- 28 Future、ExecutorService 源碼解析
- 29 押寶線程源碼面試題
- 第6章 鎖
- 30 AbstractQueuedSynchronizer 源碼解析(上)
- 31 AbstractQueuedSynchronizer 源碼解析(下)
- 32 ReentrantLock 源碼解析
- 33 CountDownLatch、Atomic 等其它源碼解析
- 34 只求問倒:連環相扣系列鎖面試題
- 35 經驗總結:各種鎖在工作中使用場景和細節
- 36 從容不迫:重寫鎖的設計結構和細節
- 第7章 線程池
- 37 ThreadPoolExecutor 源碼解析
- 38 線程池源碼面試題
- 39 經驗總結:不同場景,如何使用線程池
- 40 打動面試官:線程池流程編排中的運用實戰
- 第8章 Lambda 流
- 41 突破難點:如何看 Lambda 源碼
- 42 常用的 Lambda 表達式使用場景解析和應用
- 第9章 其他
- 43 ThreadLocal 源碼解析
- 44 場景實戰:ThreadLocal 在上下文傳值場景下的實踐
- 45 Socket 源碼及面試題
- 46 ServerSocket 源碼及面試題
- 47 工作實戰:Socket 結合線程池的使用
- 第10章 專欄總結
- 48 一起看過的 Java 源碼和面試真題