Java JDK1.8 核心特性詳解------Stream(流)的使用

在前面的章節(Java JDK1.8 核心特性詳解------Stream(流)的基本介紹),我講述了流的基本介紹,包括流的一些特性以及簡單的用法。在下面這篇文章裏,我們會更加具體的學習如何使用Stream對數據進行篩選、映射、查找、匹配、歸約等基本功能,以及如何用新的方式創建流


目錄

流的基本使用

篩選

映射

查找和匹配

 數值流

構建流


流的基本使用

下面這個List是後面用來篩選的基本數據。由於《Java 8 實戰》在講這部分內容的時候有配圖,有利於大家的理解,因此我在舉案例的時候採用的是書中的內容,然後在代碼下面會貼上代碼的解析圖(其實還有一部分是懶,不想畫圖(●'◡'●)),代碼中會用到Lambda表達式和方法引用,兩種表達式作用是一樣的,怎麼時候可以看以前的博客(JDK1.8 總目錄裏有)。

        List<Dish> menu = Arrays.asList(
                //參數分別爲食物名稱,是否是蔬菜,卡路里,食物類型
                //每個參數都有對應get方法
                new Dish("豬肉", false, 800, "肉"),
                new Dish("牛肉", false, 700, "肉"),
                new Dish("雞肉", false, 400, "肉"),
                new Dish("蝦", false, 300, "魚"),
                new Dish("三文魚", false, 450, "魚"),
                new Dish("米飯", false, 350, "其他"),
                new Dish("蔬菜", true, 530, "其他"),
                new Dish("水果", true, 120, "其他"),
                new Dish("披薩", true, 550, "其他"));

篩選

用boolean篩選

Stream提供了 filter 方法,該方法接收一個boolean作爲參數,並返回包含所有符合謂語的流。當我們要判斷數據是否滿足某個條件(某個字段爲boolean,或者某個字段表達式結果爲boolean)來篩選時,可以使用這個方法。:

        //篩選出菜單中是蔬菜的食物
        List<Dish> dishList = menu.stream().filter(dish->dish.isVegetarian()).collect(Collectors.toList());
        List<Dish> dishList = menu.stream().filter(Dish::isVegetarian).collect(Collectors.toList());

篩選各異的元素

Stream提供了 distinct 方法,該方法會將一個包含重複元素的流轉化爲每個元素數量都爲1的流(根據元素的equal和hashCode方法實現,類似數據庫的DISTINCT方法):

        List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
        //先篩選出集合中是偶數的元素,然後對元素做去重操作
        numbers.stream().filter(i -> i%2==0).distinct().forEach(System.out::print);
-----------------------------------------------------------------------------------------
打印結果:24

截短流

流支持 limit(n) 方法(還是類似於數據庫的LIMIT關鍵字),他會給我們返回一個限定數量n的流,當原先流元素數量少於n時,就返回原先流的全部元素。

        //篩選菜單中卡路里大於300的食物的前三道菜
        List<Dish> dishList = menu.stream().filter(d -> d.getCalories() > 300).limit(3).collect(Collectors.toList());

注意,之前說過,Stream是把一個元素按照中間操作執行完以後纔去執行下一個元素的。從上面的圖我們可以看到,當對元素5執行完以後,已經滿足了limit(3)這個操作(已經獲得三個元素)。那麼,就會馬上返回limit(3)產生的流(元素2,3,5組成的數據流),不再會對元素6進行操作。這個就是Stream的優化。

跳過元素

流還支持 skip(n) 方法,返回一個扔掉了前n個元素的流。當流中元素不足n個,則返回一個空流。

        //篩選菜單中卡路里大於300的食物的菜,並跳過前兩道
       List<Dish> dishList = menu.stream().filter(d -> d.getCalories() > 300).skip(2).collect(Collectors.toList());

表中可以看出,過濾以後有元素1、2、3、4滿足要求,但是跳過了元素1、2,只取了後面的3、4。如果篩選過後只有元素1、2,那麼最後會返回一個數量爲0的空流。

映射

將一個類型元素轉爲另一個類型的元素

  流支持map方法,它接受一個函數作爲參數。然後將函數應用到每個元素,將元素轉爲另一個類型,並將新的類型的元素轉成一個新的流返回。

        //將獲取菜單的食物名,相當於傳入的是Dish類型的元素,然後返回String類型的參數
        List<String> stringList = menu.stream().map(dish -> dish.getName()).collect(Collectors.toList());
        List<String> stringList = menu.stream().map(Dish::getName).collect(Collectors.toList());

因爲getName返回的是String類型的元素,因此,map返回的流也變成了Stream<String>類型。

流的扁平化

上面的map方法可以幫我們把流中的某一個元素轉成另一個類型的不同元素,但是不能把流中的所有元素轉成另一個類型的某一個元素。舉個例子,我們想得到hello world這個兩個單詞中有哪些不一樣的字母,我們可能會這樣寫。

        Arrays.asList("hello", "word").stream().map(s -> s.split("")).distinct().collect(Collectors.toList());

 沒錯,這樣寫是錯的。map返回的是Stream<String[]>,就像下面圖所示。

遇到這種問題,我們可以是使用 flatMap 來解決這個問題。

//Arrays有一個靜態方法Arrays.stream()可以將數組[T]轉成Stream<T>。
List<String> collect = Arrays.asList("hello", "word").stream().map(s -> s.split("")).flatMap(Arrays::stream).distinct()
                .collect(Collectors.toList());

 我們可以這樣理解,flatMap 的作用是把一種流轉成另一種流,並將轉換後的流合併成一個流,還是不太清楚的可以對比上面和下面的代碼。

//這個方法跟上面的方法是等價的,flatMap把Stream<Stream<String>>流轉成了Stream<Integer>流
Stream<Stream<String>> collect1 = Arrays.asList("hello", "word").stream().map(s -> s.split("")).map(Arrays::stream).distinct();
Stream<String> stringStream = collect1.flatMap(a -> a);

查找和匹配

匹配

Stream提供了多種根據謂語(表達式爲boolean)來匹配的方法:allMatchanyMatchnoneMatch。分別對應是否匹配所有元素是否匹配至少一個元素是否沒有任何元素匹配,這些方法返回的也是boolean。

        List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 4, 2);
        if (numbers.stream().allMatch(i -> {
            System.out.print(i);
            return i < 4;
        })) {
            System.out.println("");
            System.out.println("所有的元素都小於4");
        } else {
            System.out.println("");
            System.out.println("存在元素不小於4");
        }

        if (numbers.stream().anyMatch(i -> {
            System.out.print(i);
            return i > 2;
        })) {
            System.out.println("");
            System.out.println("最少有一個元素大於2");
        }

        if (numbers.stream().noneMatch(i -> {
            System.out.print(i);
            return i > 3;
        })) {
            System.out.println("");
            System.out.println("沒有一個元素都大於3");
        } else {
            System.out.println("");
            System.out.println("存在一個元素大於3");
        }
-------------------------------------------------------------------------------
運行結果:
121334
存在元素不小於4
1213
最少有一個元素大於2
121334
存在一個元素大於3

大家可以通過debug的方式感受一下運行方式。這三個操作都用到了短路(類似 || 和 &&),當有一個條件不滿足條件時,就直接返回false。

查找

Stream存在兩個查找方法  findFirst() findAny() ,這兩個方法分別是返回流中的第一個元素和流中的任意一個元素,可以配合其他流來使用。這兩個方法會返回Optional<T>類型的結果。下面舉個例子 :
 

        List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 4, 2);
        Optional<Integer> first = numbers.stream().filter(i->i>2).findFirst();
        System.out.println(first.get());
        Optional<Integer> any = numbers.stream().filter(i->i>2).findAny();
        System.out.println(any.get());
-------------------------------------------------------------------------------
運行結果:
3
3

Optional<T>是JDK8的一個新的容器類,代表一個值存在或不存在,後面會講這個內容(雖然我覺得這個類沒什麼用)。你們可能會奇怪  findFirst() findAny() 很像,有什麼區別。原因是並行,我們之前說過,用流可以很方便的使用並行,當我們對有順序的流執行並行處理時,如果我們想在並行的時候按照流的順序返回第一個元素,那使用對並行限制更大一點的findFirst()。如果你只要返回一個元素,那就可以使用findAny(),他可能會返回順序流中第二或者第三個滿足要求的元素。

歸約

之前的方法中,流之前的元素是沒有互動的,例如加、減、乘、除、比較等。但是,往往現實中我們需要對集合中的元素進行組合。舉個栗子:我們要對一個流裏的所有元素求總和,在以前我們可能使用for循環來實現。在JDK1.8中,Stream提供了reduce 操作來表達更復雜的查詢,例如對int集合裏的元素求和,或者查詢int集合中最大的一個數。這些操作要把流中的所有元素反覆結合起來。這類操作被歸類爲 歸約操作 。下面舉個例子:

        //計算數組的和
        List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 4, 2);
        //傳統求和方式
        int s = 0;
        for (int i = 0; i < numbers.size(); i++) {
            s += numbers.get(i);
        }
        //使用流求和
        Integer sum = numbers.stream().reduce(0, (a, b) -> a + b);
        //與上面的等價
        Integer sum2 = numbers.stream().reduce(0, Integer::sum);

我們可以看到,使用流求和方便了很多。reduce(i,函數式接口 接收兩個參數,一個i,就是總變量的初始值,一個是函數式接口的實現類,一般使用Lambda表達式。我們通過圖來演示上面流的過程

reduce方法第一步會將初始化值 i 當成a,然後從流中獲取第一個元素,當成b。之後將a和b進行操作(這裏的操作是a+b)後的值重新指定成a,再去流中獲取一個值當成b,就這樣一直計算直到最後。同樣的,我們可以用reduce來獲取一個數組中的最大值。

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

下面這個表是到目前爲止我們提到過的Stream提供的方法:

 數值流

我們之前用reduce對數據進行歸約(Integer sum2 = numbers.stream().reduce(0, Integer::sum);),但是,這個代碼存在一個問題,這裏暗含了拆箱的成本,每個Integer都會被拆箱成一個基本數據類型。Stream給我們提供瞭解決方法。

原始類型特化流

Java 8 提供了三個接口來幫助我們處理裝箱,拆箱的成本,分別是:IntStream,DoubleStream和LongStream。這三個接口可以將流中的元素特化成基本數據類型,也可以把基本數據類型裝箱爲包裝數據類型,使用方法也基本相同。

映射到數值流:

將流轉化爲特殊版本版本的常用方法是 mapToInt ,mapToDouble ,mapToLong  這三個方法會將Stream<T>流轉成一個特定的流。舉個例子:

        //計算卡路里總和
        int sum = menu.stream()
                      //這裏返回的是一個IntStream,而不是Stream<Integer>
                      .mapToInt(Dish::getCalories)
                      .sum();

在上面這段代碼中 ,我們將返回的數據轉化成IntStream,並且調用IntStream接口的sum方法,求出卡路里的總和。除此之外,IntStream還支持max ,mix,average等方法。

轉換回封裝對象:

 如果我們要將特殊流轉換回普通的流,可以使用boxed方法。

        Stream<Integer> boxed = intStream.boxed();

生成數據範圍

在平時,我們可能需要生成一個範圍內的數字,例如,想要生成1-100的數字。Java  8 提供了兩個可以用於IntStream和LongStream的靜態方法:range和rangeClosed。兩個方法都是第一個參數接受起始值,第二個接受結束值。range不包括結束值,rangeClosed包含結束值。這兩種方式生成的流都爲原始數據類型,不存在拆箱和裝箱的成本。舉個例子

        //count=50,包含100
        long count = IntStream.rangeClosed(1, 100).filter(i -> i % 2 == 0).count();
        //count2=49,不包含100
        long count2 = IntStream.range(1, 100).filter(i -> i % 2 == 0).count();

構建流

之前一般是使用stream的方法生成流,接下來將介紹幾種另外的生成流的方法。

由值創建流

可以使用Stream.of顯性的創建一個流。

        Stream<String> stream = Stream.of("Java 8", "JDK 1.8", "Stream");

由數組創建流 

可以使用靜態方法Arrays.stream從數組創建一個流。

        int[] number = {1, 2, 3, 4, 5};
        IntStream stream1 = Arrays.stream(number);

由文件生成流 

java.nio.file.Files有很多靜態方法都能返回一個流,例如,Files.lines會返回給定文件中的一行。

        Stream<String> lines = Files.lines(Paths.get("data.txt"));

 由函數生成流:創建無限流

Stream API提供了兩個靜態方法來從函數生成流:Stream.iterate和Stream.generate,這兩種方式產生的流都是包裝類型,在使用時可能會包含拆箱和裝箱的成本,很大程度上會影響程序的效率。

迭代

iterate接受一個初始值,還有一個依次應用在每個產生的新值上的Lambda,這有點類似reduce方法,但是reduce是對初始化值來回操作,而iterate會在初始化值的基礎上生成新的值。舉個例子:

        //這會生成從1到100的100個值,sum值爲5050
        int sum = Stream.iterate(1, n -> n + 1).limit(100).mapToInt(n -> n).sum();

 iterate一般配合limit使用,輸出我們限定的數量,不然會一直生成下去,我們認爲這個流是無界的。再舉一個複雜的例子。使用iterate生成斐波那契數列。

//該語句會生成斐波那契數列的前20個元素
Stream.iterate(new int[]{0,1}, n -> new int[]{n[1],n[0]+n[1]}).limit(20).map(n->n[0]).forEach(System.out::println);

 生成

generate方法是根據Supplier<T>的Lambda表達式(()->T)創建無限流。下面這個例子,將通過Math::random生成一個數量爲5的流,並打印。

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

更多與JDK1.8相關的文章請看:Java JDK1.8 核心特性詳解----(總目錄篇)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章