Java---流式編程

集合優(yōu)化了對象的存儲,而流則是對數(shù)據(jù)的處理

流是一系列與特定存儲機制無關(guān)的元素,利用流,我們無需迭代集合中的元素,就可以提取和操作它們。這些管道通常被組合在一起,在流上形成一條操作管道。

使用流的一個核心好處是,它使得程序更加短小并且更易理解。同時流還可以同 Lambda 表達式和方法引用(method references)一起使用。

流一個重要的特性是:流是懶加載的,意味著它在絕對必要的情況下才計算。這個特性是我們可以表示非常大(甚至無限)的序列,而不用擔心內(nèi)存的問題。

流的支持

Java 設(shè)計者在設(shè)計流時面臨著這樣一個難題:現(xiàn)存的大量類庫不僅為 Java 所用,同時也被應(yīng)用在整個 Java 生態(tài)圈數(shù)百萬行的代碼中。如何將一個全新的流的概念融入到現(xiàn)有類庫中呢?

Java 8 采用的解決方案是:在接口中添加被 default默認)修飾的方法。通過這種方案,設(shè)計者們可以將流式(stream)方法平滑地嵌入到現(xiàn)有類中。流方法預(yù)置的操作幾乎已滿足了我們平常所有的需求。

流的操作有三種:創(chuàng)建流、修改流元素(中間操作)、消費流(終端操作)

流的創(chuàng)建:

流的創(chuàng)建方式有以下這幾種方式:

  • Stream.of():需要傳入一組元素參數(shù)。
public class StreamOf {
    public static void main(String[] args) {
        Stream.of("It's ", "a ", "wonderful ", "day ", "for ", "pie!");
            .forEach(System.out::println);
    }
}
// 中間需要傳入一組元素參數(shù)
// 代碼中傳入的是一組字符串元素,forEach() 傳入一個方法引用。
  • stream(): 對于集合類,可以直接調(diào)用集合類中的 stream()方法:
List<String> list = Arrays.asList("It's a wonderful day for pie!".split(" "))
list.stream()
    .forEach(System.out::print);
  • Stream.generate(): 需要傳入一個 Supplier<T> ,Suppier 是一個通用的函數(shù)式接口,所以可以配合Lambda 使用。
// Stream.generate() 中間傳入的參數(shù) Generator 類實現(xiàn)Supplier 接口,
//所以實現(xiàn)的 get(),Stream.generate(new Generator()) 會自動調(diào)用 get() 產(chǎn)生所需要的流元素。
public class Generator implements Supplier<String> {
    Random rand = new Random(47);
    char[] letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();

    public String get() {
        return "" + letters[rand.nextInt(letters.length)];
    }

    public static void main(String[] args) {
        String word = Stream.generate(new Generator())
                            .limit(30)
                            .collect(Collectors.joining());
        System.out.println(word);
    }
}

搭配 lambda 表達式使用:傳入與 supplier 接口中 get() 方法簽名相同的方法引用

Stream.generate(() -> "duplicate")
              .limit(3)
              .forEach(System.out::println);
// 輸出
// “duplicate”
// “duplicate”
// “duplicate”
  • Stream.iterate(): 該函數(shù)有兩個參數(shù),以種子(第一個參數(shù))開頭,并將其傳給方法(第二個參數(shù),需要傳入一個方法引用)。方法的結(jié)果將添加到流,并存儲作為第一個參數(shù)用于下次調(diào)用 iterate()。
基本類型流的生成
  • InStream.range(): 生成整型序列的流,range() 需要傳入兩個參數(shù),指定一個序列范圍。
  • LongStream:生成 Long 序列的流
  • DoubleStream:生成 double 序列的流
public class Ranges {
    public static void main(String[] args) {
      System.out.println(range(10, 20).sum());
    }
}
// sum() :用于將流中的元素相加求和
數(shù)組流生成
  • Arrays.stream(數(shù)組) :利用 Arrays 類中含有一個名為 stream() 的靜態(tài)方法將數(shù)組轉(zhuǎn)換成為流
Arrays.stream(new double[] { 3.14159, 2.718, 1.618 })
            .forEach(n -> System.out.format("%f ", n));

同時也可以

 Arrays.stream(new int[] { 1, 3, 5, 7, 15, 28, 37 }, 3, 6)

此時Arrays.stream() 的調(diào)用有兩個額外的參數(shù)。第一個參數(shù)告訴 stream() 從數(shù)組的哪個位置開始選擇元素,第二個參數(shù)用于告知在哪里停止。

  • splitAsStream() :Java 8 在 java.util.regex.Pattern 中增加的一個新的方法。這個方法可以根據(jù)傳入的公式將字符序列轉(zhuǎn)化為流。但是有一個限制,輸入只能是 CharSequence,因此不能將流作為 splitAsStream() 的參數(shù)。
中間操作

中間操作,用于從一個流中獲取對象,并進行處理,再將對象作為另一個流從后端輸出,以連接到其他操作。

查看流中元素

peek(): 該操作的目的是幫助調(diào)試。它允許你無修改地查看流中的元素。因為 peek() 符合無返回值的 Consumer 函數(shù)式接口,所以我們只能觀察,無法使用不同的元素來替換流中的對象

排序

sorted() 操作排序,使用默認比較器實現(xiàn),另一種形式是傳入一個比較器。另一種形式的實現(xiàn):傳入一個 Comparator 參數(shù)。

移除元素

distinct(): 可用于消除流中的重復(fù)元素。相比創(chuàng)建一個 Set 集合,該方法的工作量要少得多。

filter(Predicate):過濾操作會保留與傳遞進去的過濾器函數(shù)計算結(jié)果為 true 元素。

isPrime() 作為過濾器函數(shù),用于檢測質(zhì)數(shù)。

應(yīng)用函數(shù)到元素
  • map() 會獲取流中的所有元素,并且對流中元素應(yīng)用函數(shù)操作從而產(chǎn)生新的元素,并將其傳遞到后續(xù)的流中。

  • mapToInt(ToIntFunction):操作同上,但結(jié)果是 IntStream。

  • mapToLong(ToLongFunction):操作同上,但結(jié)果是 LongStream。

  • mapToDouble(ToDoubleFunction):操作同上,但結(jié)果是 DoubleStream。

使用 map 也可以產(chǎn)生和接收類型不同的類型
map(類::new) 傳入一個類的構(gòu)造器。可以把接收類型轉(zhuǎn)換為其他類。例如 int 轉(zhuǎn)成一個自定義的類型。

在 flatMap 中組合流

map() 函數(shù)的功能是產(chǎn)生流,而如果我們傳入的元素是流,則會產(chǎn)生元素流的流。
此時可以利用 flatMap() :該函數(shù)功能與 map 一樣。但不同是該函數(shù)會把流扁平化為元素,最終產(chǎn)生的是元素。

public class FlatMap {
    public static void main(String[] args) {
        Stream.of(1, 2, 3)
        .flatMap(i -> Stream.of("Gonzo", "Fozzie", "Beaker"))
        .forEach(System.out::println);
    }
}
其他操作:
  • ints(): 方法產(chǎn)生一個流并且 ints() 方法有多種方式的重載 — 兩個參數(shù)限定了數(shù)值產(chǎn)生的邊界.
    相應(yīng)其他數(shù)據(jù)類型的操作:longs()

  • limit() : 操作獲取指定數(shù)量的元素
    limit(8):或者前 8 個元素。

  • forEach() 方法遍歷輸出,它根據(jù)傳遞給它的函數(shù)對每個流對象執(zhí)行操作。
    通常使用該方法傳入 System.out::println 來遍歷輸出流中的每個元素。

  • boxed(): 流操作將會自動地把基本類型包裝成為對應(yīng)的裝箱類型, 對應(yīng)解箱 unboxed()

  • skip() 操作 : 它根據(jù)參數(shù)丟棄指定數(shù)量的流元素

Optional 對象

一些標準流返回 Optional 對象,此對象可以作為流的持有者,即使查看的元素不存在也能友好的提示我們。

流中的一些操作方法

  • findFirst() 返回一個包含第一個元素的 Optional 對象,如果流為空則返回 Optional.empty

  • findAny() 返回包含任意元素的 Optional 對象,如果流為空則返回 Optional.empty

  • max() 和 min() 返回一個包含最大值或者最小值的 Optional 對象,如果流為空則返回 Optional.empty

class OptionalsFromEmptyStreams {
    public static void main(String[] args) {
        System.out.println(Stream.<String>empty()
             .findFirst());
    }
}
// 輸出:Optional.empty

當流為空時,返回 Optional.empty 對象。
空流可以通過 Stream.<String>empty() 創(chuàng)建,也可以運用 Stream.empty(),使用此方法時,需要賦值給一個指定的類型的變量,不然 java 編輯器無法進行類型推導(dǎo),即

Stream<String> s = Stream.empty()

當接收到 Optional 對象時,應(yīng)首先調(diào)用 isPresent()檢查其中是否包含元素。如果存在,可使用 get() 獲取。

class OptionalBasics {
    static void test(Optional<String> optString) {
        if(optString.isPresent())
            System.out.println(optString.get()); 
        else
            System.out.println("Nothing inside!");
    }
    public static void main(String[] args) {
        test(Stream.of("Epithets").findFirst());
        test(Stream.<String>empty().findFirst());
    }
}
便利函數(shù)

通常對于流返回的 Optional對象,我們需要進行解包,才能獲取到里面的元素。
流中提供了一些便利函數(shù),用于解包 Optional 對象,簡化了 “對所包含的對象的檢查和執(zhí)行操作”:

  • ifPresent(Consumer):當值存在時調(diào)用Consumer,否則什么也不做。
  • orElse(otherObject):如果值存在則直接返回,否則生成 otherObject。
  • orElseGet(Supplier):如果值存在則直接返回,否則使用 Supplier 函數(shù)生成一個可替代對象。
  • orElseThrow(Supplier):如果值存在直接返回,否則使用 Supplier 函數(shù)生成一個異常。
創(chuàng)建 Optional

當需要在代碼中添加 Optional對象時,可以利用 Optional對象的靜態(tài)方法來創(chuàng)建:

  • empty():生成一個空 Optional。
  • of(value):將一個非空值包裝到 Optional 里。
  • ofNullable(value):針對一個可能為空的值,為空時自動生成 Optional.empty,否則將值包裝在 Optional中。
Optional 對象的操作

當流管道生成了 Optional 對象,可以使用下列一些方法對對象進行操作。

  • filter(Predicate):將 Predicate 應(yīng)用于 Optional 中的內(nèi)容并返回結(jié)果。當 Optional 不滿足 Predicate 時返回空。如果 Optional 為空,則直接返回。

  • map(Function):如果 Optional不為空,應(yīng)用 FunctionOptional 中的內(nèi)容,并返回結(jié)果。否則直接返回 Optional.empty。

  • flatMap(Function):同 map(),但是提供的映射函數(shù)將結(jié)果包裝在 Optional 對象中,因此 flatMap() 不會在最后進行任何包裝。

一般來說,流的 filter() 會在 Predicate 返回 false 時移除流元素。而Optional.filter()在失敗時不會刪除 Optional,而是將其保留下來,并轉(zhuǎn)化為空。

終端操作
數(shù)組
  • toArray():將流轉(zhuǎn)換成適當類型的數(shù)組。
  • toArray(generator):在特殊情況下,生成自定義類型的數(shù)組。
循環(huán)
  • forEach(Consumer)常見如 System.out::println 作為 Consumer 函數(shù)。
  • forEachOrdered(Consumer): 保證 forEach 按照原始流順序操作
集合
  • collect(Collector):使用 Collector 收集流元素到結(jié)果集合中。
  • collect(Supplier, BiConsumer, BiConsumer):同上,第一個參數(shù) Supplier 創(chuàng)建了一個新結(jié)果集合,第二個參數(shù) BiConsumer 將下一個元素包含到結(jié)果中,第三個參數(shù) BiConsumer 用于將兩個值組合起來。

Collectors 還有其他更加復(fù)雜的實現(xiàn),可以通過查看文檔了解。
例如:實現(xiàn)將流元素收集到 TreeSet 中的集合。可以利用將集合的的構(gòu)造函數(shù)引用傳遞給 Collectors.toCollection(),從而構(gòu)建任何類型的集合

public class TreeSetOfWords {
    public static void
    main(String[] args) throws Exception {
        Set<String> words2 =
                Files.lines(Paths.get("TreeSetOfWords.java"))
                        .flatMap(s -> Arrays.stream(s.split("\\W+")))
                        .filter(s -> !s.matches("\\d+")) // No numbers
                        .map(String::trim)
                        .filter(s -> s.length() > 2)
                        .limit(100)
                        .collect(Collectors.toCollection(TreeSet::new));
        System.out.println(words2);
    }
}

Files.lines() 打開 Path 并將其轉(zhuǎn)換成為行流。

組合
  • reduce(BinaryOperator):使用 BinaryOperator 來組合所有流中的元素。因為流可能為空,其返回值為 Optional。
  • reduce(identity, BinaryOperator):功能同上,但是使用 identity 作為其組合的初始值。因此如果流為空,identity 就是結(jié)果。
  • reduce(identity, BiFunction, BinaryOperator):更復(fù)雜的使用形式(暫不介紹),這里把它包含在內(nèi),因為它可以提高效率。通常,我們可以顯式地組合 map() 和 reduce() 來更簡單的表達它。
匹配
  • allMatch(Predicate) :如果流的每個元素根據(jù)提供的 Predicate 都返回 true 時,結(jié)果返回為 true。在第一個 false 時,則停止執(zhí)行計算。
  • anyMatch(Predicate):如果流中的任意一個元素根據(jù)提供的 Predicate 返回 true 時,結(jié)果返回為 true。在第一個 false 是停止執(zhí)行計算。
  • noneMatch(Predicate):如果流的每個元素根據(jù)提供的 Predicate 都返回 false 時,結(jié)果返回為 true。在第一個 true 時停止執(zhí)行計算。
查找
  • findFirst():返回第一個流元素的 Optional,如果流為空返回 Optional.empty。
  • findAny():返回含有任意流元素的 Optional,如果流為空返回 Optional.empty。
信息
  • count():流中的元素個數(shù)。
    max(Comparator):根據(jù)所傳入的 Comparator 所決定的“最大”元素。
  • min(Comparator):根據(jù)所傳入的 Comparator 所決定的“最小”元素。
數(shù)字流信息
  • average():求取流元素平均值。
  • max() 和 min():數(shù)值流操作無需 Comparator。
  • sum():對所有流元素進行求和。
  • summaryStatistics():生成可能有用的數(shù)據(jù)。目前并不太清楚這個方法存在的必要性,因為我們其實可以用更直接的方法獲得需要的數(shù)據(jù)。

collect() 收集操作,它根據(jù)參數(shù)來組合所有流中的元素。
當傳入的參數(shù)是Collectors.joining(),你將會得到一個 String 類型的結(jié)果,每個元素都根據(jù) joining() 的參數(shù)來進行分割。其他不同的 Collectors 的參數(shù)則會產(chǎn)生不同的結(jié)果

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本次課程的標題不像之前那樣易懂,是一個陌生的概念,“流式編程”是個什么東西? 在了解流式編程之前先思考一下“流”,...
    tommy990607閱讀 13,451評論 1 1
  • Stream流 說到Stream便容易想到I/O Stream,而實際上,誰規(guī)定“流”就一定是“IO流”呢?在Ja...
    哈哈大圣閱讀 1,969評論 0 3
  • Java8 in action 沒有共享的可變數(shù)據(jù),將方法和函數(shù)即代碼傳遞給其他方法的能力就是我們平常所說的函數(shù)式...
    鐵牛很鐵閱讀 1,388評論 1 2
  • 00-流式編程思想1 背景 事件數(shù)據(jù)的產(chǎn)生隨著時間的推移逐漸下降 人們對某件事的理解往往來自基于有效論據(jù)的結(jié)論。要...
    蝸牛寫java閱讀 902評論 0 1
  • “周周,我要去南京。我已經(jīng)聯(lián)系到南京那邊的醫(yī)院了,到時候過去直接任職?!焙诎抵?,我的聲音有些沙啞。 躺在對面床上的...
    阿長青閱讀 3,078評論 108 99

友情鏈接更多精彩內(nèi)容