JDK1.8新特性中最值得注意的當然就是Lambda表達式了,還有新增了很多關于Lambda表達式的新特性,比如函數式接口、方法引用和構造器調用。處理之外還有Stream API、接口中的默認方法和靜態方法、新的時間日期API,HashMap以及ConcurrentHashMap結構的更改等等。
1、Lambda表達式
(1)什么是Lambda表達式呢?Lambda表達式本質上就是一段匿名內部類,也可以是一段可以傳遞的代碼。
語法如下:
(2)Lambda表達式到底方便在哪里?舉例說明
我們發現實際上這些過濾方法的核心就只有if語句中的條件判斷,其他均為模版代碼,每次變更一下需求,都需要新增一個方法,然后復制黏貼,假設這個過濾方法有幾百行,那么這樣的做法難免笨拙了一點。如何進行優化呢?
a.使用設計模式
看起來好像代碼更多了一點,不過這是在只有兩個例子的情況下,如果有100個過濾那么會發現還是設計模式比較簡潔一點,而且設計模式好在的地方是對代碼進行了整合,看起來邏輯更簡潔明了。不過對于程序員來說,還是要搬運很多代碼,接下來進一步優化
b.使用匿名內部類
匿名內部類:沒有顯示名字的內部類
本質:匿名內部類會隱式得繼承一個類或實現一個接口,或者說,匿名內部類時一個繼承了該類或者實現了該接口的子類匿名對象
格式:new?類名/接口/抽象類(){}
使用匿名內部類,就不需要每次都新建一個實現類,直接在方法內部實現。看起來是不是更簡潔明了一點。
c.使用Lambda表達式
Lambda表達式就是一段更加簡潔明了的匿名內部類。
d.還可以使用Stream API
2、函數式接口
問題一:為什么要新增函數式接口?
答案是為了配合lambda表達式使用,當接口中存在多個抽象方法的時候,單純使用lambda表達式并不能智能匹配對應的抽象方法,因此引入了函數式接口的概念
問題二:什么是函數式接口
概念:只定義了一個抽象方法的接口(Object類的public方法除外),就是函數式接口,并且還提供了注解:@FunctionallInterface
理解:首先要理解什么是函數,這里的函數跟編程中的函數并不一樣,而是與數學中函數的概念一樣——一種映射的關系,接下來看看分類就就很明了了。
常見的四大函數式接口:
a.Consumer<T>:消費型接口,有參無返回值
b.Supplier<T>:供給型接口,無參有返回值
c.Function<T,R>:函數式接口,有參有返回值
d.Predicate<T>:斷言型接口,有參有返回值,返回值是boolean類型
在四大核心函數式接口基礎上,還提供了諸如BiFunction、BinaryOperation、toIntFunction等擴展式的函數式接口,都是在這四種函數式接口上擴展而來的
三、方法引用和構造器調用
方法引用和構造器調用是在基于Lambda表達式的基礎之上的:若lambda體中的內容有方法已經實現了,那么可以使用"方法引用"。可以理解為方法引用是lambda表達式的另外一種表現形式并且其語法比lambda表達式更加簡單
1、方法引用
三種表現形式:
a.對象::實例方法名
b.類::靜態方法名
c.類::實例方法名(lambda參數列表中的第一個參數是實例方法的調用這,第二個參數是實例方法時可用)
2、構造器調用
格式:ClassName::new
還有一種數組引用
格式:Type[]::new
4、Stream API
Stream操作的三個步驟:
(1)創建stream
(2)中間操作(過濾、map)
(3)終止操作
還有功能比較強大的兩個終止操作 reduce和collect
reduce操作: reduce:(T identity,BinaryOperator)/reduce(BinaryOperator)-可以將流中元素反復結合起來,得到一個值
并行流和串行流:在jdk1.8新的stream包中針對集合的操作也提供了并行操作流和串行操作流。并行流就是把內容切割成多個數據塊,并且使用多個線程分別處理每個數據塊的內容。Stream api中聲明可以通過parallel()與sequential()方法在并行流和串行流之間進行切換。
jkd1.8并行流使用的是fork/join框架進行并行操作
ForkJoin框架:在必要的情況下,將一個大任務,進行拆分(fork)成若干個小任務(拆到不可再拆時),再將一個個的小任務運算的結果進行join匯總
關鍵:遞歸分合、分而治之
采用”工作竊取“工作模式(work-stealing):當執行新的任務時,它可以將其拆分成更小的任務執行,并將小任務加到線程隊列中,然后再從一個隨機線程的隊列中偷一個并把它放到自己的隊列中
相對于一般的線程池實現,fork/join框架的優勢體現在對其包含的任務的處理方式上
一般的線程池中,如果一個線程正在執行的任務由于某些原因無法繼續,那么該線程會處于等待狀態
fork/join框架實現中,如果某個子問題由于等待另外一個子問題的完成而無法繼續運行,那么處理該子問題的線程會主動尋找其它尚未運行的子問題來執行,這個方式減少了線程的等待時間,提高了性能
五、接口中的默認方法和靜態方法
在接口中可以使用default和static關鍵字來修飾接口中定義的普通方法
在JDK1.8中很多接口會新增方法,為了保證1.8向下兼容,1.7版本中的接口實現類不用每個都重新實現新添加的接口方法,引入了default默認實現,static的用法是直接用借口名去調方法既可。當一個類繼承父類又實現接口時,若后兩者方法名相同,則有限繼承父類中的同名方法,即”類優先“,如果實現兩個同名方法的接口,則要求實現類必須手動聲明默認實現哪個接口中的方法
六、新的時間日期API
1、使用過時間日期API的都知道,之前使用的java.util.Date月份從0開始,我們一般會+1使用,很不方便,新的java.time.LocalDate月份和星期都改成了enum;
2、java.util.Date和SimpleDateFormat都不是線程安全的,而LocalDate和LocalTime和最基本的String一樣,是不變類型,不但線程安全,而且不能修改。
之前SimpleDateFormat多線程環境下一般都是配合ThreadLocal使用,新的LocalDate顯然更加簡潔明了。
3、java.util.Date是一個“萬能接口”,它包含日期、時間,還有毫秒數,更加明確需求取舍
4、新接口更好用的原因是考慮到了日期時間的操作,經常發生往前推或往后推幾天的情況。用java.util.Date配合Calendar要寫好多代碼,而且一般的開發人員還不一定能寫對。
七、HashMap、ConcurrentHashMap數據結構優化(面試重點)
面試問題:你能說說HashMap/ConcurrentHashMap的結構嗎?
(一)JDK1.8之前
hashMap采用的是哈希表(數組+鏈表)
ConcurrentHashMap采用的是哈希表(數組+鏈表),通過分段鎖機制來實現線程安全。
1、hashMap默認大小16,一個0-15索引的數組。
2、存儲方式:
(1)首先調用元素的hashcode方法計算出哈希碼值,經過哈希算法算成數組的索引值;
(2)如果對應的索引處沒有元素,直接存放,如果有對象在,那么比較它們的equals方法比較內容
a.如果內容一樣,后一個value值會將前一個value值覆蓋
b.如果不一樣,后加的會放在前面,形成一個鏈表,形成了碰撞
3、缺點:加載因子:0.75,數組擴容,達到總容量的75%,就進行擴容,但是無法避免碰撞的情況發生
(二)JDK1.8之后
hashMap采用的是數組+鏈表+紅黑樹
ConcurrentHashMap采用的是數組+鏈表+紅黑樹,通過CAS+Synchronized來實現線程安全
1、當碰撞的個數>8時&&總容量>64,會有紅黑樹的引入,除了添加之后,效率都比鏈表高
2、鏈表新進元素加到末尾
3、ConcurrentHashMap(鎖分段機制),concurrentLevel,JDK1.8采用CAS算法(無鎖算法,不再使用鎖分段)
4、紅黑樹(自平衡的二叉查找樹,基于二叉查找樹、完美平衡二叉樹)
問題二:HashMap為什么不是線程安全的?
答:HashMap中的變量沒有用volatile關鍵字修飾來保證可見性和有序性,也沒有用任何鎖來保證原子性,在多線程運行的環境下,可能會產生并發問題,如多個線程同時插入時,可能會導致閉環。
問題三:ConcurrentHashMap怎么保證線程安全?
答:JDK1.7中,ConcurrentHashMap采用鎖分段機制來保證線程安全同時提高并發性;JDK1.8中,ConcurrentHashMap采用的是CAS加上Synchronized來保證線程安全,當沒有發生沖突的使用使用CAS來保證線程安全,當發生沖突的時候用Synchronized來鎖住。
PS:有關鎖可以參考我之前寫的JUC之Locks鎖面經整理