Java 8 In Action

我的博客:zhenganwen.topjava

函數式編程

函數式編程給個人直觀感覺:數據庫

  • 讓方法參數具有行爲能力,以使方法可以從容地應對頻繁的業務需求變動。(替代接口的匿名實現類的編寫)
  • 簡化代碼的編寫,並加強代碼的可讀性

引言——讓方法參數具有行爲能力

假設你如今是一個農場主,你採摘了一筐蘋果以下:編程

List<Apple> apples = Arrays.asList(
    new Apple("red", 100),
    new Apple("red", 300),
    new Apple("red", 500),
    new Apple("green", 200),
    new Apple("green", 400),
    new Apple("green", 600),
    new Apple("yellow", 300),
    new Apple("yellow", 400),
    new Apple("yellow", 500)
);
複製代碼

Apple數組

@Data
@AllArgsConstructor
public class Apple {
    private String color;
    private int weight;
}
複製代碼

如今須要你編寫一個方法,挑選出籮筐中顏色爲綠色的蘋果,因而你垂手可得地寫了以下代碼:緩存

@Test
public void pickGreenApples() {
    List<Apple> list = new ArrayList<>();
    for (Apple apple : apples) {
        if (Objects.equals(apple.getColor(), "green")) {
            list.add(apple);
        }
    }
    System.out.println(list);
}

[Apple(color=green, weight=200), Apple(color=green, weight=400), Apple(color=green, weight=600)]
複製代碼

若是須要你挑選出紅色的呢?你發現能夠將按照顏色挑選蘋果抽取出來以供複用:bash

public void pickByColor(List<Apple> apples, String color) {
    for (Apple apple : apples) {
        if (Objects.equals(apple.getColor(), color)) {
            System.out.println(apple);
        }
    }
}

@Test
public void testPickByColor() {
    pickByColor(apples, "red");
}

Apple(color=red, weight=100)
Apple(color=red, weight=300)
Apple(color=red, weight=500)
複製代碼

好了,如今我須要你挑選出重量在400g以上,顏色不是綠色的蘋果呢?你會發現,根據不一樣的顏色和重量挑選標準可以組合成若干的挑選策略,那是否是針對每一個策略咱們都須要編寫一個方法呢?這固然不可能,咱們也沒法事先預知顧客須要什麼樣的蘋果。併發

如今咱們來理一下業務需求,其實也就是咱們給顧客一個蘋果,由他來判斷是否符合他的食用標準,這能夠採用策略模式來實現:app

  1. 將判斷某個蘋果是否符合標準這一行爲抽象爲一個接口:框架

    public interface AppleJudgementStrategy {
        /** * 你給我一個apple,我判斷他是否符合挑選出來的標準 * @param apple the apple in container * @return true if apple need be picked */
        boolean judge(Apple apple);
    }
    複製代碼
  2. 挑選蘋果的方法根據策略挑選蘋果(面向接口編程,靈活性更大)dom

    public void pickByStrategy(List<Apple> apples,AppleJudgementStrategy strategy) {
        for (Apple apple : apples) {
            if (strategy.judge(apple)) {
                // apple 符合既定的挑選策略
                System.out.println(apple);
            }
        }
    }
    複製代碼
  3. 業務方法根據實際的業務需求建立具體的挑選策略傳給挑選方法

    @Test
    public void testPickByStrategy() {
        // 挑選400g以上且顏色不是綠色的
        pickByStrategy(apples, new AppleJudgementStrategy() {
            @Override
            public boolean judge(Apple apple) {
                return apple.getWeight() >= 400 && !Objects.equals(apple.getColor(), "green");
            }
        });
    }
    
    Apple(color=red, weight=500)
    Apple(color=yellow, weight=400)
    Apple(color=yellow, weight=500)
    複製代碼

那麼以上的代碼重構是無需基於Java 8的,但其中有一個弊端:策略模式的實現要麼須要建立一個新類,要麼使用匿名類的方式。在此過程當中,new/implements AppleJudgementStrategypublic boolean judge的大量重複是很冗餘的,Java 8引入的Lambda表達式就很好的改善了這一點,如上述代碼可簡化以下:

@Test
public void testPickByStrategyWithLambda() {
    pickByStrategy(apples, apple -> apple.getWeight() >= 400 && !Objects.equals(apple.getColor(), "green"));
}

Apple(color=red, weight=500)
Apple(color=yellow, weight=400)
Apple(color=yellow, weight=500)
複製代碼

本節經過挑選蘋果的例子初步體驗了一下Lambda表達式的魅力,但其的做用不只於此,接下來讓咱們打開函數式編程的大門。

爲何要引入函數式編程

正如上一節說表現的,面對同一業務(挑選蘋果)的需求的頻繁變動,業務方法僅能接受基本類型、引用類型(對象類型)的參數已經不能知足咱們的需求了,咱們指望可以經過參數接受某一特定的行爲(如判斷某個蘋果是否應該被挑選出來)以使方法可以「以不變應萬變」。

上例中,雖然咱們能經過策略模式達到此目的,但若是究其本質,策略接口其實就是對一個函數的封裝,而咱們業務方法參數接收該對象也僅僅是爲了調用該對象實現的接口方法。不管咱們是在調用業務方法以前建立一個該接口的實現類而後在調用業務方法傳參時new該實現類,仍是在調用業務方法傳參時直接new一個匿名類,這些建立類的操做都只是爲了遵照「在Java中,類是第一公民的事實」(即先有類,後有方法,方法必須封裝在類中)。所以,建立類和聲明方法的過程顯得有些多餘,其實業務方法只是想要一個具體的策略而已,如return apple.getWeight() >= 400(挑選出重量大於400g的蘋果)。

所以你會看到Java 8引入Lambda表達式以後,業務調用方能夠變動以下:

// 業務方法
public void pickByStrategy(List<Apple> apples,AppleJudgementStrategy strategy) {
    for (Apple apple : apples) {
        if (strategy.judge(apple)) {
            // apple 符合既定的挑選策略
            System.out.println(apple);
        }
    }
}

// 調用業務
@Test
public void testPickByStrategy() {
    // 挑選400g以上的
    pickByStrategy( apples, (apple) -> { return apple.getWeight() >= 400 } );
}
複製代碼

咱們使用一個Lambda表達式(apple) -> { return apple.getWeight() >= 400 }代替了AppleJudgementStrategy實例的建立

什麼是Lambda表達式

Lambda表達式能夠理解爲函數表達式,在上例中指的就是(apple) -> { return apple.getWeight() >= 400 },表示對接口AppleJudgementStrategy函數boolean judge(Apple apple);的一個實現。其中(apple)表示函數接受一個Apple類型的對象;{ return apple.getWeight() >= 400 }則是函數體,表示傳入Apple對象的重量大於400時返回true

Lambda表達式和接口是密切相關的,並且能使用Lambda表達式代替其實例的接口必須是隻聲明瞭一個抽象方法的接口

  • 先要有一個接口,不能是抽象類
  • 該接口中有且僅有一個不帶方法體的抽象方法
  • 該接口中能夠有若干個帶有方法體的defaultstatic方法
  • 能夠在接口上標註@FunctionalInterface以表示該接口是函數式接口,其實例能夠經過Lambda表達式建立,而且該註解能夠約束該接口知足上述三點規則

注意:Java 8 對接口進行了從新定義,爲了得到更好的兼容性,接口中的方法能夠有方法體,此時該方法需被標記爲default,即該方法有一個默認的實現,子類能夠選擇性的重寫。不像抽象方法,必須被具體子類重寫。

此外接口中還能夠定義帶有方法體的靜態方法,能夠經過接口名.方法名的形式訪問,這一點與類靜態方法無異。

上述兩點打破了Java 8 以前對接口的定義:接口中的方法必須都是抽象方法(public abstract)。

也就是說在AppleJudgementStrategy添加若干defaultstatic方法,都是不影響使用Lambda表達式來代替其實例的(在IDE中,@FunctionalInterface註解不會報紅表示這是一個合法的函數式接口):

@FunctionalInterface
public interface AppleJudgementStrategy {
    /** * 你給我一個apple,我判斷他是否符合挑選出來的標準 * @param apple the apple in container * @return true if apple need be picked */
    boolean judge(Apple apple);

    default void fun1() {
        System.out.println("這是帶有默認實現的方法");
    }

    static void fun2() {
        System.out.println("這是定義在接口中的靜態方法");
    }
}
複製代碼

有了函數式接口以後咱們就能夠在須要該接口實例的地方使用lambda表達式了。

實戰

下面咱們經過實戰來鞏固lambda表達式的運用。

  1. 編寫函數式接口:

    @FunctionalInterface
    public interface AccumulatorFunction {
    
        /** * 該函數聚合兩個整數經過運算生成一個結果 * @param a 整數a * @param b 整數b * @return 運算結果 */
        int accumulate(int a, int b);
    }
    複製代碼
  2. 編寫業務方法,參數接受函數式接口實例,讓該參數具有行爲能力

    /** * 經過既定的計算規則對輸入值a和b得出輸出結果 * @param a * @param b * @param accumulatorFunction * @return */
    public int compute(int a, int b, AccumulatorFunction accumulatorFunction) {
        return accumulatorFunction.accumulate(a,b);
    }
    複製代碼
  3. 編寫業務調用方,經過lambda表達式闡述行爲

    @Test
    public void testAccumulatorFunction() {
        int res1 = compute(1, 2, (a, b) -> {		//闡述的行爲是求和
            return a + b;
        });
        System.out.println("1加2的結果是:" + res1);
        int res2 = compute(1, 2, (a, b) -> {		//闡述的行爲是求乘積
            return a * b;
        });
        System.out.println("1乘2的結果是:" + res2);
    }
    
    12的結果是:3
    12的結果是:2
    複製代碼

Lambda表達式的編寫規則

經過上一節咱們知道lambda表達式是和函數式接口密切相關的,在須要傳入函數式接口實例時咱們能夠編寫lambda表達式,編寫時咱們要特別關注該接口抽象方法定義的參數列表以及返回值。

假設函數式接口聲明以下(其中R,T1,T2,T3爲基本類型或引用類型):

@FunctionalInterface
public interface MyFunction{
    R fun(T1 t1, T2 t2, T3 t3);
}
複製代碼

那麼你的lambda表達式就應該編寫以下:

(t1, t2, t3) -> {	// 參數名自定義,爲a,b,c也能夠,可是要知道在方法體中訪問時a的類型是T1,b的類型是T2
    // do you service with t1,t2,t3
    return instance_of_R;
}
複製代碼

總結以下,嚴格意義上的lambda表達式需包含:

  • 入參列表,用小括號括起來
  • 箭頭,由入參列表指向函數體
  • 函數體,由大括號括起來,其中編寫語句,若函數有返回值則還應用return語句做爲結尾

可是爲了書寫簡潔,上述幾點有時不是必須的。

入參列表只有一個參數時能夠省略小括號

t1 -> {
    // do your service with t1
    return instance_of_R;
}
複製代碼

其餘狀況下(包括無參時)都必須有小括號

函數體能夠用表達式代替

當函數體只有一條語句時,如return t1+t2+t3(假設t1,t2,t3,R都是int型),那麼方法體能夠省略大括號和return

(t1,t2,t3) -> t1+t2+t3
複製代碼

只要函數體包含了語句(必須以分號結尾),那麼函數體就須要加上大括號

function包 & 方法推導

在Java 8 中,爲咱們新增了一個java.util.function包,其中定義的就所有都是函數式接口,其中最爲主要的接口以下:

  • Consumer,接受一個參數,沒有返回值。表明了消費型函數,函數調用消費你傳入的參數,但不給你返回任何信息
  • Supplier,不接收參數,返回一個對象。表明了生產型函數,經過函數調用可以獲取特定的對象
  • Predicate,接收一個對象,返回一個布爾值。表明了斷言型函數,對接收的對象斷言是否符合某種標註。(咱們上面定義的AppleJudgementFunction就屬於這種函數。
  • Function,接收一個參數,返回函數處理結果。表明了輸入-輸出型函數。

該包下的其餘全部接口都是基於以上接口在參數接受個數(如BiConsumer消費兩個參數)、和參數接收類型(如IntConsumer僅用於消費int型參數)上作了一個具體化。

Consumer In Action

對每一個蘋果進行「消費」操做:

public void consumerApples(List<Apple> apples, Consumer<Apple> consumer) {
    for (Apple apple : apples) {
        consumer.accept(apple);
    }
}
複製代碼

如「消費」操做就是將傳入的蘋果打印一下:

@Test
public void testConsumer() {
    consumerApples(apples, apple -> System.out.println(apple));
}

Apple(color=red, weight=100)
Apple(color=red, weight=300)
Apple(color=red, weight=500)
Apple(color=green, weight=200)
Apple(color=green, weight=400)
Apple(color=green, weight=600)
Apple(color=yellow, weight=300)
Apple(color=yellow, weight=400)
Apple(color=yellow, weight=500)
複製代碼

若是你使用的IDE是IDEA,那麼它會提示你System.out.println(apple)能夠Lambda can be replaced with method inference(即該lambda表達式可使用方法推導),因而咱們使用快捷鍵alt + Enter尋求代碼優化提示,表明被替換成了apple -> System.out::println

這裏引伸出了lambda的一個新用法:方法推導。當咱們在編寫lambda時,若是方法體只是一個表達式,而且該表達式調用的方法行爲與此處對應的函數接口的行爲一致時,可使用方法推導(類名::方法名對象名::方法名)。

由於這裏咱們須要一個Consumer,而println的定義與Consumer.accept的函數行爲是一致的(接受一個對象,無返回值):

public void println(Object x) {
    String s = String.valueOf(x);
    synchronized (this) {
        print(s);
        newLine();
    }
}
複製代碼

所以IDEA提示咱們此處可使用方法推導apple -> System.out::println代替方法調用apple -> System.out.println(apple)。如此,前者看起來更具可讀性:使用println行爲消費這個apple

Supplier In Action

Supplier像是一個工廠方法,你能夠經過它來獲取對象實例。

如,經過Supplier你給我一個蘋果,我來打印它的信息

public void printApple(Supplier<Apple> appleSupplier) {
    Apple apple = appleSupplier.get();
    System.out.println(apple);
}

@Test
public void testSupplier() {
    printApple(() -> {
        return new Apple("red", 666);
    });
}

Apple(color=red, weight=666)
複製代碼

若是Apple提供無參構造方法,那麼這裏可使用構造函數的方法推導(無參構造函數不接收參數,但返回一個對象,和Supplier.get的函數類型一致):

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Apple {
    private String color;
    private int weight;
}

@Test
public void testSupplier() {
    // printApple(() -> {
    // return new Apple("red", 666);
    // });
    printApple(Apple::new);
}

Apple(color=null, weight=0)
複製代碼

Predicate In Action

Predicate則是用來斷言對象,即按照某種策略斷定入參對象是否符合標準的。

爲此咱們能夠將先前寫的挑選蘋果的業務方法中的AppleJudgementStrategy換成Predicate,做用是同樣的,之後用到策略模式的地方直接使用Predicate<T>就好了

// public void pickByStrategy(List<Apple> apples, AppleJudgementStrategy strategy) {
// for (Apple apple : apples) {
// if (strategy.judge(apple)) {
// // apple 符合既定的挑選策略
// System.out.println(apple);
// }
// }
// }
public void pickByStrategy(List<Apple> apples, Predicate<Apple> applePredicate) {
    for (Apple apple : apples) {
        if (applePredicate.test(apple)) {
            // apple 符合既定的挑選策略
            System.out.println(apple);
        }
    }
}

@Test
public void testPickByStrategyWithLambda() {
    pickByStrategy(apples, apple -> 
                   apple.getWeight() >= 400 && !Objects.equals(apple.getColor(),"green"));
}

Apple(color=red, weight=500)
Apple(color=yellow, weight=400)
Apple(color=yellow, weight=500)
複製代碼

Function In Action

Function就不用說了,表明了最普通的函數描述:有輸入,有輸出。

咱們此前編寫的AccumulatorFunction就屬於一個BiFunction,讓咱們來使用BiFucntion對其進行改造:

// public int compute(int a, int b, AccumulatorFunction accumulatorFunction) {
// return accumulatorFunction.accumulate(a,b);
// }

public int compute(int a, int b, BiFunction<Integer,Integer,Integer> biFunction) {
    return biFunction.apply(a,b);
}

@Test
public void testAccumulatorFunction() {
    int res1 = compute(1, 2, (a, b) -> {
        return a + b;
    });
    System.out.println("1加2的結果是:" + res1);
    int res2 = compute(1, 2, (a, b) -> {
        return a * b;
    });
    System.out.println("1乘2的結果是:" + res2);
}

12的結果是:3
12的結果是:2
複製代碼

Stream——更優雅的集合操做工具類

引言——Stream初體驗

如今有一個菜品類定義以下:

public class Dish {

    public enum Type {MEAT, FISH, OTHER}

    private final String name;			//菜品名稱
    private final boolean vegetarian;	//是不是素食
    private final int calories;			//提供的卡路里
    private final Type type;			//菜品類型

    public Dish(String name, boolean vegetarian, int calories, Type type) {
        this.name = name;
        this.vegetarian = vegetarian;
        this.calories = calories;
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public boolean isVegetarian() {
        return vegetarian;
    }

    public int getCalories() {
        return calories;
    }

    public Type getType() {
        return type;
    }

    @Override
    public String toString() {
        return name;
    }
}
複製代碼

給你一份包含若干菜品的菜單:

List<Dish> menu = Arrays.asList(
    new Dish("pork", false, 800, Dish.Type.MEAT),
    new Dish("beef", false, 700, Dish.Type.MEAT),
    new Dish("chicken", false, 400, Dish.Type.MEAT),
    new Dish("french fries", true, 530, Dish.Type.OTHER),
    new Dish("rice", true, 350, Dish.Type.OTHER),
    new Dish("season fruit", true, 120, Dish.Type.OTHER),
    new Dish("pizza", true, 550, Dish.Type.OTHER),
    new Dish("prawns", false, 300, Dish.Type.FISH),
    new Dish("salmon", false, 450, Dish.Type.FISH));
複製代碼

如今要你以卡路里升序的方法打印卡路里在400如下的菜品名稱清單,在Java 8 以前,你可能須要這樣作:

@Test
public void beforeJava8() {
    // 1. filter which calories is lower than 400 and collect them
    List<Dish> filterMenu = new ArrayList<>();
    for (Dish dish : menu) {
        if (dish.getCalories() < 400) {
            filterMenu.add(dish);
        }
    }
    // 2. sort by calories ascending
    Collections.sort(filterMenu, new Comparator<Dish>() {
        @Override
        public int compare(Dish o1, Dish o2) {
            return o1.getCalories() - o2.getCalories();
        }
    });
    // 3. map Dish to Dish.getName and collect them
    List<String> nameList = new ArrayList<>();
    for (Dish dish : filterMenu) {
        nameList.add(dish.getName());
    }
    // print name list
    System.out.println(nameList);
}

[season fruit, prawns, rice]
複製代碼

在Java 8以後,經過Stream只需簡潔明瞭的一行代碼就能搞定:

@Test
public void userJava8() {
    List<String> nameList = menu.stream()
        // 1. filter which calories is lower than 400
        .filter(dish -> dish.getCalories() < 400)
        // 2. sort by calories ascending
        .sorted(Comparator.comparing(Dish::getCalories))
        // 3. map Dish to Dish.getName
        .map(Dish::getName)
        // 4. collect
        .collect(Collectors.toList());
    System.out.println(nameList);
}

[season fruit, prawns, rice]
複製代碼

Stream的強大才剛剛開始……

Stream究竟是什麼

《Java 8 In Action》給出的解釋以下:

定義

  • Sequence of elements——Stream是一個元素序列,跟集合同樣,管理着若干類型相同的對象元素
  • Source——Stream沒法憑空產生,它從一個數據提供源而來,如集合、數組、I/O資源。值得一提的是,Stream中元素組織的順序將聽從這些元素在數據提供源中組織的順序,而不會打亂這些元素的既有順序。
  • Data processing operations——Stream提供相似數據庫訪問同樣的操做,而且只需傳入lambda表達式便可按照既定的行爲操縱數據,如filter(過濾)map(映射)reduce(聚合)find(查找)match(是否有數據匹配)sort(排序)等。Stream操做還能夠被指定爲串行執行或並行執行以充分利用多CPU核心。

特性

  • Pipelining——大多數Stream操做返回的是當前Stream對象自己,所以能夠鏈式調用,造成一個數據處理的管道,可是也有一些terminal操做會終結該管道,如調用Streamcollect方法後表示該數據處理流的終結,返回最終的數據集合
  • Internal iteration——Stream將元素序列的迭代都隱藏了,咱們只需提供數據處理流中的這一個階段到下一個階段的處理行爲(經過調用Stream方法傳入lambda表達式)。

上一節菜品的例子中數據處理流可描述以下(其中的limit表示取前n個元素):

Stream的併發

Stream內部集成了Java 7 提供的ForkJoin框架,當咱們經過調用它的parallel開啓並行執行開關時,Stream會將數據序列的處理經過ForkJoinPool進行並行化執行(不只僅是開啓多個線程,底層還會根據你CPU核心數量將子任務分配到不一樣的核心執行):

@Test
public void userJava8() {
    List<String> nameList = menu.stream().parallel()
        .filter(dish -> {
            System.out.println(Thread.currentThread().getName());
            return dish.getCalories() < 400;
        })
        .sorted(Comparator.comparing(Dish::getCalories))
        .map(Dish::getName)
        .collect(Collectors.toList());
    System.out.println(nameList);
}

ForkJoinPool.commonPool-worker-7
ForkJoinPool.commonPool-worker-2
ForkJoinPool.commonPool-worker-6
ForkJoinPool.commonPool-worker-5
ForkJoinPool.commonPool-worker-3
main
ForkJoinPool.commonPool-worker-4
ForkJoinPool.commonPool-worker-1
[season fruit, prawns, rice]
複製代碼

不然在當前線程串行化執行:

@Test
public void userJava8() {
    List<String> nameList = menu.stream()
        .filter(dish -> {
            System.out.println(Thread.currentThread().getName());
            return dish.getCalories() < 400;
        })
        .sorted(Comparator.comparing(Dish::getCalories))
        .map(Dish::getName)
        .collect(Collectors.toList());
    System.out.println(nameList);
}

main
main
main
main
main
main
main
main
main
[season fruit, prawns, rice]
複製代碼

Stream實例化

上文曾提到,Stream是沒法憑空而來的,須要一個數據提供源,而咱們最多見的就是數組和集合了。

做爲一個接口,咱們是沒法經過new Stream獲取其實例的,獲取其實例的經常使用方法有如下幾種

Collection#stream

經過Collection接口中的stream()方法,從一個集合實例中建立一個Stream,對同一個集合對象調用屢次stream()會產生不一樣的Stream實例。

開篇挑選蘋果的例子中,apples.stream就是調用了此方法

Arrays.stream(T[] arr)

  • 經過Arrays.stream(),由一個數組(引用類型或基本類型組)建立一個Stream

    @Test
    public void testCreateStream() {
        Arrays.stream(new Object[]{"hello",21,99.9,true}).forEach(System.out::println);
    }
    
    hello
    21
    99.9
    true
    複製代碼

Stream.of()

  • 經過Stream.of(T t)能夠建立一個僅包含一個元素的Stream
  • 經過Stream.of(T... t)能夠從一個變長參列(其實是一個數組)建立一個Stream

range/rangeClosed & generate

使用IntStream/LongStream/DoubleStream中的方法

IntStream中的range/rangeClosed (int, int)能夠建立一個包含指定開區間/閉區間中全部元素的StreamLongStream也同樣,但DoubleStream由於區間中的浮點數有無數個所以沒有此方法

IntStream.rangeClosed(0, 4).forEach(i -> {
    new Thread(() -> {
        System.out.println("I am a thread, my name is " + Thread.currentThread().getName());
    }, "t-" + i).start();
});

I am a thread, my name is t-0
I am a thread, my name is t-1
I am a thread, my name is t-2
I am a thread, my name is t-3
I am a thread, my name is t-4
複製代碼

generate(Supplier s),三者都有此方法,經過一個生產策略來建立一個無窮大的Stream,若是在此Stream上進行操做那麼每處理完一個元素都會經過調用s.get獲取下一個要處理的元素。

final int a = 0;
IntStream.generate(() -> a).forEach(System.out::println);
複製代碼

你會發現上述程序會一直打印0

經過generate建立的stream,若是在之上進行operation,那麼該processing會一直進行下去只有遇到異常時纔會中止

IntStream.generate(i::getAndIncrement).forEach(e -> {
    System.out.println(e);
    if (e == 5) {
        throw new RuntimeException();
    }
});

0
1
2
3
4
5

java.lang.RuntimeException
複製代碼

Stream Operation詳解

Stream的數據操縱方法分爲terminalnon-terminalnon-terminal操做以後能夠繼續鏈式調用其餘operation,造成一個流式管道,而terminal操做則會終止數據流的移動。Stream中方法返回Stream(實際返回的是當前Stream實例自己)的都是non-terminal option

filter——過濾

調用Streamfilter並傳入一個Predicate能夠對元素序列進行過濾,丟棄不符合條件(是否符合條件根據你傳入的Predicate進行判斷)的元素

// print the even number between 1 and 10
IntStream.rangeClosed(1, 10).filter(i -> i % 2 == 0).forEach(System.out::println);

2
4
6
8
10
複製代碼

distinct——去重

@Test
public void testDistinct() {
    Stream.of("a", "b", "a", "d", "g", "b").distinct().forEach(System.out::println);
}

a
b
d
g
複製代碼

distinct會根據equals方法對「相等」的對象進行去重。

skip——去除前n個元素

至關於SQLlimit <offset,rows>語句中的offset

public void testSkip() {
    IntStream.rangeClosed(1,10).skip(6).forEach(System.out::println);
}

7
8
9
10
複製代碼

limit——取前n個元素,丟棄剩餘的元素

至關於SQLlimit <offset,rows>語句中的rows

// 丟棄前6個以後Stream只剩下7~10了,再截取前2個,Stream就只有7和8了
IntStream.rangeClosed(1, 10).skip(6).limit(2).forEach(System.out::println);

7
8
複製代碼

map——映射

map方法接收一個Function(接收一個對象,返回一個對象),返回什麼對象、需不須要藉助傳入的對象信息就是映射的邏輯,根據你的所需你能夠將Stream中的全部元素統一換一個類型。

// 從一份菜單(菜品集合)中提取菜品名稱清單
List<String> nameList = menu.stream().map(dish -> dish.getName()).collect(Collectors.toList());
System.out.println(menu);

[pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon]
複製代碼

flatMap——扁平化

flatMap能夠將你的元素進一步細粒度化,如從某個文件讀取文件內容後根據換行符分割建立一個以「行」爲元素的Stream,若是你想進一步將「行」按照「空格」分割獲得以「字詞」爲元素的Stream,那麼你就可使用flatMap

你須要傳入一個Function<T,Stream>:傳入一個元素,返回一個包含由該元素分割造成若干細粒度元素的Stream

Stream.of("hello", "world").
    flatMap(str -> Stream.of(str.split(""))).
    forEach(System.out::println);

h
e
l
l
o
w
o
r
l
d
複製代碼

sorted & Comparator.comparing

sort須要你傳入一個定義了排序規則的Comparator,它至關於一個BiPredicate

// 將菜品按照卡路里升序排序
menu.stream().
    sorted((dish1,dish2)->dish1.getCalories()-dish2.getCalories())
    .map(Dish::getCalories)
    .forEach(System.out::println);

120
300
350
400
450
530
550
700
800
複製代碼

你會發現IDEA提示你(dish1,dish2)->dish1.getCalories()-dish2.getCalories()能夠簡化爲Comparator.comparingInt(Dish::getCalories),使用comparing你能夠直接傳入排序字段如getColaries,它會自動幫咱們封裝成一個升序的BiPredicate,若是你須要降序則再鏈式調用reversed便可:

menu.stream().
    sorted(Comparator.comparingInt(Dish::getCalories).reversed())
    .map(Dish::getCalories)
    .forEach(System.out::println);

800
700
550
530
450
400
350
300
120
複製代碼

count——返回當前Stream中的元素個數

match——判斷Stream中是否有符合條件的元素

這是一個terminal option,當Stream調用match以後會返回一個布爾值,match會根據你傳入的Predicate返回true or false,表示當前Stream中的全部元素是否存在至少一個匹配(anyMatch)、是否所有匹配(allMatch)、是否全都不匹配(noneMatch)。

這幾僅以anyMatch的使用示例:

// 菜單中是否有卡路里小於100的菜品
boolean res = menu.stream().anyMatch(dish -> dish.getCalories() < 100);
System.out.println(res);

false
複製代碼

find

這也是一個terminal operation。一般用於在對Stream進行一系列處理以後剩下的元素中取出一個進行消費。

  • findAny,一般用於並行處理Stream時,獲取最早走完整個數據處理流程的元素。

    好比對於一個id,可能會開幾個線程並行地調接口、查數據庫、查緩存、查ES的方式獲取商品數據,但只要有其中一個方式成功返回數據那麼就直接消費這個數據,其餘方式不予等待。

    static Random random = new Random();
    
    public Object queryById(Integer id){
        // 隨機睡眠0~6秒,模仿從數據庫或者調接口獲取數據的過程
        try {
            TimeUnit.MILLISECONDS.sleep(random.nextInt(6000));
            String data = UUID.randomUUID().toString();
            System.out.println("get data -> " + data + "[id=" + id + "]");
            return data;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return e;
        }
    
    }
    
    @Test
    public void testFindAny() throws InterruptedException {
        Optional<Object> dataOptional = Stream.of(1, 2, 3, 4, 5)
            // 對於每一個id並行獲取對應的數據
            .parallel()
            .map(id -> {
                Object res = queryById(id);
                if ( res instanceof Throwable) {
                    throw new RuntimeException();
                }
                return res;
            })
            // 有一個拿到了就直接用,後面到的無論了
            .findAny();
        dataOptional.ifPresent(data -> System.out.println("consume data : " + data));
        Thread.currentThread().join();
    }
    
    get data -> 6722c684-61a6-4065-9472-a58a08bbc9d0[id=1],spend time : 442 ms
    get data -> 1975f02b-4e54-48f5-9dd0-51bf2789218e[id=2],spend time : 1820 ms
    get data -> fd4bacb1-a34d-450d-8ebd-5f390167f5f8[id=4],spend time : 4585 ms
    get data -> b2336c45-c1f9-4cd3-b076-83433fdaf543[id=3],spend time : 4772 ms
    get data -> fcc25929-7765-467a-bf36-b85afab5efe6[id=5],spend time : 5575 ms
    consume data : 6722c684-61a6-4065-9472-a58a08bbc9d0
    複製代碼

    上面的輸出中consume data始終都是spend time最短的數據

  • findFirst則必須等到Stream中元素組織順序(初始時是數據提供源的元素順序,若是你調用了sorted那麼該順序就會變)的第一個元素處理完前面的流程而後消費它:

    Optional<Object> dataOptional = Stream.of(1, 2, 3, 4, 5)
        // 對於每一個id並行獲取對應的數據
        .parallel()
        .map(id -> {
            Object res = queryById(id);
            if ( res instanceof Throwable) {
                throw new RuntimeException();
            }
            return res;
        })
        // 先拿到的先用,後面到的無論了
        .findFirst();
    dataOptional.ifPresent(data -> System.out.println("consume data : " + data));
    Thread.currentThread().join();
    
    get data -> d6ac2dd6-66b6-461c-91c4-fa2f13326210[id=4],spend time : 1271 ms
    get data -> 560f5c0e-c2ac-4030-becc-1ebe51ebedcb[id=5],spend time : 2343 ms
    get data -> 11925a9f-03e8-4136-8411-81994445167e[id=1],spend time : 2825 ms
    get data -> 0ecfa02e-3903-4d73-a18b-eb9ac833a899[id=2],spend time : 3270 ms
    get data -> 930d7091-cfa6-4561-a400-8d2e268aaa83[id=3],spend time : 5166 ms
    consume data : 11925a9f-03e8-4136-8411-81994445167e
    複製代碼

    consume data始終都是調用findFirst時在Stream排在第一位的元素。

reduce——聚合計算

reduce是對當前Stream中的元素進行求和、求乘積、求最大/小值等須要遍歷全部元素得出一個結果的聚合操做。它有以下重載方法:

  • Optional<T> reduce(BinaryOperator<T> accumulator);

    經過accumulatorStream中的元素作聚合操做,返回一個包裝了操做結果的Optional,經過該Optional能夠拿到該結果以及判斷該結果是否存在(若是Stream沒有元素,那麼天然也就沒有聚合結果了)

    accumulator是一個BiFunction,至關於你在遍歷時訪問相鄰的兩個元素得出一個結果,這樣Stream就能依據此邏輯遍歷全部元素獲得最終結果

    Optional<Dish> dishOptional = menu.stream()
        .filter(dish -> dish.getCalories() > 600)
        .reduce((d1, d2) -> d1.getCalories() < d2.getCalories() ? d1 : d2);
    if (dishOptional.isPresent()) {
        Dish dish = dishOptional.get();
        System.out.println("大於600卡路里的菜品中,卡路里最小的是:" + dish);
    } else {
        System.out.println("沒有大於600卡路里的菜品");
    }
    
    大於600卡路里的菜品中,卡路里最小的是:beef
    
    OptionalInt reduce = IntStream.rangeClosed(1, 10)
                    .reduce((i, j) -> i + j);	// -> method inference: Integer::sum
            reduce.ifPresent(System.out::println);
    55
    複製代碼

    方法邏輯的僞代碼以下:

    if(stream is emtpy){
        return optional(null)
    }else{
        result = new Element()
        foreach element in stream
            result = accumulator.apply(element, result);
        return optional(result)
    }
    複製代碼
  • T reduce(T identity, BinaryOperator<T> accumulator);

    此方法增長了一個identity,它的做用是若是調用reduce時當前Stream中沒有元素了,也應該返回一個identity做爲默認的初始結果。不然,調用空的Optionalget會報錯:

    OptionalInt reduce = IntStream.rangeClosed(1, 10)
        .filter(i -> i > 10)
        .reduce(Integer::sum);
    System.out.println(reduce.isPresent());
    System.out.println(reduce.getAsInt());
    
    
    false
    java.util.NoSuchElementException: No value present
    複製代碼

    若是加了identityreduce不管如何都將返回一個明確的結果(若是有元素就返回聚合後的結果,不然返回identity),而不是一個結果未知的Optional

    int res = IntStream.rangeClosed(1, 10)
    	.filter(i -> i > 10)
    	.reduce(0, Integer::sum);
    System.out.println(res);
    
    0
    複製代碼

    值得注意的是,identity的值不該該隨便給出,給出的規則應該符合:若是Stream中有元素,那麼對於任意元素element,給定的identity應該知足accumulator.apply(element, result),不然會出現以下使人困惑的現象:

    int res = IntStream.rangeClosed(1, 10).reduce(1, (i, j) -> i * j);
    System.out.println(res);	//3628800
    
    int res = IntStream.rangeClosed(1, 10).reduce(0, (i, j) -> i * j);
    System.out.println(res);	//0
    複製代碼

    上面給定的identity1accumulator邏輯爲累乘時,知足對於任意元素e都有e * 1 == e,所以不會影響聚合邏輯;而將identity換成0,不管Stream中有什麼元素,聚合結果都爲0。這是由reduce(identity,accumulator)的方法邏輯致使的,其僞代碼以下:

    if(stream is emtpy){
        return optional(identity)
    }else{
        result = identity
        foreach element in stream
            result = accumulator.apply(element, result);
        return optional(result)
    }
    複製代碼

    能夠發現,首先將聚合結果result置爲identity,而後將每一個元素累乘到result中(result = element * result),因爲任何數乘零都得零,所以聚合結果始終返回0了。

  • <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

    此方法又多了一個combiner,這個combiner是爲了支持並行的,若是是在串行狀態下調用那麼傳入的combiner不會起到任何做用:

    String reduce = Stream.of("h", "e", "l", "l", "o").reduce("", (a, b) -> a + b);
    System.out.println(reduce);
    
    hello
    複製代碼

    但若是是串行狀態下調用,那麼combiner會根據ForkJoin機制和Stream使用的Splierator在子任務回溯合併時對合並的兩個子結果作一些處理:

    String result = Stream.of("h", "e", "l", "l", "o")
        .parallel()
        .reduce("", (a, b) -> a + b, (a, b) -> a + b + "=");
    System.out.println(result);		//he=llo===
    複製代碼

    咱們能夠在combiner中查看當前合併的兩個子結果:

    String result = Stream.of("h", "e", "l", "l", "o")
        .parallel()
        .reduce("", (a, b) -> a + b, (a, b) -> {
            System.out.println("combine " + a + " and " + b + ", then append '='");
            return a + b + "=";
        });
    System.out.println("the final result is :" + result);
    
    
    combine l and o, then append '='
    combine l and lo=, then append '='
    combine h and e, then append '='
    combine he= and llo==, then append '='
    the final result is :he=llo===
    複製代碼

collect——收集元素到集合、分組、統計等

collect須要傳入一個CollectorCollectors爲咱們封裝了不少Collector的默認實現。

例如

  • collect(Collectors.toList()),能夠將元素收集到一個List中並返回,相似的有toSet
  • collect(Collectors.joining())能將Stream中的字符串元素拼接起來並返回
  • collect(Collectors.groupBy())可以實現分組
  • ...其中的大多數API都有着類SQL的外貌,你能夠很容易理解它

Stream In Action

下面經過一個業務員Trader和交易Transaction的例子來鞏固Stream的運用。

兩個類定義以下:

public class Trader{
    private final String name;
    private final String city;
    public Trader(String n, String c){
        this.name = n;
        this.city = c;
    }
    public String getName(){
        return this.name;
    }
    public String getCity(){
        return this.city;
    }
    public String toString(){
        return "Trader:"+this.name + " in " + this.city;
    }
}

public class Transaction{

    private final Trader trader;
    private final int year;
    private final int value;
    public Transaction(Trader trader, int year, int value){
        this.trader = trader;
        this.year = year;
        this.value = value;
    }
    public Trader getTrader(){
        return this.trader;
    }
    public int getYear(){
        return this.year;
    }
    public int getValue(){
        return this.value;
    }
    @Override
    public String toString(){
        return "{" + this.trader + ", " +
                "year: "+this.year+", " +
                "value:" + this.value +"}";
    }
}
複製代碼

需求以下:

/**
* 1. Find all transactions in the year 2011 and sort them by value (small to high).
* 2. What are all the unique cities where the traders work?
* 3. Find all traders from Cambridge and sort them by name.
* 4. Return a string of all traders’ names sorted alphabetically.
* 5. Are any traders based in Milan?
* 6. Print all transactions’ values from the traders living in Cambridge.
* 7. What’s the highest value of all the transactions?
* 8. Find the transaction with the smallest value.
*/
複製代碼

代碼示例:

//1. Find all transactions in the year 2011 and sort them by value (small to high).
List<Transaction> transactions1 = transactions.stream()
    .filter(transaction -> transaction.getYear() == 2011)
    .sorted(Comparator.comparing(Transaction::getValue))
    .collect(Collectors.toList());
System.out.println(transactions1);

//2. What are all the unique cities where the traders work?
String value = transactions.stream()
    .map(transaction -> transaction.getTrader().getCity())
    .distinct()
    .reduce("", (c1, c2) -> c1 + " " + c2);
System.out.println(value);

//3. Find all traders from Cambridge and sort them by name.
transactions.stream()
    .filter(transaction -> "Cambridge".equals(transaction.getTrader().getCity()))
    .map(Transaction::getTrader)
    .sorted(Comparator.comparing(Trader::getName))
    .forEach(System.out::println);

//4. Return a string of all traders’ names sorted alphabetically.
transactions.stream()
    .map(transaction -> transaction.getTrader().getName())
    .distinct()
    .sorted()
    .forEach(System.out::println);

//5. Are any traders based in Milan?
boolean res = transactions.stream()
    .anyMatch(transaction -> "Milan".equals(transaction.getTrader().getCity()));
System.out.println(res);

//6. Print all transactions’ values from the traders living in Cambridge.
transactions.stream()
    .filter(transaction -> "Cambridge".equals(transaction.getTrader().getCity()))
    .map(Transaction::getValue)
    .forEach(System.out::println);

//7. What’s the highest value of all the transactions?
Optional<Integer> integerOptional = transactions.stream()
    .map(Transaction::getValue)
    .reduce(Integer::max);
System.out.println(integerOptional.get());

//8. Find the transaction with the smallest value.
integerOptional = transactions.stream()
    .map(Transaction::getValue)
    .reduce(Integer::min);
System.out.println(integerOptional.get());
複製代碼

Optional——規避空指針異常

其實上節介紹reduce的使用時,就有Optional的身影,若是你沒有給出identity,那麼reduce會給你返回一個Optional,如此Optional的身份(拿到這個類的實例,你會立馬條件反射:要在訪問對象以前判斷一下Optional包裝的對象是否爲null)會提醒你進行非空判斷,不至於你拿着reduce返回的null去使用從而致使空指針異常。

這種將非空判斷交給API的機制,可以讓咱們沒必要每次拿到對象的時候都要爲其是否爲空而提心吊膽,又或十分敏感地每拿到一個對象都進行一下if (obj != null)

有了Optional之後,避免空指針的兩個點轉變以下:

  • 方法返回值
    • 以前,儘可能返回非null的對象,如空字符串"",空數組等
    • 返回Optional,如有結果則返回Optional.ofNullable(obj),若想返回null則用Optional.empty()
  • 使用方法返回值
    • 以前,調用方法拿到返回值,在使用以前要記得非空判斷
    • 以後,調用方法拿到的都是Optional,在使用以前它會提醒咱們使用isPresentifPresent規避空指針

說白了,以前須要咱們人爲的記住非空判斷,但引入Optional後,非空判斷流程交給API了,卸去了咱們對非空判斷的關注點,規範了流程開發。

Optional的建立、使用、非空判斷

相關源碼以下:

private static final Optional<?> EMPTY = new Optional<>();
private final T value;

private Optional() {
    this.value = null;
}

private Optional(T value) {
    this.value = Objects.requireNonNull(value);
}

public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}
public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}

public T get() {
    if (value == null) {
        throw new NoSuchElementException("No value present");
    }
    return value;
}
public T orElse(T other) {
    return value != null ? value : other;
}
public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
    if (value != null) {
        return value;
    } else {
        throw exceptionSupplier.get();
    }
}

public boolean isPresent() {
    return value != null;
}

public void ifPresent(Consumer<? super T> consumer) {
    if (value != null)
        consumer.accept(value);
}
複製代碼

你會發現其實他的源碼很簡單,就是將咱們要使用的對象包了一層

  • 經過of(非空對象)ofNullable(可爲空對象)來建立Optional實例

  • 經過isPresent能夠判斷內部對象是否爲null

  • 經過get能夠獲取內部對象,若是內部對象爲null則會拋異常,所以一般在調用get前要使用isPresent判斷一下

    if(optional.isPresent()){
        obj = optional.get();
    }
    複製代碼
  • orElse,若是不爲null則返回,不然

    • orElse(T t),返回你傳入的對象t
    • orElseGet(Supplier<T> s),調用s.get獲取一個對象
    • orElseGet(Supplier<Throwable> e)
  • 經過ifPresent(consumer),能夠整合判斷和對象訪問,若是對象不爲null,那就用傳入的consumer消費它

    appleOptional.ifPresent(System.out.println(apple.getName()))
    複製代碼

此外,Optional中還提供了filtermap兩個從Stream中借鑑的方法,做用相似,可自行查看源碼。

其實我還想說CompletableFutrueLocalDate/LocalTime/LocalDateTime...

參考資料

相關文章
相關標籤/搜索