一、JAVA8 說明
- java8功能的基礎就是讓方法可以成爲值,即讓方法變成了一等值,把代碼傳遞給方法的簡潔方式(方法引用、Lambda)和接口中的默認方法。
- java 8對於程序員的主要好處在於它提供了更多的編程工具和概念,能以更快,更重要的是能以更簡潔、更易於維護的方式解決新的或者現有的編程問題。三個新編程概念,流處理、用行爲參數話把代碼傳遞給方法、並行與共享的可變數據
- 行爲參數化& 函數式編程
讓方法接受多種行爲或者戰略作爲參數,並在內部使用,來完成不同的行爲(將代碼傳遞給方法的功能,即函數式編程)。行爲參數化可以讓代碼更好地適應不斷變化的要求,並且可以推遲參數化代碼的執行。但在java8之前實現起來很囉嗦。會有很多爲接口聲明許多隻用一次的實體類。造成的囉嗦代碼,在java8 以前可以用匿名類來解決。 - java8中加入默認方法的原因
默認方法給接口設計者提供了一個擴充接口的方式,而不會破壞現有的代碼。用default關鍵字來表示這一點,主要是爲了支持庫設計師,讓他們能夠寫出更容易改進的接口,但由於真正需要編寫默認方法的程序員相對較少,而且他們只是有助於程序改進,而不是用於編寫任何具體的程序。 - java.util.stream流的優點
可以在一個更高的抽象層次上寫Java8代碼了,思路變成了把這樣的流變成那樣的流,而不是一次只處理一個項目。
java8 可以透明地把輸入的不相關的部分拿到幾個CPU內核上去分別執行stream操作流水線,用不着在去搞thread了 。 - Collection和stream的區別
Collection主要是爲了儲存和訪問數據,而stream主要用於描述對數據的計算。stream允許並且提倡並行處理一個stream中的元素
二、Lambda
-
lambda的四個特點 & 爲什麼要用Lambda
1. 匿名:因爲它不像普通方法那樣有明確的名稱 2. 函數:它不像方法那樣屬於某個特定的類。但和方法一樣有參數列表,返回類型,還有可能有可以拋出的異常列表 3. 傳遞:可以作爲參數傳遞給方法或存儲在變量當中 4. 簡潔:不用想匿名類那樣寫很多模板代碼
-
函數式接口
函數式接口就是隻定義一個抽象方法的接口,默認方法和靜態方法除外 -
函數描述符
函數式接口的抽象方法的簽名基本上就是Lambda表達式的簽名。我們將這種抽象方法叫做函數描述符 -
Lambda如何做類型檢查&類型推斷
Lambda的類型是從使用Lambda的上下文推斷出來的。上下文(比如,接受它傳遞的方法的參數,或接受它的值的局部變量)中Lambda表達式需要的類型稱爲目標類型 -
Java8 提供的基礎的4個函數式接口
Predicate接口:有個方法,接受一個參數,返回boolean類型 @FunctionalInterface public interface Predicate<T>{ boolean test(T t); } Consumer接口:有個方法,接受一個參數,返回void @FunctionalInterface public interface Consumer<T>{ void accept(T t); } Function接口:有個方法,接受一個T類型的參數,返回R類型的對象 @FunctionalInterface public interface Function<T, R>{ R apply(T t); } Supplier接口:有個方法,沒有參數,返回T類型對象 @FunctionalInterface public interface Supplier<T> { T get(); }
-
4個函數式接口的原始類型的特化爲什麼需要:
因爲原始類型裝箱是在性能方面是要付出代價的。裝箱後的本質就是把原始類型包裹起來,並且存在堆裏。因此,裝箱後的值需要更多的內存,並需要額外的內存搜索來獲取被包裹的原始值 -
請注意: 任何函數式接口都不允許拋出受檢異常(checked exception)。如果你需要Lambda表達式來拋出異常,有兩種辦法:定義一個自己的函數式接口,並聲明受檢異常,或者把Lambda包在一個try/catch塊中。
-
Lambda表達式允許使用自由變量(不是參數,而是在外層作用域中定義的變量)以及對局部變量使用的限制
允許。就像匿名類一樣。 它們被稱作捕獲Lambda。
實例變量和局部變量背後的實現有一個關鍵不同。實例變量都存儲在堆中,而局部變量則保存在棧上。如果Lambda可以直接訪問局部變量,而且Lambda是在一個線程中使用的,則使用Lambda的線程,可能在分配該變量的線程將這條個變量回收之後,去訪問該變量。因此,Java在訪問自由變量時,實際上是在訪問它的副本,而不是訪問原始變量。如果局部變量僅僅賦值一次那就沒什麼區別了。 -
方法引用
基本思想:如果一個Lambda代表的只是"直接調用這個方法",最好還是用名稱來調用它,而不是去描述如何調用它。事實上,方法引用就是讓你根據已有的方法來實現創建Lambda表達式。但是,顯示地指明方法的名稱,代碼可讀寫性更好 -
方法引用的分類:
靜態方法引用
實例方法引用:
指向任意類型實例方法 的方法引用(例如 String 的 length 方法,寫作String::length)。
指向現有對象的實例方法的方法引用(假設你有一個局部變量expensiveTransaction
用於存放Transaction類型的對象,它支持實例方法getValue,那麼你就可以寫expensiveTransaction::getValue)。
構造方法引用
三、stream 流
- Collector接口
//T 是流中要收集對項目的範型,A是累加器的類型,累加器是在收集過程中用於積累部分結果的對象,R是收集操作得到的對象
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
BinaryOperator<A> combiner();
Function<A, R> finisher();
Set<Characteristics> characteristics();
}
- 自定義Collector接口實現
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
/**
* Created by snow_fgy on 2020/4/19.
*/
public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
//調用是會返回一個空的累加器實例,供收集過程中使用
@Override
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
/**
* 會執行規約操作的函數。當遍歷到流中第n個元素時,這個函數執行時會有兩個參數:保存歸約結果的累加器(n-1),還有第n個元素
* 本身。改函數將返回void,因爲累加器是原位更新,即函數的執行改變了它的內部狀態以體現遍歷元素的效果。
* 對於該類,這個函數僅僅會把當前項目添加至已經遍歷過的項的列表
*
*
* @return
*/
@Override
public BiConsumer<List<T>, T> accumulator() {
return List::add;
}
/**
* 在遍歷完流後,該方法必須返回在積累過程中的最後要調用的一個函數,以便將積累器對象轉換爲整個集合操作的最終結果。通常,就像該類情況一樣,
* 累加器對象恰好符合預期的最終結果,因此無需進行轉換。
*
*
* @return
*/
@Override
public Function<List<T>, List<T>> finisher() {
return Function.identity();
}
/**
* 供歸約操作使用的函數,它定義了對流中的各個子部分進行並行處理時,各個子部分歸約所得累加器要如何合併。對於該類,只要把從流的第二個部分
* 收集到的項目列表加到遍歷第一部分時得到的列表後面就行了。
* 1. 原始流會以遞歸的方式拆分子流,直到定義流是否需要進一步拆分的一個條件爲非
* 2. 所有的子流都可以進行並行處理
* 3. 最後將使用combiner方法返回的函數,將所有的部分結果兩兩合併
* @return
*/
@Override
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1;
};
}
/**
* 定義了收集器的行爲
* CONCURRENT: accumulator函數可以從多個線程同時調用,且該收集器可以並行歸約流。如果收集器沒有標爲UNORDERED,那麼它僅在用於無序數據源時纔可以進行並行歸約。
*
* UNORDERED: 歸約結果不受流中項目的遍歷和累積順序的影響
*
* IDENTITY_FINISH:這表明完成器方法返回的函數是一個恆等函數,可以跳過,這種情況下,累加器對象將會直接用歸約過程的最終結果。也就意味着,將累加器A不加檢查地轉換爲結果R是安全的
* @return
*/
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH, Characteristics.CONCURRENT));
}
}
-
並行流
並行流就是把一個內容分成多個數據塊,並用不同的線程分別處理每個數據塊的流 -
將順序流轉化爲並行流
對於順序流調用parallel方法並不意味着流本身有任何實際的變化。在內部實際上就是設了一個boolean標誌,表示你想讓調用parallel之後進行的操作都並行執行。 -
配置並行流使用的線程池,parallel方法,使用的線程是從那來的?有多少個?怎麼自定義這個過程?
並行流內部使用了默認的ForkJoinPool,它默認的線程數量就是你的處理器的數量,這個值是由Runtime.getRuntime().available-Processors()得到的。但是你可以通過系統屬性java.util.concurrent.ForkJlomPool.common.parallelism來改變線程池大小
System.setProperties(“java.util.concurrent.ForkJlomPool.common.parallelism”, 12);
這是一個全局設置,因爲它將會影響代碼中的所有並行流。反過來說,目前還無法專爲某個並行流指定這個值。 -
並行流的代價
並行化本身需要對流做遞歸劃分,把每個子流的歸納操作分配到不同的線程,然後把這些操作的結果合併成一個值。但在多個內核之間移動數據的代價也是很大的,所以並行化很重要的一點是要保證在內核中並行執行工作的時間比在內河之間傳輸數據的時間要長。 -
對於高效使用並行流的考慮點
1⃣️用適當的基準來檢查其性能,因爲把順序流轉換爲並行流輕而易舉,但卻不一定是好事。
2⃣️留意裝箱。自動裝箱拆箱操作會大大降低性能。java8 中有原始流來避免這種操作。
3⃣️有些操作本身在並行流上的性能就比在順序流中的差。特別是limit和findFirst等依賴元素順序的操作,他們在並行流上執行的代價非常大。例如findAny會比findFirst性能好,因爲它不一定要按順序來執行。你總是可以調用unordered方法來把有序流變成無序流。那麼如果你需要流中的n個元素而不是專門要前n個的話,對於無序並行流調用limit可能會比單個有序流更高效。
4⃣️還要考慮流的操作流水線的總計算成本。設N是要處理元素的總數,Q是一個元素通過流水線的大致處理成本,則N*Q就是這個對成本的一個粗略的定性估計。Q值高就意味這使用並行流時性能好的可能性就大。
5⃣️對於較小的數據量,選擇並行流幾乎從來都不是一個好的決定。並行流處理少數幾個元素的好處還抵不上並行話造成的額外開銷。
6⃣️要考慮流背後的數據結構是否易於分解。例如ArrayList的拆分效率比LinkedList高的多,因爲前者用不找遍歷就可以平均拆分,而後者就必須遍歷。
7⃣️流自身的特點,以及流水線中的中間操作修改流的方式,都可能改變分解過程的性能。例如,一個sized流可以分成大小相等的兩部分,這樣每個部分都可以比較高效地並行處理,但篩選操作可能丟棄元素的個數卻無法預測,導致流本身的大小未知。
8⃣️還要考慮終端操作中合併步驟的代價是大是小。如果這一步代價很大,那麼組合每個子流所產生的部分結果所付出的代價就可能超出通過並行流得到的性能提升。 -
分支/合併框架
目的是以遞歸的方式將並行的任務拆分成更小的任務,然後把每個子任務的結果合併起來生成整體結果。它是ExecutorService接口的一個實現,它把自任務分配給線程池(ForkJoinPool)。
要把任務提交到這個池,必須創建RecursiveTask的一個子類,其中R是並行化任務產生的結果類型,如果任務不返回結果,則用RecursiveAction類型。 -
使用分支/合併框架的最佳做法
①對於一個任務調用join方法後會阻塞調用方,直到該任務作出結果。因此,有必要在兩個子任務的計算都開始後在調用它。否則你得到的版本會比原始的順序算法更慢更復雜,因爲每個子任務都必須等待另一個子任務完成後才能啓動。
②不應該在recursiveTask內部使用ForkJoin的invoke方法。相反,你應該始終調用compute或者fork方法,只有順序代碼才應該用invoke來啓動並行計算
③調試使用分支/合併框架的並行計算可能有點棘手。特別是你平常都在你喜歡的IDE裏面看棧跟蹤來找問題,但放在分支-合併計算上就不行了,因爲調用compute的線程並不是概念上的調用方,後者是調用fork的那個。
④和並行流一樣,你不應理所當然地認爲在多核處理器上使用分支-合併框架就比順序設計快。
注意對於分支-合併策略必須要選一個標準,來決定任務是要進一步拆分還是已小到可以順序求值。 -
分支/合併框架工程用一種稱爲工作竊取
在實際應用中,任務差不多被平均分配到ForkJoinPool中的所有線程上。每個線程都爲分配給它的任務保存一個雙向鏈式隊列,每完成一個任務,就會從隊列頭上取出下一個任務開始執行。某個線程可能早早完成了分配給它的所有任務,也就是它的隊列已經空了,而其他的線程還很忙。這時,這個線程並沒有閒下來,而是隨機選了一個別的線程,從隊列的尾巴上“偷走”一個任務。這個過程一直繼續下去,直到所有的任務都執行完畢,所有的隊列都清空。這就是爲什麼要劃成許多小任務而不是少數幾個大任務,這有助於更好地在工作線程之間平衡負載。 -
改善代碼的可讀性
①用Lambda表達式取代匿名類
說明:某些情況下,將匿名類轉換爲Lambda表達式可能是一個比較複雜的過程。首先,匿名類和Lambda表達式中的this和super的含義是不同的。在匿名類中,this代表的是類自身,但是在Lambda中,它代表的是包含類。其次,匿名類可以屏蔽包含類的變量,而Lambda表達式不能(它們會導致編譯錯誤)。在涉及重載的上下文裏,將匿名類轉爲Lambda表達式可能導致最終的代碼更加晦澀。實際上,匿名類的類型是在初始化時確定的,而Lambda的類型取決於它的上下文。
②用方法引用重構Lambda表達式
③用stream API 重構命令式的處理 -
默認方法
默認方法的引入就是爲了以兼容的方式解決像Java API這樣的類庫的演進問題的,它讓類可以自動地繼承接口的一個默認實現。 -
那麼抽象類和抽象接口之間的區別是什麼呢?它們不都能包含抽象方法和包含方法體的實現嗎?
首先,一個類只能繼承一個抽象類,但是一個類可以實現多個接口。其次,一個抽象類可以通過實例變量(字段)保存一個通用狀態,而接口是不能有實例變量的。 -
解決默認方法衝突的規則
(1) 類中的方法優先級最高。類或父類中聲明的方法的優先級高於任何聲明爲默認方法的優先級。
(2) 如果無法依據第一條進行判斷,那麼子接口的優先級更高:函數簽名相同時,優先選擇擁有最具體實現的默認方法的接口,即如果B繼承了A,那麼B就比A更加具體。
(3) 最後,如果還是無法判斷,繼承了多個接口的類必須通過顯式覆蓋和調用期望的方法,顯式地選擇使用哪一個默認方法的實現
四、Optional
-
null帶來的種種問題
- 它是錯誤之源。NullPointerException是目前java開發中最典型的異常
- 它會使你的代碼膨脹。讓你的代碼充斥着深度嵌套的null檢查,代碼的可讀性糟糕透頂。
- 它自設毫無意義,尤其是它代表的是在靜態語言中以一種錯誤的方式對缺失變量值的建模
- 它破壞了java的哲學。Java一直試圖避免讓程序員意識到指針的存在,唯一的例外是:null指針。
- 它在Java的類型系統上開了個口子。null不屬於任何一種類型,這意味着它可以被賦值給任意引用類型的變量。這會導致問題,原因是但這個變量被傳遞到系統中的另一個部分後,你將無法獲知這個null變量最初的賦值到底是個什麼類型
-
Optional的設計初衷僅僅是要支持能返回Optional對象的語法,沒特別考慮將其作爲類的字段使用,所以它並未實現Serializable接口。由於這個原因,如果你的應用使用了某些要求序列化的庫或者框架,在域模型中使用Optional時可能會引發應用程序故障。
-
基礎類型的Optional對象,我們應該避免使用它們
因爲基礎類型的Optional對象不支持map,flatmap已經filter方法,而這些方法卻都是Optional類最有用的方法。此外,與stream一樣,Optional對象無法由基礎類型的Optional最合構成。
五、其他
- future接口概述
它在java5中引用,設計初衷是對將來某個時刻會發生的結果進行建模。它建模了一種異步計算,返回一個執行運算結果的引用,當運算結束後,這個引用被返回給調用方。在Future中觸發某些潛在耗時的操作把調用的線程解放出來,讓它能繼續執行其他有價值的工作,不再需要呆呆等待耗時的操作完成。 - 同步API
其實是對傳統方法調用的另一種稱呼:你調用了某個方法,調用方在被調用方運行的過程中等待,被調用方運行結束返回,調用方取得被調用方的返回值並繼續運行。即使調用方和被調用方在不同的線程中運行,調用方還是等待被調用方結束運行。這就是阻塞示例。 - 異步API
異步API會直接返回,或者至少在被調用方計算完成之前,將它剩餘的計算任務交給另一個線程去做,該線程和調用方法是異步的------這就是非阻塞式調用的由來。執行剩餘計算任務的線程會將它的計算結果返回給調用方。返回的方式要麼是通過回調函數。要麼是由調用方在次執行一個“等待,直到計算完成”的方法調用。 - Future執行完畢可以發送一個通知,僅在計算結果可用時執行一個有Lambda表達式或者方法引用定義的回調函數
- CompletableFutre 在一個線程內執行計算任務時,如果發生異常,並且不用其對象方法completeExceptionally()方法拋出異常,則調用方會一直阻塞下去。
- 使用工廠方法supplyAsync創建CompletableFuture
這個方法接受一個生產者(Supplier)作爲參數,返回一個CompletableFuture對象,該對象完成異步執行後會調用生產者方法的返回值。生產者方法會交由ForkJoinPool池中的某個執行線程運行,但是也可以採用這個方法的重載方法,傳第二個參數指定不同的執行線程執行生產者方法。 - 如果一個方法即不修改它內嵌類的狀態,也不修改其他對象的狀態,使用return返回所有的計算結果,那麼我們稱其爲純粹的或者無副作用的。
- 會造成副作用的情況?
1⃣️除了構造器內的初始化操作,對類中數據結構的任何修改,包括對字段的賦值(一個典型的例子:setter方法)
2⃣️拋出個異常
3⃣️進行輸入/輸出操作,比如向一個文件寫數據 - 像這種把最終的實現查詢的細節留給函數庫,我們把這種思想叫做內部迭代。它的巨大優勢在於你的查詢語句現在讀起來就像是問題陳述,由於採用了這種方式,我們馬上就能理解它的功能。採用這用‘要做什麼’風格的編程通常被稱爲聲明式編程。
10.函數無論在何處、何時調用,如果使用同樣的輸入總能持續地得到相同的結果,就具備了函數式的特徵。