1. 程式人生 > >Java8總結之Stream API

Java8總結之Stream API

Stream 流 是java 8 中處理集合的關鍵抽象概念。它可以指定你希望對集合進行的操作。我們使用 Java 8 儘量從以往迭代器轉為使用Stream操作。它與集合的區別如下:

  • Stream 自己不會儲存元素。元素可能被儲存在底層的集合中,或者根據需要產生出來
  • Stream 操作符不會改變源物件。相反,它們會返回一個持有結果的新Stream
  • Stream 操作符可能是延遲執行的。這意味著它們會等到需要結果的時候才執行。如下面的一個例子,可以看出第6行 如果非延遲執行,第一個值是不可能輸出的。
    @Test
    public void test4() {
        //生成一個無限的序列,forEach會停不下來。
Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE)).forEach(System.out::println); //可以輸出第一個值 Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE)).findFirst(); }
  • Stream 的可讀性要更好一些。而且可以很容易進行並行執行。如long count = words.parallelStream().filter(w -> w.length() > 12).count();
    只需要Stream修改為parallelStream.

當使用Stream 時,你會通過三個階段來建立一個操作流水線:
- 建立一個Stream。
- 在一個或者多個步驟中,將初始的Stream轉為另一個Stream的中間操作。
- 使用一個終止操作來產生一個結果。該操作會強制它之前的延遲操作立即執行。這和Spark中的終止操作很像。
下面我們來一步步看怎麼進行操作

建立Stream

建立Stream一般有以下幾種方式
- Collection介面中新新增的stream 方法,可以將任何集合轉為一個Stream。如:Arrays.asList("sb","e").stream()


- 如果是一個數組,可以通過Stream的靜態方法Stream.of方法將它轉為一個Stream 。如:Stream<String> words = Stream.of(contexts.split(","));
- of 方法可以接受可變的引數,因此你可以構造一個含有任意個引數的Stream Stream<String> s = Stream.of("a","b","c");
- 使用Arrays.stream(array,from,to)方法將陣列的一部分轉為Stream。
- 建立一個空Stream。Stream.empty();
- 建立無限Stream。有人說建立一個無限Stream有啥用,前面說過,建立無限時是不會執行的,只有在終止操作時才會執行,在終止操作前可以進行一些限制從而創建出一些符合條件的Stream。如下

    @Test
    public void test5() {
        List<String>  list =    Arrays.stream("a,b,c,d,e,f".split(",")).collect(Collectors.toList());
        System.err.println(JSON.toJSONString(list));
        int size = list.size();
        List<String> result =   IntStream.iterate(size-1, n -> n-1).limit(size).mapToObj(list::get).collect(Collectors.toList());
        System.err.println(JSON.toJSONString(result));
    }

看出來這個例子是做什麼的嗎?其實第6行就是將list進行反轉。輸出結果如下:

["a","b","c","d","e","f"]
["f","e","d","c","b","a"]

IntStream.iterate就是會產生一個 該方法第一個引數是一個初始值,後面的一個函式是對第一個初始值,進行應用該函式,會一直執行下去,但這個動作是延遲的,而類似limit之類的操作保證了只執行次數。
- 還有Files.lines會返回一個包含檔案中所有行的Stream。Stream介面有一個父介面AutoCloseable。當在某個Stream上呼叫close 方法時,底層的檔案也會被關閉。
- 還有Pattern.complie("[\\P{L}]+").splitAsStream(contexts) 可以按正則表示式對物件進行分隔。
- 其它方法。

轉換Stream

轉換Stream其實就是把一個Stream通過某些行為轉換成一個新的Stream。Stream這種操作很多,只能選擇幾個常用的進行說一下。

  • filter: 對於Stream中包含的元素使用給定的過濾函式進行過濾操作,新生成的Stream只包含符合條件的元素;
    Stream<T> filter(Predicate<? super T> predicate) filter接受一個Predicate (斷言)函式式介面
    Predicate 中有三個預設方法一個default(and, negate,or)法,一個預設方法(isEqual),一個抽象方法(test).
    具體分析可檢視Interface Predicate 中的介紹,測試程式碼如下:
  Predicate<String> a = x -> x != null;
  Predicate<String> b = a.and(x -> !"".equals(x));
  Stream.of("a","","b",null).filter(b.negate()).forEach(System.out::println);

filter方法示意圖:
enter image description here
- map: 對於Stream中包含的元素使用給定的轉換函式進行轉換操作,新生成的Stream只包含轉換生成的元素。這個方法有三個對於原始型別的變種方法,分別是:mapToInt,mapToLong和mapToDouble。這三個方法也比較好理解,比如mapToInt就是把原始Stream轉換成一個新的Stream,這個新生成的Stream中的元素都是int型別。之所以會有這樣三個變種方法,可以免除自動裝箱/拆箱的額外消耗;
<R> Stream<R> map(Function<? super T,? extends R> mapper) map接受一個Function函式介面。表示接受一個引數併產生結果的函式。
它有二個 default方法(andThen, compose)一個靜態方法(identity)

   Function<Integer, Integer> f = x -> x * 3;
   System.err.println(f.andThen(x -> x + 4).apply(6)); //結果 22 先執行6*3 再進行 +4操作 6*3+4
   System.err.println(f.compose(x -> Integer.valueOf(x.toString()) + 4).apply(6)); //結果為30:
   先將x+4執行*3操作(x+4)*3=3x+12 再將6執行上面的操作 3*6+12=30 

map方法示意圖:
enter image description here
- flatMap:和map類似,不同的是其每個元素轉換得到的是Stream物件,會把子Stream中的元素壓縮到父集合中;

flatMap方法示意圖:
enter image description here

     String[] words = new String[]{"Hello","World"};
        List<String[]> a = Arrays.stream(words)
                .map(word -> word.split(""))
                .distinct()
                .collect(toList());
        a.forEach(System.out::print);

enter image description here

        String[] words = new String[]{"Hello","World"};
        List<String> a = Arrays.stream(words)
                .map(word -> word.split(""))
                .flatMap(Arrays::stream)
                .distinct()
                .collect(toList());
        a.forEach(System.out::print);

使用flatMap方法的效果是,各個陣列並不是分別對映一個流,而是對映成流的內容,所有使用map(Array::stream)時生成的單個流被合併起來,即扁平化為一個流。
enter image description here

  • peek: 生成一個包含原Stream的所有元素的新Stream,同時會提供一個消費函式(Consumer例項),新Stream每個元素被消費的時候都會執行給定的消費函式;

peek方法示意圖:
enter image description here

  • limit: 對一個Stream進行截斷操作,獲取其前N個元素,如果原Stream中包含的元素個數小於N,那就獲取其所有的元素;

limit方法示意圖:
enter image description here

  • skip: 返回一個丟棄原Stream的前N個元素後剩下元素組成的新Stream,如果原Stream中包含的元素個數小於N,那麼返回空Stream;

skip方法示意圖:
enter image description here

  • distinct: 對於Stream中包含的元素進行去重操作(去重邏輯依賴元素的equals方法),新生成的Stream中沒有重複的元素;
    distinct方法示意圖
    enter image description here

  • sorted: 遍歷整個流,並在產生任何元素之前對它進行排序。

 Arrays.stream("a,w,d,e,f,b,c,j,h".split(",")).sorted().forEach(System.out::println);        Arrays.stream("a,we,deee,eff,fw,b,c,j,h".split(",")).sorted(Comparator.comparing(String::length).reversed()).forEach(System.out::println);
  • 連續多次操作
List<Integer> nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10);
System.out.println(“sum is:”+nums.stream().filter(num -> num != null).
            distinct().mapToInt(num -> num * 2).
            peek(System.out::println).skip(2).limit(4).sum());

這段程式碼演示了上面介紹的所有轉換方法(除了flatMap),簡單解釋一下這段程式碼的含義:給定一個Integer型別的List,獲取其對應的Stream物件,然後進行過濾掉null,再去重,再每個元素乘以2,再每個元素被消費的時候列印自身,在跳過前兩個元素,最後去前四個元素進行加和運算(解釋一大堆,很像廢話,因為基本看了方法名就知道要做什麼了。這個就是宣告式程式設計的一大好處!)。大家可以參考上面對於每個方法的解釋,看看最終的輸出是什麼。
- 效能問題
有些細心的同學可能會有這樣的疑問:在對於一個Stream進行多次轉換操作,每次都對Stream的每個元素進行轉換,而且是執行多次,這樣時間複雜度就是一個for迴圈裡把所有操作都做掉的N(轉換的次數)倍啊。其實不是這樣的,轉換操作都是lazy的,多個轉換操作只會在匯聚操作(見下節)的時候融合起來,一次迴圈完成。我們可以這樣簡單的理解,Stream裡有個操作函式的集合,每次轉換操作就是把轉換函式放入這個集合中,在匯聚操作的時候迴圈Stream對應的集合,然後對每個元素執行所有的函式。

聚合操作(Reduce)

  • reduce:這個方法的主要作用是把 Stream 元素組合起來。它提供一個起始值(種子),然後依照運算規則(BinaryOperator),和前面 Stream 的第一個、第二個、第 n 個元素組合。從這個意義上說,字串拼接、數值的 sum、min、max、average 都是特殊的 reduce。例如 Stream 的 sum 就相當於
Integer sum = integers.reduce(0, (a, b) -> a+b); //或
Integer sum = integers.reduce(0, Integer::sum);

也有沒有起始值的情況,這時會把 Stream 的前面兩個元素組合起來,返回的是 Optional。
- collect :該方法功能比較強大,能將流收整合很多形式。collect方法接收的多個引數中主要有一個Collector介面,該介面的實現類Collectors提供很多靜態方法便於多種形式的收集,比較強大。

1. 歸約

1.1.計數
  long count = list.stream()  .collect(Collectors.counting());  
  long count = list.stream().count(); //兩個相同
1.2. 最值
Optional<Person> oldPerson = list.stream().collect(Collectors.maxBy(Comparator.comparingInt(Person::getAge)));
1.3.求平均值
double avg = list.stream().collect(Collectors.averagingInt(Person::getAge));
1.4.求和
int summing = list.stream().collect(Collectors.summingInt(Person::getAge));
1.5.連線字串
String names = list.stream().collect(Collectors.joining());
String names = list.stream().collect(Collectors.joining(", "));
//每個字串預設分隔符為空格,若需要指定分隔符,則在joining中加入引數即可:
1.6.一般性的歸約操作

若你需要自定義一個歸約操作,那麼需要使用Collectors.reducing函式,該函式接收三個引數:
- 第一個引數為歸約的初始值
- 第二個引數為歸約操作進行的欄位
- 第三個引數為歸約操作的過程
如求合

Optional<Integer> sumAge = list.stream().collect(Collectors.reducing(0,Person::getAge,(i,j)->i+j));
//相比於 1.4更一般性一些                                   

上面例子中,reducing函式一共接收了三個引數:
- 第一個引數表示歸約的初始值。我們需要累加,因此初始值為0
- 第二個引數表示需要進行歸約操作的欄位。這裡我們對Person物件的age欄位進行累加。
- 第三個引數表示歸約的過程。這個引數接收一個Lambda表示式,而且這個Lambda表示式一定擁有兩個引數,分別表示當前相鄰的兩個元素。由於我們需要累加,因此我們只需將相鄰的兩個元素加起來即可。
Collectors.reducing方法還提供了一個單引數的過載形式。
你只需傳一個歸約的操作過程給該方法即可(即第三個引數),其他兩個引數均使用預設值。
- 第一個引數預設為流的第一個元素
- 第二個引數預設為流的元素
求合。

Optional<Integer> sumAge = list.stream().collect(Collectors.reducing((i,j)->i+j));

2.分組

分組就是將流中的元素按照指定類別進行劃分,類似於SQL語句中的GROUPBY。

2.1.一級分組

例:將所有人分為老年人、中年人、青年人

Map<String,List<Person>> result = list.stream()
                                    .collect(Collectors.groupingby((person)->{
        if(person.getAge()>60)
            return "老年人";
        else if(person.getAge()>40)
            return "中年人";
        else
            return "青年人";
                                    }));

groupingby函式接收一個Lambda表示式,該表示式返回String型別的字串,groupingby會將當前流中的元素按照Lambda返回的字串進行分組。
分組結果是一個Map< String,List< Person>>,Map的鍵就是組名,Map的值就是該組的Perosn集合。

2.2.多級分組

多級分組可以支援在完成一次分組後,分別對每個小組再進行分組。
使用具有兩個引數的groupingby過載方法即可實現多級分組。
- 第一個引數:一級分組的條件
- 第二個引數:一個新的groupingby函式,該函式包含二級分組的條件

例:將所有人分為老年人、中年人、青年人,並且將每個小組再分成:男女兩組。

Map<String,Map<String,List<Person>>> result = list.stream()
                                    .collect(Collectors.groupingby((person)->{
        if(person.getAge()>60)
            return "老年人";
        else if(person.getAge()>40)
            return "中年人";
        else
            return "青年人";},groupingby(Person::getSex)));

此時會返回一個非常複雜的結果:Map< String,Map< String,List< Person>>>。

2.3.對分組進行統計

擁有兩個引數的groupingby函式不僅僅能夠實現多幾分組,還能對分組的結果進行統計。

例:統計每一組的人數

Map<String,Long> result = list.stream()
                                    .collect(Collectors.groupingby((person)->{
        if(person.getAge()>60)
            return "老年人";
        else if(person.getAge()>40)
            return "中年人";
        else
            return "青年人";
                                    },
                                    counting()));

此時會返回一個Map< String,Long>型別的map,該map的鍵為組名,map的值為該組的元素個數。

3.分片

參考:
書:《寫給大忙人看的Java SE 8》
併發程式設計網
jdk-1.8-google