Java 8 學習筆記
整理自《Java8實戰》一書
斷斷續續半年沒有更新了,每次只能寫一點然後保存爲草稿。因爲一直在忙着寫畢業論文,過幾個月又要上班了,所以趁着這幾個月多更新一點基礎,等工作了就多更新一些應用遇到的問題。
1.變化:函數、流、默認方法、模式匹配、避免空指針等
2.Collection主要是爲了存儲和訪問數據,Stream主要用於描述對數據的計算
3.lambda表達式
基本語法:(parameters) -> expression 或者是 (parameters) -> {statement;}
當是表達式的時候不能加花括號;當是語句的時候要加上花括號
主要作用於函數式接口:只定義一個抽象方法的接口,默認方法不計入
lambda表達式允許直接以內聯的形式爲函數式接口的抽象方法提供實現,並把整個表達式作爲函數式接口的實例。匿名內部類也可以實現,只是要寫很多樣板代碼。
private static void test2(){
//Java8以前的寫法,匿名類
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("r1");
}
};
//Java8之後的寫法,lambda表達式
Runnable r2 = () -> {
System.out.println("r2");
};
test2_1(r1);
test2_1(r2);
test2_1(()->{System.out.println("r3");});
}
private static void test2_1(Runnable r){
r.run();
}
註解@FunctionalInterface用來表示函數式接口
類型檢查:根據lambda的上下文推斷出來
lambda表達式捕獲局部變量同匿名類一樣,需要聲明爲final類型
方法引用:使用 類名::方法名 來表示
方法引用就是根據已有的方法創建lambda表達式
三種形式:指向靜態方法的方法應用、指向任意類型實例方法的方法引用、指向現有對象的實例方法的方法引用
方法引用就是替代那些轉發參數的Lambda表達式的語法糖
4.流
定義:從支持數據處理操作的源生成的元素序列
粗略地說,流和集合的差異在於什麼時候進行計算。流就像一個延遲創建的集合,只有在消費者要求的時候纔會計算值
流是內部迭代,集合是外部迭代
<注意>流只能消費一次,遍歷一次之後需要重新獲取流
流操作分爲兩種:可以連接起來的流操作稱爲中間操作,關閉流的操作稱爲終端操作
中間操作:除非流水線上觸發一個終端操作,否則中間操作不會執行任何處理
5.使用流
(1)篩選和切片:在轉換爲流之後,如下操作都爲中間操作
方法名 | 作用 |
---|---|
filter | 接受一個謂詞,並根據謂詞結果返回結果爲true的流 |
distinct | 篩選不同的元素,不需要傳參 |
limit | 截斷流,傳遞一個數字參數n,表示只保留前n個數據 |
skip | 跳過元素,傳遞一個數字參數n,表示跳過前n個元素,如果元素不足n個,則返回空流 |
(2)映射
方法名 | 作用 |
---|---|
map | 接收一個函數,應用到每一個元素上 |
flatmap | 同樣的,只是將map的不同的流映射爲一個流,最終生成一個流 |
(3)查找和匹配
方法名 | 作用 |
---|---|
anyMatch | 傳遞謂詞,流中任意一個元素匹配,終端操作 |
allMatch | 傳遞謂詞,流中所有元素匹配,終端操作 |
noneMatch | 傳遞謂詞,流中沒有元素匹配,終端操作 |
findAny | 無須傳參,返回流中任意元素,返回類型Optional |
findFirst | 無須傳參,返回流中第一個元素,返回類型Optional |
(4)歸約(摺疊)
方法名 | 作用 |
---|---|
reduce | 把流中的元素組合起來 |
流的操作分爲無狀態和有狀態,諸如map/filter等從流中獲取一個元素並放到刷出流中沒有內部狀態的稱爲無狀態;諸如reduce/max/sum等需要內部狀態來積累結果的則爲有狀態。
(5)數值流
提供了原始類型流特化,即Integer轉爲int等。
提供三個IntStream、DoubleStream和LongStream
這些特化的原因並不在於流的複雜性,而是裝箱造成的複雜性
映射到數值流,只需要調用mapToInt等方法就可以映射爲IntStream流,在調用該流新添加的sum等方法進行數值操作;反過來講數值流轉換爲普通流,只需要調用boxed方法即可。
IntStream intStream = a.stream().mapToInt(A::getValue());
Stream<Integer> stream = intStream.boxed();
同樣的,對於數值操作,max等會返回OptionalInt類型
對於提供的range和rangeClosed函數,都需要提供起始和終止參數;對於range來講不包含終止參數
(6)創建流
方法名 | 作用 |
---|---|
Stream.of | 靜態方法,接收任意數量參數 |
Arrays.stream | 靜態方法,接收一個數組 |
Files.lines | 靜態方法,傳遞文件,返回文件中每一行的流 |
Stream.iterate | 迭代,依次遞推,是有序的 |
Stream.generate | 生成,不是依次對值應用函數 |
6.用流收集數據
收集器:對流調用collect方法將會對流中的元素觸發一個歸約操作
一般來講,Collectors類提供的工廠方法能適用大多數場景
(1)歸約和彙總
方法名 | 作用 |
---|---|
maxBy/minBy | 獲取最大值和最小值 |
summingInt等 | 求和 |
averagingInt等 | 求平均值 |
summarizingInt等 | 返回IntSummaryStatistics對象,包含count/sum/average等屬性 |
joining | 連接字符串,內部使用StringBuilder;重載版本可以傳遞分隔符 |
上述討論的收集器,實際上可以通過Collectors.reducing工廠方法來實現,是特殊情況
一般情況下的收集器,可以利用Collectors中的reducing方法來實現
reducing方法主要分爲兩個重載版本
方法名 | 作用 |
---|---|
reducing(a,b,c) | a表示起始值,b表示選擇的屬性,c爲lambda,是一個二元操作,最後返回一個結果 |
reducing(a) | a爲lambda,爲一個二元操作,最後返回Optional對象 |
那Stream裏面的reduce和collect又有什麼區別?
reduce旨在把兩個值結合起來生成一個新值,是不可變的歸約,並且無法並行執行;
collect的設計就是要改變容器,從而累計要輸出的結果,適合並行操作,適合表達可變容器的歸約
(2)分組
groupingBy方法,第一個參數爲類型,第二個參數接收Collector類型參數;如果只傳遞一個參數,則第二個參數默認爲toList()
groupingBy收集器只有在應用分組條件後,第一次在流中找到某個鍵對應的元素時纔會把鍵加入到分組Map中
把收集器的結果轉換爲另一種類型,使用collectingAndThen方法,其接收兩個參數,分別爲要轉換的收集器以及轉換函數,並返回另一個收集器
mapping方法也可以和groupingBy聯合,其接收兩個參數,一個函數對流中的元素進行變換,另一個則將變換的結果對象收集起來
最後收集的容器類型,可以通過collect(toCollection,HashSet::new)來控制
(3)分區
分區是分組的特殊情況,由一個謂詞(返回布爾的函數)作爲分類函數,它被稱爲分區函數;因此該方法得到的Map的鍵爲Boolean類型
partitioningBy方法只接收一個謂詞,與groupingBy方法類似,同樣可以接收兩個參數
(4)收集器接口
Collector接口一共聲明瞭五個方法
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
Function<A, R> finisher();
BinaryOperator<A> combiner();
Set<Characteristics> characteristics();
}
//T是流中要收集的項目的泛型
//A是累加器的類型,累加器是在收集過程中用於累積部分結果的對象
//R是收集操作得到的對象(通常但並不一定是集合)的類型
方法名 | 作用 |
---|---|
supplier | 該方法必須返回一個結果爲空的Supplier,也就是一個無參數函數,在調用時它會創建一個空的累加器實例,供數據收集過程使用 |
accumulator | 該方法返回執行歸約操作的函數,該函數執行到第n個元素時,有保留歸約結果的累加器和第n個元素。該函數返回void,因爲累加器是原位更新,即函數的執行改變了它的內部狀態用來體現遍歷的元素的效果 |
finisher | 遍歷完流後,該方法必須返回在累積過程中的最後要調用的一個函數,以便將累加器對象轉換爲整個集合操作的最終結果 |
combiner | 返回一個供歸約操作使用的函數,定義了流的各個子部分在進行並行處理時,各個子部分歸約所得的累加器的合併方式 |
characteristics | 返回一個不可變的Characteristics集合,定義了收集器的行爲,UNORDERED表示歸約結果不受流中項目的遍歷和累積順序的影響、CONCURRENT表示accumulator可以從多個線程同時調用,如果沒有UNORDERED標記,則僅在用於無序數據源時纔可以並行歸約、IDENTITY_FINISH表示完成器方法返回的函數是一個恆等函數,這種情況下,累加器對象將會直接用作歸約過程的最終結果 |
7.並行數據處理與性能
(1)並行流
通過對收集源調用parallelStream方法來把集合轉換爲並行流。並行流就是一個把內容分成多個數據塊,並用不同的線程分別處理每個數據塊的流。
通過調用parallel方法可以把順序流轉換爲並行流,通過調用sequential方法可以把並行流轉換爲順序流;但是流水線最終會按照最後一次parallel/sequential的調用決定。
並行流使用的線程數量默認爲CPU數量,可以通過如下方式進行更改
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","100")
//該方法是全局設置,將影響所有的並行流
並行化過程本身需要對流做遞歸劃分,把每個子流的歸納操作分配到不同的線程,然後把這些操作的結果合併成一個值。
需要注意的是,共享可變狀態會影響並行流以及並行計算。
那麼如何高效的使用並行流呢?
- 測量性能
- 裝箱問題
- 依賴元素順序的操作在並行流上性能就比順序流差
- 流的操作流水線總計算成本
- 較小數據量不適合並行流
- 流背後的數據結構是否易於分解
- 流的特點、流水線中間操作修改流的方式等都會改變分解過程的性能
- 考慮終端操作中合併步驟的代價大小
下表所示爲流的數據源和可分解性
源 | 可分解性 |
---|---|
ArrayList | 極佳 |
LinkedList | 差 |
IntStream.range | 極佳 |
Stream.iterate | 差 |
HashSet | 好 |
TreeSet | 好 |
(2)分支/合併框架
分支/合併框架的目的是以遞歸方式將可以並行執行的任務拆分成更小的任務,然後將每個子任務的結果合併起來生成整體結果。它是ExecutorService接口的一個實現,把子任務分配給線程池中(ForkJoinPool)的工作線程.
- 對一個任務調用join方法會阻塞調用方,直到該任務做出結果
- 不應該在RecursiveTask內部使用ForkJoinTask的invoke方法,相反應該直接使用compute或者fork方法,只有順序代碼才應該使用invoke來啓動並行運算
- 對子任務調用fork方法可以把它排進ForkJoinPool
該框架使用工作竊取技術來解決任務分配問題。每個線程都爲分配給它的任務保存一個雙向鏈式隊列,每完成一個任務,就會從隊列頭上取出下一個任務開始執行。基於前面所述的原因,某個線程可能早早地完成了分配的任務,此時其線程空而其他線程忙。此時,該線程將隨機選擇一個忙的線程,並從其尾部去任務開始執行。
(3)spliterator
public interface Spliterator<T> {
boolean tryAdvance(Consumer<? super T> action);
Spliterator<T> trySplit();
long estimateSize();
int characteristics();
}
- tryAdvance方法類似於iterator,會按照順序依次使用Spliterator中的元素,如果有其他元素要遍歷則返回true
- trySplit方法用於劃分元素,把一部分元素劃分給第二個Spliterator,使得其並行處理
- estimateSize方法用於估計還有多少元素需要遍歷
- characteristics方法用於表示Spliterator的特性
8.重構、測試和調試
在匿名類中,this指代類自身,再lambda中則指代包含類。
其次,匿名類可以屏蔽包含類中的變量,而lambda則會編譯錯誤。
int a = 10;
Runnable r1 = new Runnable(){
int a = 11;//正常
}
Runnable r2 = () -> {
int a = 11;//編譯錯誤
}
匿名類的類型在初始化時就可以確定,lambda的類型則取決於上下文。
當一個lambda表達式有歧義時,可以利用強制類型轉換來解決(比如兩個都是Runnable一樣的接口)。
9.默認方法
Java 8允許在接口內聲明靜態方法;也可以通過默認方法指定接口方法的默認實現
引入默認方法的目的:讓類自動的繼承接口的一個默認實現
默認方法由default修飾符修飾,並像類中聲明的其他方法一樣包含方法體
函數式接口只包含一個抽象方法,默認方法是一種非抽象方法
那在Java 8中,抽象類和抽象接口的區別在哪?
一個類只能繼承一個抽象類,但是一個類可以實現多個接口
一個抽象類可以通過實例變量保存一個通用狀態,而接口不存在實例變量
兼容性 | 描述 |
---|---|
二進制 | 現有的二進制執行文件能無縫持續鏈接和運行,爲接口添加一個方法就是二進制級的兼容 |
源代碼 | 表示引入變化之後,現有的程序依然能夠成功編譯通過 |
函數行爲 | 表示引入變化之後,程序接收同樣的輸入能得到同樣的結果 |
Java 8中引入的默認方法就是源代碼級別的兼容
解決衝突問題的規則:
- 類中的方法優先級最高。類或者父類中聲明的方法的優先級高於任何聲明爲默認方法的優先級
- 第一條無法判斷時,子接口的優先級更高;函數簽名相同時,優先選擇擁有最具體實現的默認方法的接口
- 以上依舊無法判斷時,繼承了多個接口的類必須通過顯式覆蓋和調用期望的方法,顯式的選擇使用哪一個默認方法的實現
顯式調用默認方法的方式:X.super.func();其中X爲對應的父接口
10.用Optional取代null
爲了更好地解決null問題,提出了java.util.Optional<T>類
方法 | 描述 |
---|---|
empty | 返回一個空的Optional實例 |
filter | 如果值存在且滿足條件,則返回包含該值的Optional;否則返回空的Optional |
flatMap | 如果值存在,就調用對應的mapping方法並返回Optional類型的值,否則返回空的Optional |
get | 如果值存在則返回包含該值的Optional,否則拋出NoSuchElementException異常 |
ifPresent | 如果值存在則執行使用該值的方法調用,否則什麼都不做 |
isPresent | 如果值存在就返回true,否則返回false |
map | 如果值存在,則調用mapping方法 |
of | 將指定值包裝爲Optional返回,如果爲null則拋出NullPointerException |
ofNullable | 將指定值包裝爲Optional返回,如果爲null返回空Optional |
orElse | 如果有值則返回,否則返回一個默認值 |
orElseGet | 如果有值則返回,否則返回由Supplier生成的值 |
orElseThrow | 如果有值則返回,否則返回由Supplier生成的異常 |
需要注意的是。Optional並沒有實現Serializable接口,所以無法實現序列化和反序列化
11.CompletableFuture組合式異步編程
通過工廠方法CompletableFuture.supplyAsync創建相應對象,該方法接收一個生產者Supplier作爲參數,返回一個CompletableFuture對象,該對象完成異步執行後會讀取調用生產者方法的返回值。生產者方法會交由ForkJoinPool池中的某個執行線程運行,同時重載版本第二個參數可以指定線程
進行計算密集型操作時,推薦使用stream編程;當並行的工作單元還涉及到IO操作時,使用CompletableFuture效果更好。
針對工廠方法,Async結尾的方法會將後續的任務提交到一個線程池;沒有Async結尾的方法會和前一個方法在同一個線程中運行
thenApply執行CompletableFuture的同步操作
thenCompose允許對兩個異步操作進行流水線操作,當第一個操作完成時,將其結果作爲參數傳遞給第二個操作
thenCombine允許將兩個完全不相關的異步操作整合起來,其第二個參數爲BiFunction,定義了當兩個CompletableFuture對象完成計算後的合併操作
thenAccpet定義如何處理CompletableFuture返回的結果,一旦CompletableFuture計算得到結果,就返回一個CompletableFuture<Void>
12.新的日期和時間API
java.time是Java 8 提供的日期相關的包
LocalDate、LocalTime、LocalDateTime、Instant、Duration、Period等類提供了不同的時間表示方法
其他部分:超越Java 8
略
主要內容就是以上所寫的內容。這裏都是複述,並沒有一些思考在裏面。還需要多一些實踐操作纔可以。