從零開始的Java8-Lambda

引子

首先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ī)則

  1. 類中的方法優(yōu)先級最高蚯涮。類或父類中聲明的方法的優(yōu)先級高于任何聲明為默認方法的優(yōu)先級治专。
  2. 如果無法依據第一條進行判斷,那么子接口的優(yōu)先級更高:函數(shù)簽名相同時遭顶,優(yōu)先選擇擁有最具體實現(xiàn)的默認方法的接口张峰。
  3. 最后,如果還是無法判斷棒旗,繼承了多個接口的類必須通過顯式覆蓋和調用期望的方法喘批,顯式地選擇使用哪一個默認方法的實現(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ù)量油宜。

parallel運行時監(jiān)控的線程數(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;
           }
));

輸出如下

累加器的輸出:0-2雨让,0-3雇盖,0-1
合并器的輸出:-2 - (-3),-1-1
執(zhí)行流程如下圖所示

在并行流中栖忠,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}

總結

  1. lambda由參數(shù)列表著榴,箭頭添履,主體組成。
  2. 函數(shù)式接口只能擁有一個抽象方法脑又,可以擁有多個默認方法暮胧,多個靜態(tài)方法。
  3. 方法引用實際就是Lambda的快捷寫法问麸。
  4. 流只能遍歷一次往衷。遍歷完之后,我們就說這個流已經被消費掉了严卖。你可以從原始數(shù)據源那里再獲得一個新的流來重新遍歷一遍席舍。
  5. 并行流是采用ForkJoin實現(xiàn)的。
  6. 在并行流中哮笆,不要在peek,map中不要去修改外部數(shù)據来颤。
  7. 并行流使用需要注意,不要靠猜測稠肘,請多測試福铅。
  8. 接口默認方法,優(yōu)先級最低启具,子類會繼承默認方法并且可以覆蓋默認方法本讥。如果因為多繼承問題引起沖突(子類實現(xiàn)了兩個接口,兩個接口都擁有相同的方法名鲁冯,相同函數(shù)描述符)拷沸,那么必須覆蓋該方法,如果期望調用某接口中的默認方法薯演,可以使用X.super.m(…)來顯示調用哪個接口的默認方法撞芍。
  9. 接口靜態(tài)方法,子類不會繼承跨扮,也不能覆蓋序无,但是可以定義一個名稱相同返回值相同的普通或靜態(tài)方法验毡。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市帝嗡,隨后出現(xiàn)的幾起案子晶通,更是在濱河造成了極大的恐慌,老刑警劉巖哟玷,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件狮辽,死亡現(xiàn)場離奇詭異,居然都是意外死亡巢寡,警方通過查閱死者的電腦和手機喉脖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抑月,“玉大人树叽,你說我怎么就攤上這事∏酰” “怎么了题诵?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長挨稿。 經常有香客問我仇轻,道長,這世上最難降的妖魔是什么奶甘? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任篷店,我火速辦了婚禮,結果婚禮上臭家,老公的妹妹穿的比我還像新娘疲陕。我一直安慰自己,他們只是感情好钉赁,可當我...
    茶點故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布蹄殃。 她就那樣靜靜地躺著,像睡著了一般你踩。 火紅的嫁衣襯著肌膚如雪诅岩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天带膜,我揣著相機與錄音吩谦,去河邊找鬼。 笑死膝藕,一個胖子當著我的面吹牛式廷,可吹牛的內容都是我干的。 我是一名探鬼主播芭挽,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼滑废,長吁一口氣:“原來是場噩夢啊……” “哼蝗肪!你這毒婦竟也來了?” 一聲冷哼從身側響起蠕趁,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤薛闪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后俺陋,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體逛绵,經...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年倔韭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瓢对。...
    茶點故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡寿酌,死狀恐怖,靈堂內的尸體忽然破棺而出硕蛹,到底是詐尸還是另有隱情幅垮,我是刑警寧澤纤控,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響宴卖,放射性物質發(fā)生泄漏。R本人自食惡果不足惜响谓,卻給世界環(huán)境...
    茶點故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一债查、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧卵蛉,春花似錦颁股、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至葡缰,卻和暖如春亏掀,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背泛释。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工滤愕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人胁澳。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓该互,卻偏偏與公主長得像,于是被迫代替她去往敵國和親韭畸。 傳聞我的和親對象是個殘疾皇子宇智,可洞房花燭夜當晚...
    茶點故事閱讀 44,647評論 2 354

推薦閱讀更多精彩內容

  • Java8 in action 沒有共享的可變數(shù)據蔓搞,將方法和函數(shù)即代碼傳遞給其他方法的能力就是我們平常所說的函數(shù)式...
    鐵牛很鐵閱讀 1,229評論 1 2
  • 對于Java開發(fā)者來說,Java8的版本顯然是一個具有里程碑意義的版本随橘,蘊含了許多令人激動的新特性喂分,如果能利用好這...
    jackcooper閱讀 1,023評論 0 6
  • 對于Java開發(fā)者來說,Java8的版本顯然是一個具有里程碑意義的版本机蔗,蘊含了許多令人激動的新特性蒲祈,如果能利用好這...
    huanfuan閱讀 558評論 0 9
  • 原創(chuàng)文章&經驗總結&從校招到A廠一路陽光一路滄桑 詳情請戳www.codercc.com 對于Java開發(fā)者來說,...
    你聽___閱讀 2,340評論 4 38
  • 本筆記來自 計算機程序的思維邏輯 系列文章 Lambda表達式 Lambda表達式 語法 匿名函數(shù)萝嘁,由 -> 分隔...
    碼匠閱讀 475評論 0 6