13、流式編程


   集合優化了對象的存儲,而流和對象的處理有關。

   利用流,無需迭代集合中的元素,就可以提取和操作它們。組合在一起,在流上形成一條操作管道。

   lambda表達式、方法引用、流式編程結合使用。當 Lambda 表達式和方法引用(method references)和流一起使用的時候會讓人感覺自成一體。

   聲明式編程(Declarative programming)是一種:聲明要做什麼,而非怎麼做的編程風格。正如我們在函數式編程中所看到的。

   流式編程採用內部迭代,這是流式編程的核心特性之一。這種機制使得編寫的代碼可讀性更強,也更能利用多核處理器的優勢。通過放棄對迭代過程的控制,我們把控制權交給並行化機制。
   另一個重要方面,流是懶加載的。這代表着它只在絕對必要時才計算。你可以將流看作“延遲列表”。由於計算延遲,流使我們能夠表示非常大(甚至無限)的序列,而不需要考慮內存問題。

1、JDK13中對流的介紹

  Package java.util.stream
  這個包中引入的關鍵抽象是流。類Stream、IntStream、LongStream和DoubleStream是對象和原始int、long和double類型上的流。流與集合在以下幾個方面不同:

  • 沒有存儲。流不是存儲元素的數據結構;相反,它通過計算操作的管道傳遞來自源的元素,如數據結構、數組、生成器函數或I/O通道。
  • Functional in nature 。流上的操作產生結果,但不修改其源。例如,對從集合中獲得的流進行過濾將產生一個沒有過濾元素的新流,而不是從源集合中刪除元素。
  • Laziness-seeking。許多流操作,如過濾、映射或重複刪除,可以延遲實現,從而暴露了優化的機會。例如,“查找包含三個連續元音的第一個字符串”不需要檢查所有的輸入字符串。流操作分爲中間(流生產)操作和終端(值或副作用生產)操作。中間操作總是延遲的。
  • 可能是無限的。集合的大小是有限的,而流則不必如此。諸如limit(n)或findFirst()之類的短路操作允許在有限時間內完成無限流上的計算。
  • consumable。流的元素在流的生命週期中只訪問一次。與迭代器一樣,必須生成新的流來重新訪問源的相同元素。

  流可以通過多種方式獲得。一些例子包括:

2、流支持

   如何將一個全新的流的概念融入到現有類庫中呢?
   接口部分怎麼改造呢?特別是涉及集合類接口的部分。如果你想把一個集合轉換爲流,直接向接口添加新方法會破壞所有老的接口實現類。

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

  • 創建流

  • 修改流元素(中間操作, Intermediate Operations)

  • 消費流元素(終端操作, Terminal Operations)通常意味着收集流元素(通常是到集合中)。

3、流創建

Stream.of()

   通過Stream().of()將一組元素轉化爲流

import java.util.stream.*;
public class StreamOf {
   public static void main(String[] args) {
       Stream.of("It's ", "a ", "wonderful ", "day ", "for ", "pie!")
           .forEach(System.out::print);
       System.out.println();
       Stream.of(3.14159, 2.718, 1.618)
           .forEach(System.out::println);
   }
}

輸出結果:
在這裏插入圖片描述
   每個集合都可以通過調用 stream() 方法來產生一個流。

import java.util.*;
import java.util.stream.*;

public class CollectionToStream {
    public static void main(String[] args) {
        Set<String> w = new HashSet<>(Arrays.asList("It's a wonderful day for pie!".split(" ")));
        w.stream()
        .map(x -> x + "")
        .forEach(System.out::println);
        
    }
}

range()

IntStream 類提供了 range() 方法用於生成整型序列的流。編寫循環時,這個方法會更加便利

import static java.util.stream.IntStream.*;

public class Ranges {
    public static void main(String[] args) {
        System.out.println(range(10, 20).sum());
        Repeat.repeat(5, Run::run);
    }
}

class Run {
    public static void run() {
        System.out.println("this is run");
    }
}

class Repeat {
    public static void repeat(int n, Runnable runnable) {
        range(0, n).forEach(i -> runnable.run());
    }
}

實用小功能 repeat() 可以用來替換簡單的 for 循環。代碼示例:

import static java.util.stream.IntStream.*;
public class Repeat {
    public static void repeat(int n, Runnable action) {
        range(0, n).forEach(i -> action.run());
    }
}

static Stream generate​(Supplier<? extends T> s)

返回一個無限連續的無序流,其中每個元素由提供的Supplier<? extends T>生成。這適用於生成常量流、隨機元素流等。

static <T> Stream<T> iterate​(T seed,Predicate<? super T> hasNext,UnaryOperator<T> next)

Stream.iterate() 以種子(第一個參數)開頭,並將其傳給方法(第二個參數)。方法的結果將添加到流,並存儲作爲第一個參數用於下次調用 iterate(),依次類推。

以滿足給定的hasNext謂詞爲條件,將給定的下一個函數迭代應用於初始元素,從而返回一個連續的有序流。一旦hasNext謂詞返回false,流就終止。

Stream.iterate應該生成與對應的for循環生成的元素序列相同的元素序列:

for (T index=seed; hasNext.test(index); index = next.apply(index)) {
         ...
     }

流的建造者模式

Interface Stream.Builder
在建造者設計模式(也稱構造器模式)中,首先創建一個 builder 對象,傳遞給它多個構造器信息,最後執行“構造”。Stream 庫提供了這樣的 Builder。

Arrays.stream()

Arrays 類中含有一個名爲 stream() 的靜態方法用於把數組轉換成爲流。

正則表達式

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

中間操作

跟蹤和調試

peek() 操作的目的是幫助調試。它允許你無修改地查看流中的元素。

流元素排序

Stream sorted()
返回由該流的元素組成的流,按自然順序排序。如果此流的元素不可比較,則使用java.lang。在執行終端操作時,可能會拋出ClassCastException。
對於有序流,排序是穩定的。對於無序流,沒有穩定性保證。

Stream sorted​(Comparator<? super T> comparator)
返回一個由這個流的元素組成的流,根據提供的比較器進行排序。
對於有序流,排序是穩定的。對於無序流,沒有穩定性保證。

sorted()中可以傳入一個 Comparator 參數
sorted() 預設了一些默認的比較器.
也可以把 Lambda 函數作爲參數傳遞給 sorted().

移除元素

返回由該流的不同元素(根據Object.equals(Object))組成的流。
對於有序流,不同元素的選擇是穩定的(對於重複的元素,首先出現在相遇順序中的元素將被保留)。對於無序流,沒有穩定性保證。
Stream distinct()

返回一個由與給定謂詞匹配的流的元素組成的流。
Stream filter​(Predicate<? super T> predicate)

應用函數到元素

  • <R> Stream<R> map​(Function<? super T,​? extends R> mapper) 返回一個流,該流包含將給定函數應用於該流元素的結果。
    將作用的結果放入Stream中,得到的是元素流的流,不關閉原來的流
  • IntStream mapToInt​(ToIntFunction<? super T> mapper) 返回一個IntStream,其中包含將給定函數應用於該流元素的結果。
  • mapToLong(ToLongFunction):操作同上,但結果是 LongStream
  • mapToDouble(ToDoubleFunction):操作同上,但結果是 DoubleStream。

在 map() 中組合流

flatMap() 做了兩件事:將產生流的函數應用在每個元素上(與 map() 所做的相同),然後將每個流都扁平化爲元素,因而最終產生的僅僅是元素。

每個被映射的流在其內容被放入該流之後關閉。(如果映射的流爲空,則使用空流)。所以,傳入的方法體返回也應當是流,因爲原來的流關閉了。
flatMap(Function):當 Function 產生流時使用。

flatMapToInt(Function):當 Function 產生 IntStream 時使用。

flatMapToLong(Function):當 Function 產生 LongStream 時使用。

flatMapToDouble(Function):當 Function 產生 DoubleStream 時使用。

Optional類

是否有某種對象,可作爲流元素的持有者,即使查看的元素不存在也能友好地提示我們(也就是說,不會發生異常)?
Optional 可以實現這樣的功能。一些標準流操作返回 Optional 對象,因爲它們並不能保證預期結果一定存在。包括:

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

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

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

  • reduce() 不再以 identity 形式開頭,而是將其返回值包裝在 Optional 中。(identity 對象成爲其他形式的
    reduce() 的默認結果,因此不存在空結果的風險)

注意,空流是通過 Stream.empty() 創建的。如果你在沒有任何上下文環境的情況下調用 Stream.empty(),Java 並不知道它的數據類型;這個語法解決了這個問題。

便利函數

有許多便利函數可以解包 Optional ,這簡化了上述“對所包含的對象的檢查和執行操作”的過程:

  • ifPresent(Consumer):當值存在時調用 Consumer,否則什麼也不做。
  • orElse(otherObject):如果值存在則直接返回,否則生成 otherObject。
  • orElseGet(Supplier):如果值存在則直接返回,否則使用 Supplier 函數生成一個可替代對象。
  • orElseThrow(Supplier):如果值存在直接返回,否則使用 Supplier 函數生成一個異常。

創建Optional

當我們在自己的代碼中加入 Optional 時,可以使用下面 3 個靜態方法:

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

我們不能通過傳遞 null 到 of() 來創建 Optional 對象。最安全的方法是, 使用 ofNullable() 來優雅地處理 null。

Optional 對象操作

當我們的流管道生成了 Optional 對象,下面 3 個方法可使得 Optional 的後續能做更多的操作:

  • filter(Predicate):將 Predicate 應用於 Optional 中的內容並返回結果。當 Optional 不滿足Predicate 時返回空。如果 Optional 爲空,則直接返回。

  • map(Function):如果 Optional 不爲空,應用 Function 於 Optional中的內容,並返回結果。否則直接返回 Optional.empty。

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

以上方法都不適用於數值型 Optional。一般來說,流的 filter() 會在 Predicate 返回 false 時移除流元素。而 Optional.filter() 在失敗時不會刪除 Optional,而是將其保留下來,並轉化爲空。

同 map() 一樣 , Optional.map() 應用於函數。它僅在 Optional 不爲空時才應用映射函數,並將 Optional 的內容提取到映射函數。

同 Optional.map(),Optional.flatMap() 將提取非空 Optional 的內容並將其應用在映射函數。唯一的區別就是 flatMap() 不會把結果包裝在 Optional 中,因爲映射函數已經被包裝過了。

Optional 流

假設你的生成器可能產生 null 值,那麼當用它來創建流時,你會自然地想到用 Optional 來包裝元素

終端操作

這些操作接收一個流併產生一個最終結果;它們不會向後端流提供任何東西。因此,終端操作總是你在管道中做的最後一件事情

轉化成數組

  • toArray():將流轉換成適當類型的數組。

  • toArray(generator):在特殊情況下,生成器用於分配自定義的數組存儲。
    這組方法在流操作產生的結果必須是數組形式時很有用。假如我們想在流裏複用獲取的隨機數,可以將他們保存到數組中。

forEach

應用最終的操作

  • forEach(Consumer):你已經看到過很多次 System.out::println 作爲 Consumer 函數
  • forEachOrdered(Consumer): 保證 forEach 按照原始流順序操作。

第一種形式:顯式設計爲任意順序操作元素,僅在引入 parallel() 操作時纔有意義。parallel():可實現多處理器並行操作。實現原理爲將流分割爲多個(通常數目爲 CPU 核心數)並在不同處理器上分別執行操作。因爲我們採用的是內部迭代,而不是外部迭代,所以這是可能實現的。

import java.util.*;
import java.util.stream.*;

public class ForEach {
    private static int[] rints = new Random(47).ints(0, 1000).limit(100).toArray();
    public static IntStream rands() {
        return Arrays.stream(rints);
    }
    static final int SZ = 14;
    public static void main(String[] args) {
        rands().limit(SZ).forEach(n -> System.out.format("%d ", n));
        System.out.println();
        rands().limit(SZ).parallel().forEach(n -> System.out.format("%d ", n));
        System.out.println();
        rands().limit(SZ)
                .parallel()
                .forEachOrdered(n -> System.out.format("%d ", n));
    }
}

運行結果
在這裏插入圖片描述

收集

  • collect(Collector):使用 Collector 收集流元素到結果集合中。

  • collect(Supplier, BiConsumer, BiConsumer):同上,第一個參數 Supplier
    創建了一個新結果集合,第二個參數 BiConsumer 將下一個元素包含到結果中,第三個參數 BiConsumer 用於將兩個值組合起來。

可以通過將集合的構造函數引用傳遞給 Collectors.toCollection(),從而構建任何類型的集合。
比如:.collect(Collectors.toCollection(TreeSet::new));

// streams/TreeSetOfWords.java
import java.util.*;
import java.nio.file.*;
import java.util.stream.*;
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 並將其轉換成爲行流。下一行代碼將匹配一個或多個非單詞字符(\w+)行進行分割,然後使用 Arrays.stream() 將其轉化成爲流,並將結果扁平映射成爲單詞流。使用 matches(\d+) 查找並移除全數字字符串(注意,words2 是通過的)。接下來我們使用 String.trim() 去除單詞兩邊的空白,filter() 過濾所有長度小於3的單詞,緊接着只獲取100個單詞,最後將其保存到 TreeSet 中。

我們也可以在流中生成 Map。代碼示例:

// streams/MapCollector.java
import java.util.*;
import java.util.stream.*;
class Pair {
    public final Character c;
    public final Integer i;
    Pair(Character c, Integer i) {
        this.c = c;
        this.i = i;
    }
    public Character getC() { return c; }
    public Integer getI() { return i; }
    @Override
    public String toString() {
        return "Pair(" + c + ", " + i + ")";
    }
}
class RandomPair {
    Random rand = new Random(47);
    // An infinite iterator of random capital letters:
    Iterator<Character> capChars = rand.ints(65,91)
            .mapToObj(i -> (char)i)
            .iterator();
    public Stream<Pair> stream() {
        return rand.ints(100, 1000).distinct()
                .mapToObj(i -> new Pair(capChars.next(), i));
    }
}
public class MapCollector {
    public static void main(String[] args) {
        Map<Integer, Character> map =
                new RandomPair().stream()
                        .limit(8)
                        .collect(
                                Collectors.toMap(Pair::getI, Pair::getC));
        System.out.println(map);
    }
}

在 Java 中,我們不能直接以某種方式組合兩個流。所以這裏創建了一個整數流,並且使用 mapToObj() 將其轉化成爲 Pair 流。

在大多數情況下,你可以在 java.util.stream.Collectors尋找到你想要的預先定義好的 Collector。在少數情況下當你找不到想要的時候,你可以使用第二種形式的 collect()。

// streams/SpecialCollector.java
import java.util.*;
import java.util.stream.*;
public class SpecialCollector {
    public static void main(String[] args) throws Exception {
        ArrayList<String> words =
                FileToWords.stream("Cheese.dat")
                        .collect(ArrayList::new,
                                ArrayList::add,
                                ArrayList::addAll);
        words.stream()
                .filter(s -> s.equals("cheese"))
                .forEach(System.out::println);
    }
}

組合所有流元素

  • reduce(BinaryOperator):使用 BinaryOperator 來組合所有流中的元素。因爲流可能爲空,其返回值爲Optional。
  • reduce(identity, BinaryOperator):功能同上,但是使用 identity 作爲其組合的初始值。因此如果流爲空,identity 就是結果。
  • reduce(identity, BiFunction, BinaryOperator):這個形式更爲複雜(所以我們不會介紹它),在這裏被提到是因爲它使用起來會更有效。通常,你可以顯式地組合 map() 和 reduce() 來更簡單的表達它。
// streams/Reduce.java
import java.util.*;
import java.util.stream.*;
class Frobnitz {
    int size;
    Frobnitz(int sz) { size = sz; }
    @Override
    public String toString() {
        return "Frobnitz(" + size + ")";
    }
    // Generator:
    static Random rand = new Random(47);
    static final int BOUND = 100;
    static Frobnitz supply() {
        return new Frobnitz(rand.nextInt(BOUND));
    }
}
public class Reduce {
    public static void main(String[] args) {
        Stream.generate(Frobnitz::supply)
                .limit(10)
                .peek(System.out::println)
                .reduce((fr0, fr1) -> fr0.size < 50 ? fr0 : fr1)
                .ifPresent(System.out::println);
    }
}

Lambda 表達式中的第一個參數 fr0 是上一次調用 reduce() 的結果。而第二個參數 fr1 是從流傳遞過來的值。

reduce() 中的 Lambda 表達式使用了三元表達式來獲取結果,當其 size 小於 50 的時候獲取 fr0 否則獲取序列中的下一個值 fr1。因此你會取得第一個 size 小於 50 的 Frobnitz,只要找到了就這個結果就會緊緊地攥住它,即使有其他候選者出現。雖然這是一個非常奇怪的約束,但是它確實讓你對 reduce() 有了更多的瞭解。

匹配

  • allMatch(Predicate) :如果流的每個元素根據提供的 Predicate 都返回 true 時,結果返回爲
    true。這個操作將會在第一個 false 之後短路;也就是不會在發生 false 之後繼續執行計算。

  • anyMatch(Predicate):如果流中的任意一個元素根據提供的 Predicate 返回 true 時,結果返回爲
    true。這個操作將會在第一個 true 之後短路;也就是不會在發生 true 之後繼續執行計算。

  • noneMatch(Predicate):如果流的每個元素根據提供的 Predicate 都返回 false 時,結果返回爲
    true。這個操作將會在第一個 true 之後短路;也就是不會在發生 true 之後繼續執行計算。

import java.util.function.BiPredicate;

interface Matcher extends BiPredicate<Stream<Integer>,Predicate<Integer>> {}

public class Matching {
    static void show(Matcher match, int val) {
    System.out.println(match.test(IntStream
    .rangeClosed(1, 9).boxed().peek(n -> System.out.format("%d ", n)), 
    n -> n < val));
    }

    public static void main(String[] args) {
        show(Stream::allMatch, 10);
        show(Stream::allMatch, 4);
        show(Stream::anyMatch, 2);
        show(Stream::anyMatch, 0);
        show(Stream::noneMatch, 5);
        show(Stream::noneMatch, 0);
    }
}

BiPredicate 是一個二元謂詞,這意味着它只能接受兩個參數並且只返回 true 或者 false。它的第一個參數是我們要測試的流,第二個參數是一個謂詞 Predicate。因爲 Matcher 適用於所有的 Stream::*Match 方法形式,所以我們可以傳遞每一個到 show() 中。match.test() 的調用會被轉換成 Stream::*Match 函數的調用。

show() 獲取兩個參數,Matcher 匹配器和用於表示謂詞測試 n < val 中最大值的 val。這個方法生成一個從 1 到 9 的整數流。peek() 是用於向我們展示測試在短路之前的情況。你可以在輸出中發現每一次短路都會發生。

元素查找

  • findFirst():返回一個含有第一個流元素的 Optional,如果流爲空返回 Optional.empty。

  • findAny(:返回含有任意流元素的 Optional,如果流爲空返回 Optional.empty。

// streams/SelectElement.java
import java.util.*;
import java.util.stream.*;
import static streams.RandInts.*;
public class SelectElement {
    public static void main(String[] args) {
        System.out.println(rands().findFirst().getAsInt());
        System.out.println(
                rands().parallel().findFirst().getAsInt());
        System.out.println(rands().findAny().getAsInt());
        System.out.println(
                rands().parallel().findAny().getAsInt());
    }
}

findFirst() 無論流是否爲並行化的,總是會選擇流中的第一個元素。對於非並行流,findAny()會選擇流中的第一個元素(即使從定義上來看是選擇任意元素)。在這個例子中,我們使用 parallel() 來並行流從而引入 findAny() 選擇非第一個流元素的可能性。

如果必須選擇流中最後一個元素,那就使用 reduce()

// streams/LastElement.java
import java.util.*;
import java.util.stream.*;
public class LastElement {
    public static void main(String[] args) {
        OptionalInt last = IntStream.range(10, 20)
                .reduce((n1, n2) -> n2);
        System.out.println(last.orElse(-1));
        // Non-numeric object:
        Optional<String> lastobj =
                Stream.of("one", "two", "three")
                        .reduce((n1, n2) -> n2);
        System.out.println(
                lastobj.orElse("Nothing there!"));
    }
}

信息

  • count():流中的元素個數。
  • max(Comparator):根據所傳入的 Comparator 所決定的“最大”元素。
  • min(Comparator):根據所傳入的 Comparator 所決定的“最小”元素。

數字流信息

  • average() :求取流元素平均值。
  • max() 和 min():因爲這些操作在數字流上面,所以不需要 Comparator。
  • sum():對所有流元素進行求和。
  • summaryStatistics():生成可能有用的數據。目前還不太清楚他們爲什麼覺得有必要這樣做,因爲你可以使用直接的方法產生所有的數據。
// streams/NumericStreamInfo.java
import java.util.stream.*;
import static streams.RandInts.*;
public class NumericStreamInfo {
    public static void main(String[] args) {
        System.out.println(rands().average().getAsDouble());
        System.out.println(rands().max().getAsInt());
        System.out.println(rands().min().getAsInt());
        System.out.println(rands().sum());
        System.out.println(rands().summaryStatistics());
    }
}

運行結果

507.94
998
8
50794
IntSummaryStatistics{count=100, sum=50794, min=8, average=507.940000, max=998}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章