java1.8新特性之stream

什麼是Stream?

Stream字面意思是流,在java中是指一個來自數據源的元素隊列並支援聚合操作,存在於java.util包中,又或者說是能應用在一組元素上一次執行的操作序列。(stream是一個由特定類型對象組成的一個支援聚合操作的隊列。)注意Java中的Stream並不會存儲元素,而是按需計算。關於這個概念需要以下幾點解釋:1、數據源流的來源。 它可以是列表,集合,數組(java.util.Collection的子類),I/O channel, 產生器generator等(注意Map是不支援的);2、聚合操作。類似於SQL語句一樣的操作, 如filter, map, reduce, find, match, sorted等。因此stream流和以前的Collection操作是完全不同, Stream操作還有兩個非常基礎的特徵:Pipelining和內部迭代。

Pipelining也就是中間操作,它都會返迴流對象本身。 這樣多個操作的設計可以串聯起不同的運算操作,進而形成一個管道, 如同流式風格(fluent style)。 這樣做還可以對操作進行優化, 比如延遲執行(laziness)和短路( short-circuiting)等。內部迭代, 以前對集合遍歷都是通過Iterator或者For-Each的方式來顯式的在集合外部進行迭代, 這種方式叫做外部迭代。而我們的Stream則提供了內部迭代方式, 是通過訪問者模式(Visitor)來實現的。

也就是說Stream操作分為中間操作和最終操作兩種。其中最終操作用於返回特定類型的計算結果,而中間操作則返回Stream對象本身,這樣就可以將多個操作依次串起來且使得操作優化成為可能。

生成流

在Java1.8 中, 集合介面提供了兩個方法來生成流:stream()串列流parallelStream()並行流,即Stream的操作可以分為串列stream()和並行parallelStream()。舉個例子來說:

List<String> strings = Arrays.asList("who","what","when","why","which");
List<String> filterd = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());

流的各種運算操作

接下來介紹流的各種操作運算,使得你在適當的時候可以選擇相應的流運算。

1、forEach 循環

Stream提供了新的方法forEach來迭代流中的每個數據。舉個例子來說:

       List<String> stringList = Arrays.asList("who","what","when","why","which");

        // 方式一:JDK1.8之前的循環方式
        for(String string:stringList){
            System.out.println(string);
        }

        // 方式二:使用Stream的forEach方法
        stringList.stream().forEach(e -> System.out.println(e));

        // 方式三:方式二的簡化形式,因為方法引用也屬於函數式介面,因此Lambda表達式可以用方法引用來代替
        stringList.stream().forEach(System.out::println);

2、filter 過濾

filter方法用於通過設置條件來過濾出滿足條件的元素。舉個例子來說,下面就是用於輸出字元串列表中的空字元串的個數:

        List<String> stringList = Arrays.asList("","welcome","","to","visit","my","","website");
        long count = stringList.stream().filter(e -> e.isEmpty()).count();
        System.out.println(count);

3、map 映射

請注意這裡的map不是指地圖map,而是一種函數,用於映射每個元素執行某些操作得到對應的結果。舉個例子來說,下面就是使用map來輸出元素對應的平方數:

        List<Integer> integerList = Arrays.asList(2,3,4,5,6);
        List<Integer> integers = integerList.stream().map(i->i*i).collect(Collectors.toList());
        integerList.stream().forEach(System.out::println);

上面介紹的只是map的最基本用法。map對於Stream中包含的元素使用給定的轉換函數進行轉換操作,新生成的Stream只包含轉換生成的元素。這個方法有三個對於原始類型的變種方法,分別是:mapToIntmapToLongmapToDouble。顧名思義像mapToInt就是將原始Stream轉換成一個新的Stream,不過新生成的Stream中的元素都是int類型。三個變種方法可以免除自動裝箱/拆箱的額外消耗。map方法示意圖:

4、flatMap 映射

flatMap映射和map映射類似,不過它的每個元素轉換得到的是Stream對象,會把子Stream中的元素壓縮到父集合中,說白了就是將幾個小的list合併成一個大的list。flatMap方法示意圖:

合併的過程可以參看下面這張圖片:

舉個例子來說,下面是jdk1.8之前的合併方式,需要先構造一個複合類型List,然後通過兩次遍歷循環來實現將複合類型List轉為單一類型List,這個過程其實挺複雜的:

        List<String> fruitList = Arrays.asList("banana","orange","watermelon");
        List<String> vegetableList = Arrays.asList("kale","leek","carrot");
        List<String> transportList = Arrays.asList("car","bike","train");

        //將多個元素合成一個複合類型集合,元素類型List<String>
        List<List<String>> lists = new ArrayList<>();
        lists.add(fruitList);
        lists.add(vegetableList);
        lists.add(transportList);

        //將多個元素合成一個單一類型集合,元素類型String
        List<String> newList = new ArrayList<>();
        for(List<String> list:lists){
            for(String item:list){
                newList.add(item);
            }
        }

那麼使用jdk1.8提供的stream流,同時輔助of、collect和flatMap就可以直接進行轉換:

        List<String> fruitList = Arrays.asList("banana","orange","watermelon");
        List<String> vegetableList = Arrays.asList("kale","leek","carrot");
        List<String> transportList = Arrays.asList("car","bike","train");
        
        //將多個元素合成一個複合類型集合,元素類型List<String>
        List<List<String>> lists = Stream.of(fruitList,vegetableList,transportList).collect(Collectors.toList());

        //將多個元素合成一個單一類型集合,元素類型String
        List<String> flatMap = Stream.of(fruitList,vegetableList,transportList)
                .flatMap(list ->list.stream())
                .collect(Collectors.toList());
        System.out.println(flatMap);

5、sorted 排序

sorted方法用於對流進行排序。舉個例子來說,下面的程式碼就是用於對字元串按照給定的規則進行排序並輸出:

        List<String> stringList = Arrays.asList("c","a","f","d","b","e");
        stringList.stream().sorted((s1,s2) -> s1.compareTo(s2)).forEach(System.out::println);

再舉個例子,對10個隨機數進行排序並輸出:

        Random random = new Random();
        random.ints().limit(10).sorted().forEach(System.out::println);

6、distinct 去除重複

distinct方法用於去除流中重複的元素,缺點就是不能設置去重的條件。舉個例子來說:

        List<String> stringList = Arrays.asList("do","what","you","want","to","do","and","do","it");
        stringList.stream().distinct().forEach(System.out::println);

7、of 生成Stream對象

of方法用於生成Stream對象,注意它是Stream對象的方法。舉個例子來說:

        Stream<Object> objectStream= Stream.of("do","what","you","want","to","do","and","do","it");
        objectStream.forEach(System.out::println);

8、count 計算總數

count方法用於計算流中元素的總數。舉個例子來說:

        Stream<Object> objectStream = Stream.of("do","what","you","want","to","do","and","do","it");
        long count = objectStream.count();
        System.out.println(count);

9、min和max 最小/最大

min/max方法用於返迴流中那個元素最小(最大)的,注意返回的是一個Optional對象。舉個例子來說:

        List<String> integerList = Arrays.asList("1","2","3","4","5","6","7");
        Optional<String> optionalInteger = integerList.stream().max((a,b) -> a.compareTo(b));
        String result =  optionalInteger.get();
        System.out.println(result);  //結果為7

10、collect

collect方法的使用較為複雜,這裡僅僅介紹一些常用的方法即可。collect方法可以將Stream轉為Collection對象或者是Object類型的數組等,舉個例子來說:

        List<String> stringList= Arrays.asList("do","what","you","want","to","do","and","do","it");
        //Stream轉Collection
        stringList.stream().collect(Collectors.toList());
        //Stream轉Object[]數組
        Object[] objects = stringList.stream().toArray();

11、concat

concat方法用於合併流對象,注意這時Stream對象的方法。舉個例子來說:

        List<String> fruitList = Arrays.asList("banana","orange","watermelon");
        List<String> vegetableList = Arrays.asList("kale","leek","carrot");

        Stream<String> stringStream = Stream.concat(fruitList.stream(),vegetableList.stream());
        stringStream.forEach(System.out::println);

12、skip和limit

通常大家都會將skip和limit放在一塊進行學習和對比,那是因為兩者具有類似的作用,都是對流進行裁剪的中間方法。

skip方法。先來看skip方法,顧名思義skip(n)用於跳過前面n個元素,然後再返回新的流,如圖所示:

為了驗證上面圖片的作用,這裡舉一個例子來進行說明:

public static void skipTest(long n){
        Stream<Integer> integerStream = Stream.of(1,2,3,4,5,6);
        integerStream.skip(n).forEach(System.out::println);
    }

方法skip()中的參數n不同將會導致不同的結果,具體情況如下:
(1)、當n<0時,運行結果會拋出IllegalArgumentException異常;(2)、當n=0時,相當沒有跳過任何元素,原封不動地截取流中的元素(這種通常沒有意義,基本不會這樣操作);(3)、當0<n<length時,表示跳過n個元素後(不包括元素n),結果返回含有剩下的元素的流(使用頻率較多);(4)、當n>=length時,表示跳過所有元素,結果返回空流,你可以使用count方法來判斷此時流中元素的總數必定為0。

limit方法。說完了skip()方法,接下來聊聊limit()方法。顧名思義這個就是限制流中的元素,即用於將前n個元素返回新的流,如圖所示:

同樣也通過舉一個例子來進行說明:

 public static void limitTest(long n){
        Stream<Integer> integerStream = Stream.of(1,2,3,4,5,6);
        integerStream.limit(n).forEach(System.out::println);
    }

方法limit()中的參數n不同將會導致不同的結果,具體情況如下:
(1)、當n<0時,運行結果會拋出IllegalArgumentException異常;(2)、當n=0時,相當不取元素,結果返回空流;(3)、當0<n<length時,表示取前n個元素,結果返回新的流(使用頻率較多);(4)、當n>=length時,表示取所有元素,結果返迴流本身,你可以使用count方法來判斷此時流中元素的總數必定為length。

區別:注意這裡談skiplimit方法的區別是局限於有限流skiplimit方法都是對流進行截取操作,區別在於skip方法必須時刻監測流中元素的狀態,才能判斷是否需要丟棄,因此skip屬於狀態操作。而limit只關心截取的是否是其length,是就立馬中斷操作返迴流,因此limit屬於中斷操作。

13、並行(parallel)執行

parallelStream是流並行處理程式的代替方法。舉個例子來說,下面是使用 parallelStream並行流來輸出空字元串的數量:

        List<String> stringList= Arrays.asList("a","","b","","e","","c","","f");
        //獲取空字元串的數量
        long count = stringList.parallelStream().filter(string -> string.isEmpty()).count();
        System.out.println(count);  // 4

14、anyMatch、allMatch和noneMatch

anyMatch方法用於判斷流中是否存在滿足特定條件的元素,返回類型是boolean類型。(只要有一個條件滿足即返回true)

        List<String> stringList = Arrays.asList("hello","the","fruit","name","is","banana");
        Boolean result = stringList.parallelStream().anyMatch(item -> item.equals("name"));
        System.out.println(result);  // true

allMatch方法用於判斷流中是否存在滿足特定條件的元素,返回類型是boolean類型。(必須全部滿足才會返回true)

        List<String> stringList = Arrays.asList("hello","the","fruit","name","is","banana");
        Boolean result = stringList.parallelStream().allMatch(item -> item.equals("name"));
        System.out.println(result);  // false

noneMatch方法用於判斷流中是否存在滿足特定條件的元素,返回類型是boolean類型。(全都不滿足才會返回true)

        List<String> stringList = Arrays.asList("hello","the","fruit","name","is","banana");
        Boolean result = stringList.parallelStream().noneMatch(item -> item.equals("name"));
        System.out.println(result);  // false

上面這個例子就是因為有一個滿足條件就返回了false。

15、reduce

reduce的意思是減少,而Stream中reduce方法就是用於實現這個目的,它根據一定的規則將Stream中的元素進行計算後返回一個唯一的值。舉個例子來說:

        Stream<String> stringStream = Stream.of("my","name","is","envy");
        Optional<String> stringOptional = stringStream.reduce((before, after) -> before+"、"+after);
        stringOptional.ifPresent(System.out::println);  // my、name、is、envy

16、findFirst和findAny

findFirst方法用於返回list列表中第一個元素,注意如果元素不存在則拋異常。舉個例子來說:

        List<String> stringList = Arrays.asList("do","what","you","want","to","do","and","do","it");
        Optional<String> result = stringList.parallelStream().findFirst();
        System.out.println(result.get());  // do

注意若Optional為空,則get方法會拋出異常,但是你可以使用orElse(defaultVal);或使用orElseGet(() -> {// doSomething; return defaultVal;});來返回默認值。舉個例子來說:

        List<String> stringList = Arrays.asList();
        Optional<String> result = stringList.parallelStream().findFirst();
        System.out.println(result.orElse("沒有元素"));  // 沒有元素

        List<String> stringList = Arrays.asList();
        Optional<String> result = stringList.parallelStream().findFirst();
        System.out.println(result.orElseGet(() ->{return "沒有元素";}));  // 沒有元素

17、summaryStatistics統計

summaryStatistics方法用於產生統計結果的收集器,舉個例子來說:

        List<Integer> integerList = Arrays.asList(3,2,3,5,6,8,9);
        IntSummaryStatistics result = integerList.stream().mapToInt((x)->x).summaryStatistics();
        System.out.println("列表中最大的數:"+result.getMax());
        System.out.println("列表中最小的數:"+result.getMin());
        System.out.println("列表中所有數之和:"+result.getSum());
        System.out.println("列表中所有數的平均數:"+result.getAverage());
        System.out.println("列表中元素的個數:"+result.getCount());

18、Joining集合元素的拼接

集合元素的拼接,其實就是指定分隔符將列表中的元素合併成一個字元串,注意joining方法是存在於Collectors中的。舉個例子來說:

        List<String> stringList = Arrays.asList("my","name","is");
        System.out.println(stringList);  // [my, name, is]

        String result = stringList.stream().collect(Collectors.joining(","));
        System.out.println(result);  // my,name,is

        String newString = Stream.of("I","come","from bei").collect(
                Collectors.collectingAndThen(
                        Collectors.joining(","),x-> x+"jing"));
        System.out.println(newString);  // I,come,from beijing

19、Collectors之流轉換成集合

Collectors類實現了很多歸約操作,例如將流轉換成集合和聚合元素等,Collectors 可用於返回列表或字元串。下面通過舉一些經常會使用到的例子來進行說明:
先在外部新建一個Student實體類,後續會使用到:

public class Student {
    private String name;
    private Long score;
   //getter/setter/toString/AllArgsConstructor
}

然後看下面的例子程式碼:

        List<Integer> integerList = Arrays.asList(1,2,3,4,5);
        //流轉列表
        List<Integer> newList = integerList.stream().map(i -> i*10).collect(Collectors.toList());
        System.out.println("新列表:"+newList);  //[10, 20, 30, 40, 50]

        //流轉集合
        Set<Integer> integerSet = integerList.stream().map(i -> i*10).collect(Collectors.toSet());
        System.out.println("新集合:"+integerSet);  //[50, 20, 40, 10, 30]

        //流轉映射
        Map<String,String> stringStringMap = integerList.stream().map(i ->i*10).collect(
                Collectors.toMap(key -> "key"+key/10,value -> "value:"+value)
        );
        System.out.println("新映射:"+stringStringMap);  //{key1=value:10, key2=value:20, key5=value:50, key3=value:30, key4=value:40}


        //流轉有序集合TreeSet
        TreeSet<Integer> integerTreeSet = Stream.of(1,6,3,7,2).collect(Collectors.toCollection(TreeSet::new));
        System.out.println("新有序集合:"+integerTreeSet);  //[1, 2, 3, 6, 7]


        //自定義對象流
        List<Student> studentList = Arrays.asList(
                new Student("envy",100L),
                new Student("movie",90L),
                new Student("book",80L)
        );

        //獲得對象
        Map<String,Student> studentAndModelMap = studentList.stream().collect(Collectors.toMap(
                Student::getName, Function.identity()
        ));
        Student student = studentAndModelMap.get("envy");
        System.out.println(student);  //Student{name='envy', score=100}

        //獲得屬性
        Map<String,Long> studentAndStudentScoreMap = studentList.stream().collect(Collectors.toMap(
                Student::getName, Student::getScore
        ));
        Long score = studentAndStudentScoreMap.get("envy");
        System.out.println(score);  //100

20、Collectors之元素聚合

其實這個元素聚合歸根結底還是Collectors類中的方,下面就來介紹聚合元素這個操作,Collectors 可用於返回列表或字元串。下面通過舉一些經常會使用到的例子來進行說明:

        //元素聚合

        List<Integer> integerList = Arrays.asList(1,5,8,3,6,2,9,7,4);

        //求最大值
        Integer maxValue = integerList.stream().collect(
                Collectors.collectingAndThen(
                        Collectors.maxBy((a,b) -> a-b), Optional::get
                ));
        System.out.println(maxValue);  // 9

        //求最小值
        Integer minValue = integerList.stream().collect(
                Collectors.collectingAndThen(
                        Collectors.minBy((a,b) -> a-b), Optional::get
                ));
        System.out.println(minValue);  // 1

        //求和
        Integer sumValue = integerList.stream().collect(
                Collectors.summingInt(item ->item)
        );
        System.out.println(sumValue);  // 45

        //平均值
        Double avgValue = integerList.stream().collect(
                Collectors.averagingDouble(x -> x)
        );
        System.out.println(avgValue);  // 5.0


        //集合轉映射
        String listToMap = Stream.of("my","name","is","envy").collect(
                Collectors.mapping(
                        x->x.toUpperCase(),Collectors.joining(",")
                )
        );
        System.out.println(listToMap);  // MY,NAME,IS,ENVY

21、累計操作

reducing累計操作,也是Collectors類中的方法,用於進行元素的累計操作。先來看一個例子,用於計算出[2,3,5,6]這個列表中所有元素各加1之後的所有元素之和,很簡單口算都可以知道答案是20。你可能有很多種想法,這裡提供幾種以供你參考:

        //方法一,不使用stream
        int[] ints = {2,3,5,6};
        int resultSum =0;
        for(int i:ints){
            i++;
            resultSum+=i;
        }
        System.out.println(resultSum);  //20

        //方法二,使用stream流的map+summingInt方法
        List<Integer> integerList = Arrays.asList(2,3,5,6);
        Integer integerOne = integerList.stream().map(i ->i+1).collect(
                Collectors.summingInt(x ->x)
        );
        System.out.println(integerOne);  //20


        //方法三,使用stream流的reducing方法
        Integer integerTwo = integerList.stream().collect(
                Collectors.reducing(0,x->x+1,(sum,b) -> {
                    return sum+b;
                })
        );
        System.out.println(integerTwo);  //20


        // reducing還可以用於更複雜的累計計算,不局限於加減乘除等操作
        Integer integerThree = integerList.stream().collect(
                Collectors.reducing(1,x->x+1,(result,b) -> {
                    return result*b;
                })
        );
        System.out.println(integerThree);  // 3*4*6*7=504

22、groupingBy 分組

groupingBy分組這個功能在實際開發中用的非常多,因此有必須要好好用一下,它也是存在於Collectors類中的。來看一下這個Collectors.groupingBy方法的源碼,它有三個重載方法,這裡就以只有一個參數的方法為例進行說明:

public static <T, K> Collector<T, ?, Map<K, List<T>>>
    groupingBy(Function<? super T, ? extends K> classifier) {
        return groupingBy(classifier, toList());
    }

可以發現它的參數只有一個:Function<? super T, ? extends K> classifier,類型是Function類型也就是個函數,Function的返回值可以是要分組的條件,或者是要分組的欄位。groupingBy方法的返回的結果是一個Map,其中key的數據類型為Function體中的計算類型(也就是參數類型),value是List類型也就是分組的結果。

接下來通過一個例子來介紹如何使用它,這個例子也非常簡單,給定[1,2,3,4,5,6,7,8,9]這個列表,如何將其按照奇數和偶數來劃分為兩組。用以往的知識你可能會這樣操作:

        List<Integer> oneList = new ArrayList<>();  //奇數列表
        List<Integer> twoList = new ArrayList<>();  //偶數列表
        for(Integer item:integerList){
            if(item%2==0){
                twoList.add(item);
            }else {
                oneList.add(item);
            }
        }
        System.out.println(oneList);  // [1, 3, 5, 7, 9]
        System.out.println(twoList);  // [2, 4, 6, 8]

但是如果你使用了stream那就變得簡單多了:

        //方法二,使用stream
        Map<Boolean, List<Integer>> resultMap = integerList.stream().collect(
                Collectors.groupingBy(item -> item%2 ==0)
        );
        System.out.println(resultMap);  // {false=[1, 3, 5, 7, 9], true=[2, 4, 6, 8]}

        Map<Boolean, List<Integer>> twoPartition  = integerList.stream().collect(
                Collectors.partitioningBy(item -> item%2 ==0)
        );
        System.out.println(twoPartition);  //twoPartition就是將結果為為兩組


        //還可以自定義分組的條件
        List<Student> studentList = Arrays.asList(
                new Student("book",100L,1),
                new Student("movie",90L,2),
                new Student("fruit",80L,2),
                new Student("vegetable",70L,4)
        );
        //根據某個欄位進行分組
        Map<Integer,List<Student>> studentMap = studentList.stream().collect(
                Collectors.groupingBy(item ->item.getId())
        );
        System.out.println(studentMap);
        //{1=[Student{name='book', score=100}], 2=[Student{name='movie', score=90}, Student{name='fruit', score=80}], 4=[Student{name='vegetable', score=70}]}

        //還可以結合前面的統計結果處理器來對結果進行分析
        Map<Integer, LongSummaryStatistics> summaryStatisticsMap = studentList.stream().collect(
                Collectors.groupingBy(
                        Student::getId, Collectors.summarizingLong(Student::getScore)
                )
        );

        LongSummaryStatistics statisticsOne = summaryStatisticsMap.get(1);
        LongSummaryStatistics statisticsTwo = summaryStatisticsMap.get(2);

        System.out.println(statisticsOne.getMax());  //100
        System.out.println(statisticsOne.getMin());  //100
        System.out.println(statisticsOne.getAverage());  //100.0
        System.out.println(statisticsOne.getCount());  //1
        System.out.println(statisticsOne.getSum());  //100.0

        System.out.println("*********");
        System.out.println(statisticsTwo.getMax());  //90
        System.out.println(statisticsTwo.getMin());  //80
        System.out.println(statisticsTwo.getAverage());  //85.0
        System.out.println(statisticsTwo.getCount());  //2
        System.out.println(statisticsTwo.getSum());  //170
    }

上面基本上把日常開發過程中可能會遇到的場景都進行了介紹,但是我覺得這是做了第一步如何使用它,後續會出一些文章來好好研究裡面的源碼,同時會對上面的一些方法進行更深層次的研究和使用。

獲取更多內容可以關注微信公眾號:餘思部落格。