引子
首先Lambda配合Stream擁有很強大的數(shù)據處理能力焙矛,并且能夠以更加清晰的表達方式描述數(shù)據楷怒,大大減少了代碼的冗余蛋勺。在平常開發(fā)中,能大大提高開發(fā)效率鸠删,學習它的目的也正因為如此抱完,此文介紹了一些Lambda相關的知識以及一些注意事項,避免濫用反而起到反作用刃泡。
Lambda基本介紹
Lambda:可以理解為一種匿名函數(shù):它沒有名稱巧娱,但它有參數(shù)列表碉怔、函數(shù)主體、返回類型禁添,可能還有一個可以拋出的異常列表撮胧。
Lambda示例
// 在以前,我們使用匿名類是這樣:
Thread t = new Thread(new Runnable() {
public void run(){
System.out.println("Hello world");
}
});
// 現(xiàn)在用Lambda表達式的話,看起來是這樣:
Thread t = new Thread(() -> System.out.println("Hello world"));
從上面的例子中可以看出,采用匿名內部類和采用Lambda的寫法,Lambda的寫法明顯更加精簡和清晰了
Lambda最基本的構成
() -> System.out.println("Hello world");
- 參數(shù)列表: 空參則括號里面什么都不寫
- ->: 把參數(shù)列表與Lambda主體分隔開
- Lambda主體: 代碼具體邏輯
帶入參,并且有返回值
// 第一種寫法,如果Lambda主體部分不帶花括號,可以不用寫return,返回的具體類型編譯器會自動推斷
(String s1, String s2) -> s1.concat(s2);
// 第二種寫法,如果Lambda主體部分加了花括號,要帶返回值必須加上return,否者就是Void類型的匿名函數(shù)
(String s1, String s2) -> {
return s1.concat(s2);
};
默認方法
如果要在接口中添加新方法老翘,則必須在實現(xiàn)該接口的類中提供其實現(xiàn)代碼崎页。為了解決這個問題美旧,Java 8引入了默認方法的概念劝堪,它允許接口具有默認方法捉蚤,而不會影響其實現(xiàn)類。默認方法不是抽象方法卫键,子類實現(xiàn)了該接口會繼承該默認實現(xiàn)傀履,子類也可以覆蓋該默認實現(xiàn)。
對于學習函數(shù)式接口關系不大莉炉,可以當做是一個新特性钓账,如果不打算了解可以直接跳過。
子類可以繼承接口的默認方法
// 定義接口1
interface MyInterface1 {
default void defaultMethod() {
System.out.println("defaultMethod1");
}
}
// 定義接口2絮宁,接口2繼承了接口1官扣,也默認繼承了接口1的默認方法
interface MyInterface2 extends MyInterface1 {
}
static class A implements MyInterface2 {
}
static class B implements MyInterface1 {
}
public static void main(String[] args) {
A a = new A();
a.defaultMethod();
B b = new B();
b.defaultMethod();
}
// 輸出
// defaultMethod1
// defaultMethod1
子類可以覆蓋接口的默認方法
// 定義接口1
interface MyInterface1 {
default void defaultMethod() {
System.out.println("defaultMethod1");
}
}
static class A implements MyInterface1 {
public void defaultMethod() {
System.out.println("defaultMethod1 from MyInterface1");
}
}
public static void main(String[] args) {
A a = new A();
a.defaultMethod();
}
// 輸出
// defaultMethod1 from MyInterface1
子類實現(xiàn)了兩個擁有相同默認方法,可以通過:接口名稱.super.方法名()調用
// 定義接口1
interface MyInterface1 {
default void defaultMethod() {
System.out.println("defaultMethod1");
}
}
interface MyInterface2 {
default void defaultMethod() {
System.out.println("defaultMethod2");
}
}
static class A implements MyInterface1,MyInterface2 {
// 此時必須實現(xiàn)該方法羞福,否則通過不了編譯
@Override
public void defaultMethod() {
// 如果要調用MyInterface2的默認方法,可以使用MyInterface2.super.defaultMethod();
MyInterface2.super.defaultMethod();
}
}
public static void main(String[] args) {
A a = new A();
a.defaultMethod();
}
// 輸出
// defaultMethod2
子類對接口默認方法調用規(guī)則
- 類中的方法優(yōu)先級最高蚯涮。類或父類中聲明的方法的優(yōu)先級高于任何聲明為默認方法的優(yōu)先級治专。
- 如果無法依據第一條進行判斷,那么子接口的優(yōu)先級更高:函數(shù)簽名相同時遭顶,優(yōu)先選擇擁有最具體實現(xiàn)的默認方法的接口张峰。
- 最后,如果還是無法判斷棒旗,繼承了多個接口的類必須通過顯式覆蓋和調用期望的方法喘批,顯式地選擇使用哪一個默認方法的實現(xiàn)。
上面第1條規(guī)則以及第3條規(guī)則都已經展示過铣揉,下面展示第二條規(guī)則饶深,該類圖繼承關系如下:
按照規(guī)則2,MyInterface2比MyInterface1更加具體逛拱,所以A會調用MyInterface2的defalutMethod
interface MyInterface1 {
default void defaultMethod() {
System.out.println("defaultMethod1");
}
}
interface MyInterface2 extends MyInterface1{
default void defaultMethod() {
System.out.println("defaultMethod2");
}
}
static class A implements MyInterface1,MyInterface2 {
}
public static void main(String[] args) {
A a = new A();
a.defaultMethod();
}
// 輸出
// defaultMethod2
接口靜態(tài)方法
與接口的默認方法類似敌厘,需要加上關鍵字static,靜態(tài)方法需要朽合,并且由于定義是完整的并且方法是靜態(tài)的俱两,因此在實現(xiàn)類中不能覆蓋或更改這些方法饱狂。
interface MyInterface1 {
default void defaultMethod() {
System.out.println("defaultMethod1");
}
static void staticMethod() {
System.out.println("staticMethod1");
}
}
public static void main(String[] args) {
MyInterface1.staticMethod();
}
// 輸出
// staticMethod1
過于簡單,就不上更多的例子了宪彩,下面直接說明與默認方法的區(qū)別就差不多了解了休讳。
與默認方法的相同點:
- 靜態(tài)方法與默認方法必須要有默認的實現(xiàn);
- 靜態(tài)方法與默認方法都不是抽象方法尿孔;
與默認方法的不同點:
- 子類實現(xiàn)了該接口俊柔,子類不會繼承靜態(tài)接口方法,并且也不能覆蓋靜態(tài)接口方法纳猫,但是子類可以定義與父接口一樣的靜態(tài)方法婆咸,如果要調用接口的靜態(tài)方法只能以接口名稱.方法名()來調用;接口的默認方法子類是可以繼承默認方法并且也可以覆蓋的
函數(shù)式接口
說起Lambda芜辕,就必須了解函數(shù)式接口尚骄,因為要使用Lambda,必須在函數(shù)式接口上使用。
函數(shù)式接口:就是一個有且僅有一個抽象方法侵续,但是可以有多個默認方法的接口,這樣的接口可以隱式轉換為Lambda表達式倔丈。一般在函數(shù)式接口上都有個注解@FunctionalInterface,該注解的作用類似@Override一樣告訴編譯器這是一個函數(shù)式接口状蜗,用于編譯期間檢測該接口是否僅有一個抽象方法需五,如果擁有多個則編譯不通過。如下圖所示
在函數(shù)式接口上使用lambda表達式
函數(shù)式接口可以被隱式轉換為 lambda 表達式轧坎。
如下例子
Thread t = new Thread(() -> System.out.println("Hello world"));
我們可以看看Thread的構造:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
其中入參為Runnable類型的接口宏邮,繼續(xù)查看Runnable接口
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
可以看出在jdk1.8中,Runnable就是一個函數(shù)式接口
Runnable r1 = () -> System.out.println("Hello world")
// 等價于
Runnable r2 = new Runnable() {
public void run(){
System.out.println("Hello world");
}
}
run()方法簽名:參數(shù)列表為空缸血,返回為void蜜氨;lambda簽名:() -> void 參數(shù)列表為空,返回為void可以看出Runnable的run方法簽名與lambda的簽名匹配捎泻,我們將這種對方法抽象描述叫作函數(shù)描述符
在java8中飒炎,提供了很多函數(shù)式接口,可以用于描述各種Lambda表達式的簽名
函數(shù)式接口 | 函數(shù)描述符 |
---|---|
Predicate<T> | T->boolean |
Consumer<T> | T->void |
Function<T,R> | T->R |
Supplier<T> | ()->T |
UnaryOperator<T> | T->T |
BiPredicate<L,R> | (L,R)->boolean |
BiConsumer<T,U> | (T,U)->void |
BiFunction<T,U,R> | (T,U)->R |
這些都是較為常用的函數(shù)式接口笆豁,還有很多都在
java.util.function
包下郎汪,有興趣可以自行查看。
Stream
一個新的抽象闯狱,稱為流煞赢,可以以聲明的方式處理數(shù)據。提供了一系列的api扩氢,使用類似sql語句直觀的方式來提供對集合處理的高階抽象耕驰。
另外還有兩個特點:
- 流水線:很多流操作本身會返回一個流,這樣多個操作就可以鏈接起來,形成一個大的流水線朦肘。這樣做可以對操作進行優(yōu)化饭弓, 比如延遲執(zhí)行和短路。流水線的操作可以看作對數(shù)據源進行數(shù)據庫式查詢媒抠。
- 內部迭代:以前對集合遍歷都是通過Iterator或者For-Each的方式, 顯式的在集合外部進行迭代弟断, 這叫做外部迭代。 Stream提供了內部迭代的方式趴生。
示例
// 創(chuàng)建一個1至10的集合
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
numbers
.stream() //將集合轉化成串行流
.filter(i -> i % 2 == 0) //過濾掉奇數(shù)
.limit(3) //取出前三個元素
.map(Double::valueOf) // 將int類型轉換成double類型
.forEach(System.out::println); // 迭代并打印出元素
// 最終輸出結果:2.0 4.0 6.0
如果采用傳統(tǒng)的For-Each迭代方式來處理集合阀趴,可以想象代碼可不是這短短這幾行了。
整個流水線的操作包含兩個
- 中間操作:形成一條操作流水線苍匆。例如:filter()對流操作并返回一個流刘急,limit()對流操作也會返回流,這樣可以通過多個中間操作連接起來合成一個查詢浸踩。注意:整個流水線中叔汁,除非觸發(fā)了一個終端操作,否者中間操作不會執(zhí)行任何處理检碗。因為多個中間操作可以合并起來据块,在終端操作時一次性全部處理。
- 終端操作:執(zhí)行流水線折剃,生成處理結果另假。
方法引用
上面例子中有一行代碼為:map(Double::valueOf)
,::這個寫法是什么意思呢怕犁?
實際上Double::valueOf就是一個方法引用边篮。map(Double::valueOf)
等價于map(element -> Double.value(element))
類名放在分隔符::前,方法的名稱放在后面奏甫。
例如苟耻,Double::valueOf就是引用了Double類中定義的方法valueOf,并且不需要加括號;
方法引用就是Lambda表達式的快捷寫法扶檐,例如:
-
(Integer i) -> Double.valueOf(i)
---Double::valueOf
-
(String s) -> System.out.println(s)
---System.out::println
-
(str, i) -> str.substring(i)
---String::substring
方法引用的種類
- 靜態(tài)方法引用:ClassName::methodName
- 實例上的實例方法引用:instanceReference::methodName
- 超類上的實例方法引用:super::methodName
- 類型上的實例方法引用:ClassName::methodName
- 構造方法引用:Class::new
- 數(shù)組構造方法引用:TypeName[]::new
生成流
- Collection的stream()方法或者parallelStream() ,例如Arrays.asList(1,2,3).stream()。
- Arrays.stream(Object[]) 例如Arrays.stream(new int[]{1,2,3})胁艰。
- 使用流的靜態(tài)方法款筑,比如Stream.of(Object[]), IntStream.range(int, int) 或者 Stream.iterate(Object, UnaryOperator) ,如Stream.iterate(0, n -> n * 2) , 或者generate(Supplier<T> s) 如Stream.generate(Math::random)腾么。
- BufferedReader.lines() 從文件中獲得行的流奈梳。
- Files類的操作路徑的方法,如list解虱、find攘须、walk等。
- 隨機數(shù)流Random.ints()殴泰。
- 其它一些類提供了創(chuàng)建流的方法于宙,如BitSet.stream(), Pattern.splitAsStream(java.lang.CharSequence), 和 JarFile.stream()浮驳。
- 更底層的使用StreamSupport,它提供了將Spliterator轉換成流的方法捞魁。
// 列舉一些常用創(chuàng)建流的例子
List<Integer> list = Arrays.asList(1, 2, 3);
// Collection的stream方法
Stream<Integer> stream = list.stream();
// Stream的of方法
Stream<List<Integer>> stream2 = Stream.of(list);
// BufferedReader的lines方法
BufferedReader bufferedReader = new BufferedReader(new FileReader("filePath"));
Stream<String> lines = bufferedReader.lines();
中間操作
filter
Stream<T> filter(Predicate<? super T> predicate)
返回此流中匹配元素組成的流
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
numbers
.stream()
.filter(i -> i % 2 == 0) //過濾掉奇數(shù)
.forEach(System.out::println); // 終端操作至会,打印結果
// 輸出:2 4 6 8 10
map
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
返回一個流,該流的元素映射成另外的值谱俭,新的值類型可以與原來的類型不同
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
numbers
.stream()
.map(i -> i + "str ") // 轉換成String
.forEach(System.out::print); // 終端操作奉件,打印結果
// 輸出:1str 2str 3str 4str 5str 6str 7str 8str 9str 10str
mapToInt
IntStream mapToInt(ToIntFunction<? super T> mapper)
返回一個IntStream,該流的元素映射成int類型的流 IntStream:原始流
List<String> strings = Arrays.asList("1","2","3");
strings
.stream()
.mapToInt(Integer::parseInt) // 轉換成int
.forEach(System.out::println); // 終端操作昆著,打印結果
// 輸出:int類型的 1 2 3
mapToLong县貌,mapToDouble與mapToInt類似只不過原始類型不同而已,下面會單獨講解這三個原始流的作用及區(qū)別。
flatMap
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
返回一個Stream凑懂,和map類似煤痕,不同的是會將每個元素的扁平化。
flatMap的理解可能稍微有點難征候,通過下面兩個例子來展示杭攻。
例子1:引用java8實戰(zhàn)中的例子 將集合中的兩個字符串根據字母去重復
List<String> strings = Arrays.asList("Hello", "World");
List<String> list = strings
.stream() // 將集合轉成流
.map(s -> s.split("")) // 轉換成['H','e','l','l','o'],['W','o','r','l','d'] 兩個數(shù)組
.flatMap(Arrays::stream) // 將兩個數(shù)組扁平化成為['H','e','l','l','o','W','o','r','l','d'],實際上還是把兩個數(shù)組再次轉成流
.distinct() // 去除重復元素
.collect(Collectors.toList()); // 終端操作疤坝,轉化成集合
System.out.println(list);
// 輸出: [H, e, l, o, W, r, d]
引入java8實戰(zhàn)的流程圖如下:
例子2:
List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(4, 5, 6);
Stream.of(numbers1, numbers2) // 將兩個集合轉成流
.flatMap(numbers -> numbers.stream()) // 兩個集合流扁平化為[1,2,3,4,5,6]
.forEach(System.out::println);
// 輸出: 1兆解,2,3跑揉,4锅睛,5,6
distinct
Stream<T> distinct()
過濾流中重復的元素
Arrays.asList(1, 2, 3, 2, 3, 4)
.stream()
.distinct() // 去除重復
.forEach(System.out::println);
// 輸出: 1234
sorted
Stream<T> sorted()
對流中的元素順序排序
Arrays.asList(1, 3, 5, 2, 4)
.stream()
.sorted() // 順序排序
.forEach(System.out::println);
// 輸出: 12345
上面的例子中只支持順序排序历谍,如果要倒序呢现拒?Stream中sorted還提供了一個重載方法:
Stream<T> sorted(Comparator<? super T> comparator);
可以通過傳入Comparator來實現(xiàn)自己的排序規(guī)則
Arrays.asList(1, 3, 5, 2, 4)
.stream()
.sorted(Comparator.reverseOrder()) // 倒序排序
.forEach(System.out::println);
// 輸出: 54321
limit
Stream<T> limit(long maxSize)
截取流,返回一個不超過給定長度的流
Arrays.asList(1, 2, 3, 4, 5)
.stream()
.limit(3) // 截取前三個元素
.forEach(System.out::println);
// 輸出: 123
skip
Stream<T> skip(long n)
跳過給定長度的流
Arrays.asList(1, 2, 3, 4, 5)
.stream()
.skip(2) // 跳過前兩個元素
.forEach(System.out::println);
// 輸出: 345
parallel
S parallel()
將流轉成并行流
Arrays.asList(1, 2, 3, 4, 5)
.stream()
.parallel() // 轉成并行流
.forEach(System.out::println);
// 由于是并行的望侈,每次輸出結果都會不一致
// 輸出: 1 5 2 4 3
parallel線程安全需要注意的點
直接上例子
// 反例1
for (int i = 0; i < 5; i++) {
List<Integer> list = new ArrayList<>();
IntStream.rangeClosed(1, 1000).parallel().forEach(element->{
list.add(element);
});
System.out.println(list.size());
}
// 輸出:981 990 962 ...... 多次運行會發(fā)現(xiàn)每次結果都不一樣印蔬,并且有時還會報ArrayIndexOutOfBoundsException數(shù)組越界
// 這邊體現(xiàn)了在多線程中操作共享變量引發(fā)的問題,例如list容器當前容量為50脱衙,兩個線程同時進入方法體侥猬,此時線程A持有的list里面有49個元素,線程B持有的list里面也是49個元素捐韩,然后線程A執(zhí)行l(wèi)ist.add()完成退唠,此時容器內的元素的數(shù)量有50,由于線程之間不可見荤胁,線程B也進入到了add方法并且過了list容器擴容的檢查瞧预,然后添加元素時發(fā)生ArrayIndexOutOfBoundsException
// 如果要能安全的新增,那么可以使用線程安全的容器
List<Integer> list = Collections.synchronizedList(new ArrayList<>());
List<Integer> list = new CopyOnWriteArrayList<>();
// 反例2
List<Integer> list = new ArrayList<>(1000);
long count = IntStream.rangeClosed(1, 1000).parallel().map(element -> {
list.add(element);
return element;
}).count();
long count = IntStream.rangeClosed(1, 1000).parallel().peek(element -> {
list.add(element);
}).count();
// 使用并行流時,不要去操作共享變量垢油,以上例子皆為反例
parallel性能上需要注意的點
對并行流的效率進行測試盆驹,每臺機器上的結果可能不一致,請自行注意秸苗。下面例子全部采用遍歷五次召娜,取其中最快的一次。
// 串行與并行流效率測試 基于i7 8核cpu
// 對100_000_000求和
// for求和性能測試
static void testFor(long size) {
List<Long> timeList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
long sum = 0;
long start = System.currentTimeMillis();
for (long j = 0L; j <= size; j++) {
sum += j;
}
long end = System.currentTimeMillis();
timeList.add((end - start));
}
System.out.println("For 處理時間:" + (timeList.stream().mapToLong(Long::longValue)).min().getAsLong() + "ms");
}
// 并行流求和性能測試
static void testParallel(long size) {
List<Long> timeList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
long start = System.currentTimeMillis();
Stream.iterate(0L, (element -> element + 1L)).limit(size).parallel().reduce(0L,Long::sum);
long end = System.currentTimeMillis();
timeList.add((end - start));
}
System.out.println("ParallelStream 處理時間:" + (timeList.stream().mapToLong(Long::longValue)).min().getAsLong() + "ms");
}
public static void main(String[] args) {
// 初始值
long size = 10_000_000L;
testFor(size);
testParallelStream(size);
}
// 輸出為:
// For 處理時間:5ms
// ParallelStream 處理時間:254ms
為什么并行的會比傳統(tǒng)For要慢惊楼,是因為Stream.iterate生成的是裝箱對象玖瘸,在求和過程中,裝箱對象需要拆箱檀咙,計算完還會在裝箱雅倒,數(shù)據量越大,那么采用裝箱對象計算則會越慢弧可∶锵唬可以稍微更改一行代碼:
Stream.iterate(0L, (element -> element + 1)).limit(size).parallel().reduce(0L,Long::sum);
更改為
Stream.iterate(0L, (element -> element + 1)).mapToLong(Long::longValue).limit(size).parallel().reduce(0L,Long::sum);
,這邊生成流的時候先轉成原始流,然后在去做計算
// 并行流求和性能測試
static void testParallelStream(long size) {
List<Long> timeList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
long start = System.currentTimeMillis();
Stream.iterate(0L, (element -> element + 1L)).mapToLong(Long::longValue).limit(size).parallel().reduce(0L,Long::sum);
long end = System.currentTimeMillis();
timeList.add((end - start));
}
System.out.println("ParallelStream 處理時間:" + (timeList.stream().mapToLong(Long::longValue)).min().getAsLong() + "ms");
}
public static void main(String[] args) {
// 初始值
long size = 10_000_000L;
testFor(size);
testParallelStream(size);
}
// 輸出為:
// For 處理時間:5ms
// ParallelStream 處理時間:143ms
// 可以看出提升了接近一倍的性能棕诵,在數(shù)據量更大的情況下裁良,會更高。
// 在java8里校套,還提供了3個生成原始流的對象:LongStream,DoubleStream,IntStream价脾,下面直接測試采用原始流來做測試
static void testParallelLongStream(long size) {
List<Long> timeList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
long start = System.currentTimeMillis();
LongStream.rangeClosed(0, size).parallel().sum();
long end = System.currentTimeMillis();
timeList.add((end - start));
}
System.out.println("ParallelLongStream 處理時間:" + (timeList.stream().mapToLong(Long::longValue)).min().getAsLong() + "ms");
}
public static void main(String[] args) {
// 初始值
long size = 10_000_000L;
testFor(size);
testParallelStream(size);
testParallelLongStream(size)
}
// 輸出為:
// For 處理時間:5ms
// ParallelStream 處理時間:148ms
// ParallelLongStream 處理時間:1ms
雖然將序列流轉成并行流很容易,但是不恰當?shù)氖褂梅吹箷蔀樨搩?yōu)化笛匙。在數(shù)據量不大的情況下侨把,并行不一定比順序的要快,反倒要慢上很多妹孙,因為數(shù)據量小的情況下秋柄,在線程的上下文切換之間的開銷已經大于數(shù)據處理的開銷了。以及在做數(shù)值計算的情況下蠢正,要留意是否是裝箱對象骇笔,自動裝箱拆箱在數(shù)據量大起來會成為性能上的累贅。
下面再看一個例子
static void testStructure(Collection<Long> c) {
List<Long> timeList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
long start = System.currentTimeMillis();
c.parallelStream().reduce(0L, Long::sum);
long end = System.currentTimeMillis();
timeList.add((end - start));
}
// 取五次中最快的一次
System.out.println("處理時間:" + (timeList.stream().mapToLong(Long::longValue)).min().getAsLong() + "ms");
}
public static void main(String[] args) {
// 使用ArrayList容器
ArrayList<Long> arrayList = Stream.iterate(1L, a -> a + 1L).limit(10_000_000L).collect(toCollection(ArrayList::new));
// 使用LinkedList容器
LinkedList<Long> linkedList = Stream.iterate(1L, a -> a + 1L).limit(10_000_000L).collect(toCollection(LinkedList::new));
testStructure(linkedList);
testStructure(arrayList);
}
// 輸出
// 處理時間:420ms
// 處理時間:36ms
在選用數(shù)據結構上嚣崭,可以看出ArrayList在并行中效率要高于LinkedList蜘拉,這是因為ArrayList的拆分效率比LinkedList高得多,前者用不著遍歷就可以平均拆分有鹿,而后者則必須遍歷。
按照可分解性總結了一些流數(shù)據源適不適于并行
數(shù)據源 | 可分解性 |
---|---|
ArrayList | 極佳 |
IntStream.range | 極佳 |
HashSet | 好 |
TreeSet | 好 |
LinkedList | 差 |
Stream.iterate | 差 |
parallel操作上需要注意的點
并行流底層使用的是java7引入的Fork/Join(并發(fā)框架)谎脯,它可以以并行的方式將任務拆分成更小的任務葱跋,然后將每個子任務的結果合并起來生成整體的結果。此文不多描述,有興趣者自行查閱娱俺。需要注意的是稍味,使用并行流時,內部使用了默認的 ForkJoinPool荠卷,池的大小為默認的cup核數(shù)-1(java8實戰(zhàn)說的是默認核數(shù)模庐,如果看過此書的請自行測試),
Runtime.getRuntime().availableProcessors()
來查看cpu的核心數(shù)量油宜。
在使用并行流時請注意掂碱,如果為IO密集型的并行,如果在多處使用慎冤,極有可能會影響所有的并行流疼燥,因為使用的是系統(tǒng)全局的ForkJoinPool,當池子里的線程被占用了蚁堤,那么別處要使用線程只能等待它被釋放醉者。
// 模擬8個任務,獨占線程并且不釋放
Runnable runnable = () -> IntStream.rangeClosed(1, 8).parallel().forEach(c -> {
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 啟用任務
new Thread(runnable).start();
System.out.println("任務開始");
// 等待一會披诗,讓池子里的線程充分被占用
Thread.sleep(1000);
IntStream.rangeClosed(0, 1000).parallel().forEach(c -> {
try {
// 打印當前前程撬即,查看是否使用了ForkJoinPool中的線程
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
可以看出,當池子里的線程被占用完呈队,別的地方使用了并行流剥槐,完全變成了單線程執(zhí)行。如果要避免這種情況掂咒,可以設置JVM啟動參數(shù)
-Djava.util.concurrent.ForkJoinPool.common.parallelism=16
來設置ForkJoinPool的大小才沧,也可以使用代碼System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "16")
來設置全局的參數(shù),以上兩種方法及其不推薦绍刮,因為它將影響所有的并行流温圆,推薦使用自定義ForkJoinPool的方式,如下所示
Runnable runnable = () -> IntStream.rangeClosed(1, 8).parallel().forEach(c -> {
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 啟用任務
new Thread(runnable).start();
System.out.println("任務開始");
// 設置一個容量為10的ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(10);
// 執(zhí)行任務
ForkJoinTask<?> submit = forkJoinPool.submit(() -> {
IntStream.rangeClosed(0, 20).parallel().forEach(c -> {
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
});
while (!submit.isDone()) {
Thread.sleep(500);
}
sequential
S sequential()
將流轉成序列流
Arrays.asList(1, 2, 3, 4, 5)
.parallelStream()
.sequential() // 轉成序列流
.forEach(System.out::println);
// 輸出: 1 2 3 4 5
終端操作
allMatch孩革,anyMatch岁歉,noneMatch
boolean anyMatch(Predicate<? super T> predicate)
anyMatch:此流的任意元素有一個匹配返回ture,都不匹配返回false
boolean allMatch(Predicate<? super T> predicate)
allMatch:此流的所有元素是都匹配返回ture膝蜈,否者為false
boolean noneMatch(Predicate<? super T> predicate)
noneMatch:此流中沒有一個元素匹配返回ture,否者返回false
// 全部匹配
System.out.println(Stream.of(5, 6, 7, 8, 9).allMatch(i -> i >= 5)); // true
System.out.println(Stream.of(5, 6, 7, 8, 9).allMatch(i -> i > 5)); // false
// 任意一個匹配
System.out.println(Stream.of(5, 6, 7, 8, 9).anyMatch(i -> i > 5)); // true
System.out.println(Stream.of(5, 6, 7, 8, 9).anyMatch(i -> i > 9)); // false
// 都不匹配
System.out.println(Stream.of(5, 6, 7, 8, 9).noneMatch(i -> i > 5)); // false
System.out.println(Stream.of(5, 6, 7, 8, 9).noneMatch(i -> i > 9)); // true
reduce
聚合操作 sum()锅移、max()、min()饱搏、count()調用的都是reduce
Optional<T> reduce(BinaryOperator<T> accumulator)
無初始值非剃,按傳入的lambda的累加規(guī)則來聚合數(shù)據
// 無默認值,求和
Optional<Integer> sum1 = Arrays.asList(1, 2, 3, 4, 5)
.stream()
.reduce((a, b) -> a + b);
System.out.println(sum1.get()); // 輸出:15
T reduce(T identity, BinaryOperator<T> accumulator)
第一個參數(shù)為初始值推沸,第二個參數(shù)為累加器(歸并數(shù)據的lambda)
// 有默認值备绽,求和
Integer sum2 = Arrays.asList(1, 2, 3, 4, 5)
.stream()
.reduce(5, (a, b) -> a + b);
System.out.println(sum2); // 輸出:20
// 求最大值
Integer max = Arrays.asList(1, 2, 3, 4, 5)
.stream()
.reduce(0, Integer::max); // 也可以寫成 reduce(0, (a, b) -> a > b ? a : b);
System.out.println(max); // 輸出:20
<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner)
combiner:合并器,用于合并累加器的值券坞,這個參數(shù)只有在并行流下才會生效
reduce操作可以并行進行,為了避免競爭肺素,每個reduce線程都會有獨立的result恨锚,combiner的作用在于合并每個線程的result得到最終結果。
Integer reduce = Arrays.asList(1, 2, 3, 4, 5)
.parallelStream()
.reduce(0, (a, b) -> a + b, (c, d) -> c + d);
System.out.println(reduce); // 輸出:20
reduce在并行流中的注意事項
System.out.println(
Arrays.asList(1, 2, 3)
.parallelStream()
.reduce(0,(a, b) -> (a - b),(c, d) -> c + d)
);
// 如果無意料倍靡,那么輸出將會是 -6猴伶,當運行程序的時候結果卻是 -2,這與我們的預期結果大大不符
// 為什么會是-3呢塌西,那么在序列流和并行流結果不一致他挎,將以上代碼修改一下,把參數(shù)和線程打印出來
System.out.println(
Arrays.asList(1, 2, 3)
.parallelStream()
.reduce(0,
(a, b) -> {
System.out.format("a:%s b:%s Thread:%s \n", a, b, Thread.currentThread().getName());
return a - b;
},
(c, d) -> {
System.out.format("c:%s d:%s Thread:%s \n", c, d, Thread.currentThread().getName());
return c - d;
}
));
在并行流中栖忠,reduce計算的方式與序列流不同崔挖,這歸根于fork/join的特殊性,所有任務不斷拆分庵寞,如果有初始值狸相,那么會在累加階段會以每個初始值與流中的數(shù)據累加,例如初始值為1捐川,執(zhí)行一個求和的累加脓鹃,那么如果有N個元素,那么最終結果值為SUM + (N * 1)古沥,在相乘瘸右,相加,相減等等計算在使用并行流時需要好好考慮由并行帶來的影響岩齿,當然如果只是聚合計算(sum,avg,max,min)可以放心的使用太颤,如果采用自定義計算規(guī)則,那么一定需要謹慎使用盹沈,并測試龄章。
findFirst,findAny
Optional<T> findFirst()
返回此流的第一個元素的Optional,如果流為空乞封,則返回空Optional做裙。
Optional<T> findAny()
返回此流的任意一個元素的Optional,如果流為空肃晚,則返回空Optional锚贱。
findFirst在并行流中的執(zhí)行代價非常大,需要注意
Optional<Integer> first = Arrays.asList(1, 2, 3, 4, 5)
.stream().findFirst();
System.out.println(first.get()); // 輸出 1
Optional<Integer> any = Arrays.asList(1, 2, 3, 4, 5)
.stream().findAny();
System.out.println(any.get()); // 因為是順序流关串,所以輸出1
collect
<R, A> R collect(Collector<? super T, A, R> collector)
收集拧廊,對數(shù)據做聚合杂穷,將流轉換為其他形式,比如List,Map卦绣,Integer,Long...
// 準備一些初始數(shù)據
@Data
@AllArgsConstructor
class Student {
private String name;
private Integer age;
}
// 初始化數(shù)據
Student student1 = new Student("zhangsan", 20);
Student student2 = new Student("lisi", 15);
Student student3 = new Student("wangwu", 10);
Student student4 = new Student("zhaoliu", 20);
List<Student> students = Arrays.asList(student1, student2, student3, student4);
// 如果要取出所有學生的姓名并轉成集合可以寫成
List<String> names = students.stream()
.map(Student::getName) // 獲取name
.collect(Collectors.toList()); // 轉成List
System.out.println(names); // 輸出:[zhangsan, lisi, wangwu, zhaoliu]
// 以年齡為key,姓名為value轉成Map可以寫成
Map<Integer, String> map = students.stream()
.collect(Collectors.toMap(Student::getAge, Student::getName)); // 此寫法會有問題飞蚓,如果Map的key重復了滤港,會報java.lang.IllegalStateException: Duplicate key 如果可以確保key不會重復就可以省略第三個參數(shù)
Map<Integer, String> map = students.stream()
.collect(Collectors.toMap(Student::getAge, Student::getName, (first, second) -> second)); // 前面兩個參數(shù)是映射key和value,第三個參數(shù)為如果key重復了要如何處理趴拧,是保留舊的還是選擇新的
System.out.println(map); // 輸出:{20=zhaoliu, 10=wangwu, 15=lisi} 因為zhangsan和zhaoliu的年齡都是20,按照我們的策略溅漾,始終選擇新的,所以key為20的value是zhaoliu
Map<Integer, List<Student>> groupByAge = students.stream()
.collect(Collectors.groupingBy(Student::getAge)); // 根據age分組
System.out.println(groupByAge);
// 輸出:{20=[Student(name=zhangsan, age=20), Student(name=zhaoliu, age=20)], 10=[Student(name=wangwu, age=10)], 15=[Student(name=lisi, age=15)]}
<R> R collect(Supplier<R> supplier,BiConsumer<R, ? super T> accumulator,BiConsumer<R, R> combiner)
supplier:定義一個容器
accumulator:該容器怎么添加流中的數(shù)據
combiner:容器如何去聚合
// 仿Collectors.toList(),簡單實現(xiàn)一個toList()
// 1.定義一個List容器
// 2.調用List的add方法將元素添加到容器中
// 3.采用List的addAll方法聚合容器
List<Integer> toList = Arrays.asList(1, 2, 3, 4).stream().collect(ArrayList::new, List::add, List::addAll);
System.out.println(toList);
// 輸出:[1, 2, 3, 4]
// 仿Collectors.toMap(),簡單實現(xiàn)toMap()
// 1.定義一個Map容器
// 2.調用Map的merge方法將元素添加到容器中
// 3.采用Map的putAll方法聚合容器
Map<Object, Object> map = students.stream()
.collect(HashMap::new,
(holder, element) -> {
holder.merge(element.getAge(), element.getName(), (u, v) -> {
return u;
// throw new IllegalStateException(String.format("Duplicate key %s", u));
});
}, Map::putAll);
System.out.println(map);
// 輸出:{20=zhangsan, 10=wangwu, 15=lisi}
總結
- lambda由參數(shù)列表著榴,箭頭添履,主體組成。
- 函數(shù)式接口只能擁有一個抽象方法脑又,可以擁有多個默認方法暮胧,多個靜態(tài)方法。
- 方法引用實際就是Lambda的快捷寫法问麸。
- 流只能遍歷一次往衷。遍歷完之后,我們就說這個流已經被消費掉了严卖。你可以從原始數(shù)據源那里再獲得一個新的流來重新遍歷一遍席舍。
- 并行流是采用ForkJoin實現(xiàn)的。
- 在并行流中哮笆,不要在peek,map中不要去修改外部數(shù)據来颤。
- 并行流使用需要注意,不要靠猜測稠肘,請多測試福铅。
- 接口默認方法,優(yōu)先級最低启具,子類會繼承默認方法并且可以覆蓋默認方法本讥。如果因為多繼承問題引起沖突(子類實現(xiàn)了兩個接口,兩個接口都擁有相同的方法名鲁冯,相同函數(shù)描述符)拷沸,那么必須覆蓋該方法,如果期望調用某接口中的默認方法薯演,可以使用X.super.m(…)來顯示調用哪個接口的默認方法撞芍。
- 接口靜態(tài)方法,子類不會繼承跨扮,也不能覆蓋序无,但是可以定義一個名稱相同返回值相同的普通或靜態(tài)方法验毡。