使用流

Philip發表於2018-08-31

在本篇中,你將會看到Stream API支援的許多操作。這些操作能讓你快速完成複雜的資料查詢,如篩選、切片、對映、查詢、匹配、和歸約。接下來,我們會看看一些特殊的流:數值流、來自檔案和陣列等多種來源的流,最後是無限流。

  1. 篩選和切片
  • 用謂詞篩選(篩選):Stream介面支援filter方法。該操作會接受一個謂詞(一個返回Boolean的函式)作為引數,並返回一個包括所有符合謂詞的元素的流。
  • 篩選各異的元素(去重):流還支援一個叫做distinct的方法,它會返回一個元素各異(根據流所生成元素的hashCode和equals方法的實現)的流。
  • 截短流(擷取):流支援limit(n)方法,該方法會返回一個不超過給定長度的流。所需的長度作為引數傳遞給limit。如果流是有序的,則最多會返回前n個元素。
  • 跳過元素(跳過):流還支援skip(n)方法,返回一個扔掉了前n個元素的流。如果流中元素不足n個,則返回一個空流。請注意,limit(n)和skip(n)是互補的!
  1. 對映
  • 對流中每一個元素應用函式:流支援map方法,它會接受一個函式(Function)作為引數。這個函式會被應用到每個元素上,並將其對映成一個新的元素(使用對映一詞,是因為它和轉換類似,但其中的細微差別在於它是建立一個“新版本”,而不是去“修改”)。
  • 流的扁平化:使用flatMap方法的效果是,各個陣列並不是分別對映成一個流,而是對映成流的內容。所有使用map(Array::Stream)時生成的單個流都被合併起來,即扁平化為一個流。
  1. 查詢和匹配
  • 檢查謂詞是否至少匹配一個元素:anyMatch方法可以回答“流中是否有一個元素能匹配給定的謂詞”。
  • 檢查謂詞是否匹配所有元素:allMatch方法的工作原理和anyMatch類似,但它會看看流中的元素是否都能匹配給定的謂詞。和allMatch相對的是noneMatch。它可以確保流中沒有任何元素與給定的謂詞匹配。
  • 查詢元素:findAny方法將返回當前流中的任意元素。它可以與其他流操作結合使用。
  • 查詢第一個元素:有些流有一個出現順序來指定流中專案出現的邏輯順序(比如由List或排序好的資料列生成的流)。對於這種流,你可能想要找到第一個元素。為此有一個findFirst方法,它的工作方式類似於findAny。
  1. 歸約
  • 元素求和

int sum = numbers.stream().reduce(0, (a, b) -> a + b);

  • 一個初始值,這裡是0;
  • 一個BinaryOperator((T,T) -> T)來將兩個元素結合起來產生一個新值,這裡我們用的是lambda (a,b) -> a+b。 reduce接受兩個引數:

你可以使用方法引用讓這段程式碼更簡潔。在Java8裡,Integer類現在有一個靜態的sum方法來對兩個數求和,這恰好是我們想要的,用不著反覆用Lambda寫同一段程式碼了:

int sum = numbers.stream().reduce(0, Integer::sum);

reduce還有一個過載的變體,它不接受初始值,但是會返回一個Optional物件:

Optional<Integer> sum = numbers.stream().reduce(Integer::sum);

  • 最大值和最小值

原來,只要用歸約就可以計算最大值和最小值了!讓我們來看看如何利用剛剛學到的reduce來計算流中最大或最小的元素。正如你前面看到的,reduce接受兩個引數:

  • 一個初始值;
  • 一個Lambda來把兩個流元素結合起來併產生一個新值
Optional<Integer> max = number.stream().reduce(Integer::max);
Optional<Integer> max = number.stream().reduce(Integer::min);
複製程式碼

當然你也可以寫成Lambda (x, y) -> x < y ? x : y

  1. 數值流
  • 原始型別流特化

  Java8引入了三個原始型別特化流介面:IntStream、DoubleStream和LongStream,分別將流中的元素特化為int、long和double,從而避免了暗含的裝箱成本。每個介面都帶來了進行常用數值歸約的新方法,比如對數值流求和的sum,找到最大元素的max。此外還有在必要時再把它們轉換回物件流的方法。要記住的是,這些特化的原因並不在於流的複雜性,而是裝箱造成的複雜性--即類似int和Integer之間的效率差異。

  將流對映為特化版本的常用方法是mapToInt、mapToDouble和mapToLong。這些方法和前面說的map方法的工作方式一樣,只是它們返回的是一個特化流,而不是Stream<T>。例如:

int calories = menu.stream()
    .mapToInt(Dish::getCalories)
    .sum();
複製程式碼

  這裡,mapToInt會從每道菜中提取熱量(用一個Integer表示),並返回一個IntStream(而不是一個Stream<Integer>)。然後你就可以呼叫IntStream介面中定義的sum方法,對卡路里求和了!請注意,如果流是空的,sum預設返回0。IntStream還支援其他的方便方法,如max、min、average等。

  同樣,一旦有了數值流,你可能回想把它轉換回非特化流。例如,IntStream上的操作只能產生原始整數:IntStream的map操作接受的Lambda必須接受int並返回int(一個IntUnaryOperator)。但是你可能想生成另一類值,比如Dish。為此,你需要訪問Stream介面中定義的那些更廣義的操作。要把原始流轉換為一般流(每個int都會裝箱成一個Integer),可以使用boxed方法:

IntStream intStream = menu.stream().map(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
複製程式碼

  對於三種原始特化流,也分別有一個Optional原始型別特化版本:OptionalInt、OptionalDouble和OptionalLong。

  例如,要找到IntStream中的最大元素,可以呼叫max方法,它會返回一個OptionalInt:

OptionalInt maxCalories = menu.stream()
    .mapToInt(Dish::getColories)
    .max();
複製程式碼

  現在,如果沒有最大值的話,你就可以顯式處理OptionalInt去定義一個預設值了:

int max = maxCalories.orElse(1);//如果沒有最大值,顯式提供一個預設對大值

  • 數值範圍

  Java8引入了兩個可以用於IntStream和LongStream的靜態方法,幫助生成這種範圍:range和rangeClosed。這兩個方法都是第一個引數接受起始值,第二個引數接受結束值。但是range是不包含結束值的,而rangeClosed包含結束值。

  1. 構建流
  • 由值構建流

  你可以使用靜態方法Stream.of,通過顯式值建立一個流。它可以接受任意數量的引數。例如,以下程式碼直接使用Stream.of建立一個字串流。然後,你可以將字串轉換為大寫,再一個一個列印出來:

Stream<String> stream = Stream.of("Java 8", "Lambdas", "In", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
複製程式碼

  你可以使用empty得到一個空流:

Stream<String> emptyStream = Stream.empty();

  • 由陣列建立流

  你可以使用靜態方法Arrays.Stream從陣列建立一個流。它接受一個陣列作為引數。例如,你可以將一個原始型別int的陣列轉換為IntStream:

int[] numbers = {2, 3, 4, 5, 6}; int sum = Arrays.stream(numbers).sum();

  • 由檔案生成

  • 由函式生成流:建立無限流

    • 迭代(iterator)

    我們先來看一個iterator的簡單例子,然後在解釋:

    Stream.iterator(0, n -> n + 2)
        .limit(10)
        .forEach(System.out::println);
    複製程式碼

      iterator方法接受一個初始值(在這裡是0),還有一個依次應用在每個產生的新值上的Lambda(UnaryOperator型別)。這裡,我們使用Lambda n -> n + 2,返回的是前一個元素加上2。因此,iterator方法生成了一個所有正偶數的流:流的第一個元素是初始值0。請注意,此操作將生成一個無限流--這個流沒有結尾,因為值是按需計算的,可以永遠計算下去。我們使用limit方法來顯式限制流的大小。這裡只選擇了前10個偶數,然後可以呼叫forEach終端操作來消費流,並分別列印每個元素。

    • 生成(generate)

    與iterator方法類似,generate方法也可讓你按需生成一個無限流。但generate不是依次對每個新生成的值應用函式的。它接受一個Supplier<T>型別的Lambda提供新的值。我們先來看一個簡單的用法:

    Stream.generate(Math::random).limit(5).forEach(System.out::println);

    這段程式碼將生成一個流,其中有五個0到1之間的隨機雙精度數。

相關文章