1. 程式人生 > >Java8新特性整理之流的介紹與使用

Java8新特性整理之流的介紹與使用

Java8新特性整理之流的介紹與使用

 

流是什麼

官方定義:支援順序和並行聚合操作的元素序列。

這裡有幾個關鍵詞,順序、並行、聚合、元素序列。

所謂順序就是單執行緒順序執行,並行就是多執行緒分解執行,聚合就是將順序或並行執行的結果計算後得出最終結果,元素序列則是將資料來源(陣列,檔案,集合等)流化後的資料結構。

流與集合

上面說的還是有些不明朗,下面結合Java中的集合(Collection)來進一步解釋流。

Java現有的集合概念和新的流概念都提供了介面,來配合代表元素型有序值的資料介面。所
謂有序,就是說我們一般是按順序取用值,而不是隨機取用的。

舉例來說,如果要觀看蒼老師課程,有兩種方式,一種是下載到本地觀看,另一種是線上觀看。而下載觀看的方式等待下載的時間比較長同時佔用磁碟空間較大;線上觀看就比較快了,可以只觀看高潮部分,而且只佔用很少的緩衝空間。

對比來看,蒼老師課程是位元組或幀的資料結構,用集合方式處理的話(下載到本地觀看)需要將整個結構中的資料都計算一遍,而用流的方式處理的話(線上觀看高潮部分),只需要計算某個位元組(幀)範圍。

區別

  • 集合是記憶體資料結構,可以增刪元素,而流是概念上固定的資料結構,不可以增刪元素,只進行計算。
  • 集合可以多次遍歷,而流只能遍歷一次(下一次需要從資料來源再獲得一個新的流)。
  • 集合使用外部迭代(如for-each),而流使用內部迭代(流內部幫你把迭代做了)。
  • 關鍵區別,集合是有界的,流可以是無界的。

流的操作

java.util.stream.Stream中的Stream介面定義了許多操作。它們可以分為兩大類。

  • 中間操作,諸如filter或sorted等中間操作會返回另一個流,即返回值為Stream的方法。
  • 終端操作,終端操作會從流的流水線生成結果,其結果是任何不是流的值,即返回值不為Stream的方法。

使用流

流的使用一般包括三件事:
- 一個數據源(如集合)來執行一個查詢;
- 一箇中間操作鏈,形成一條流的流水線;
- 一個終端操作,執行流水線,並能生成結果。

下面介紹下流中的常用方法:

中間操作常用方法

filter方法:

接受一個返回boolean的Lambda表示式的引數,返回由流元素組成的流,該流與給定謂詞匹配。

distinct方法:

返回由不同物件組成的流,內部使用物件的equals方法比較是否相同。

skip方法:

接受一個long型別的引數n,表示頭n個數,返回由剩下的元素組成的流,如果流容器中的元素比n小,則返回空的流。

limit方法:

接受一個long型別的引數maxSize,表示限制的最大數量,返回一個不超過maxSize長度的流。

sorted方法:

接受一個Comparator型別的引數,表示函式引用作為引數,返回根據Comparator介面中定義的行為組成排序後的流。

map方法:

接收一個函式(方法引用)作為引數,返回一個流,由將給定函式應用於該流元素的結果組成。

終端操作常用方法

anyMatch方法:

流中是否有一個元素能匹配給定的謂詞

allMatch方法:

流中的元素是否都能匹配給定的謂詞。

noneMatch方法:

和allMatch相對的是noneMatch。它可以確保流中沒有任何元素與給定的謂詞匹配。

findAny方法:

返回當前流中的任意元素。

findFirst方法:

找到第一個元素。

forEach方法:

遍歷流中的每一個元素。

collect方法:

接受一個Collectors類中的方法(收集器)作為引數,返回一個歸約的結果。

collect是一個終端操作,它接受的引數是將流中元素累積到彙總結果的各種方式(稱為收集器)。

reduce方法:

把一個流中的元素反覆結合起來,返回一個歸約的結果(將流歸約成一個值)。

count方法:

返回流中的元素個數。

舉個例子

前面都是理論知識,下面舉個栗子:

Dish.java

public class Dish {
    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;
    }

    public enum Type {
        MEAT, FISH, OTHER;
    }
}

初始化資料:

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));

使用:

List<String> dishes = menu.stream()
                .filter(dish -> dish.getCalories() > 300)   // 從流中過濾元素
                .map(Dish::getName)                         // 提取元素
                .limit(3)                                   // 截斷流,使其元素不超過給定的數量
                .collect(toList());                         // 將流轉換為列表  

上面例子會取出卡路里大於300的前三個Dish的名字列表。

數值流

下面來談談流的特化 – 數值流

對前面Dish中的菜的卡路里求和:

int calories = menu.stream()
            .map(Dish::getCalories)
            .reduce(0, Integer::sum);

reduce方法第一個引數表示初始值,第二個引數代表接受兩個引數的函式,Integer的sum方法接受兩個引數,所以可以傳遞一個方法引用。

乍一看,這個方法好像沒什麼問題,輸出結果也正確。

但你其實忽略了一個問題,map方法會返回一個Stream型別的流,其中T是引用型別,所以它有一個暗含的裝箱成本,會造成效能的降低。

怎麼解決上面的問題呢?

Java 8引入了三個原始型別特化流介面來解決這個問題:IntStream、DoubleStream和LongStream,分別將流中的元素特化為int、long和double,從而避免了暗含的裝箱成本。每個介面都帶來了進行常用數值歸約的新方法,比如對數值流求和的sum,找到最大元素的max。

現在改正上面的程式碼:

int calories = menu.stream()
            .mapToInt(Dish::getCalories)
            .sum();

當然,如有你需要轉換回物件流,則需要呼叫原始型別特化流介面的boxed方法進行裝箱。

什麼是並行流

並行流就是一個把內容分成多個數據塊,並用不同的執行緒分別處理每個資料塊的流。

這樣一來,你就可以自動把給定操作的工作負荷分配給多核處理器的所有核心,讓它們都忙起來。

可以通過對收集源呼叫parallelStreamparallel方法來把集合轉換為並行流:

 int calories = menu.parallelStream()
                .mapToInt(Dish::getCalories)
                .sum();

 int calories = menu.stream()
                .parallel()
                .mapToInt(Dish::getCalories)
                .sum();

下表按照可分解性總結了一些流資料來源適不適於並行。

可分解性
ArrayList 極佳
LinkedList
IntStream.range 極佳
Stream.iterate
HashSet
TreeSet