jdk8的新特性總結(二):StreamAPI

這篇文章我們一起來學習java8最重要的新特性之一,強大的StreamAPI,在本文中的代碼示例中我們會大量使用lambda表達式,如果對lambda表達式還不太熟悉,可以看一下上一篇文章。

java8中Java8中有兩大最爲重要的改變。第一個是 Lambda 表達式;另外一個則是 Stream API(java.util.stream.*)。Stream 是 Java8 中處理集合的關鍵抽象概念,它可以指定你希望對集合進行的操作,可以執行非常複雜的查找、過濾和映射數據等操作。
使用Stream API 對集合數據進行操作,就類似於使用 SQL 執行的數據庫查詢。也可以使用 Stream API 來並行執行操作。簡而言之,Stream API 提供了一種高效且易於使用的處理數據的方式。

一、首先我們來思考一個問題,什麼是Stream(流)?

流是一個數據渠道,用於操作數據源(集合,數組等)所生成的元素序列,總之,集合主要是數據,流主要是計算。

二、Stream操作的三個步驟:

  • 創建Stream

通過一個數據源獲取一個流,例如,List中的stream()方法可以直接返回一個Stream對象。

  • 中間操作

我們需要對流中的數據進行的操作,比如循環處理(map),過濾(filter)等

  • 終止操作

流都是惰性求值的,這個我們在後面會講到,需要進行一個終止操作,這樣纔會返回中間操作後的數據。

如下圖:

Stream操作的三個步驟

注意:

Stream只是一個計算通道,自己不會存儲元素;

Stream不會改變源對象,相反,他們會返回一個新的Stream對象。

Stream操作是延時的,只有在執行終止操作時纔會執行。

三、創建Steam的方式:

1、由Collection子類(List,Set)來創建流:

java8擴展了Collection接口,提供了stream(返回順序流)和parallelStream(返回並行流)兩個方法。並行流我們後面在詳細討論,示例代碼:

@Test
public void test(){
    List<String> list = Arrays.asList("a","b","c");
    Stream stram = list.stream();
    Stream parallelSteam = list.parallelStream();
}

2、由數據來創建流:

數組可以通過Arrays工具類的stream方法來獲得一個Steam對象,示例代碼:

public void test2(){
    String[] strArr = new String[]{"a","b","c"};
    Stream stram = Arrays.stream(strArr);
    //還有許多重載形式的方法,可以返回帶類型的Stream,例如:
    IntStream stram2 = Arrays.stream(new int []{1,2,3,4,5});
}

3、通過具體值來創建流:

通過Stream的靜態方法Stream.of(T...values)可以創建一個流,它可以接受任意個值,代碼示例:

@Test
public void test3() {
    Stream s = Stream.of("1","2",3,new String[]{"a", "b", "c"});
}

4、通過函數來創建流(無限流)

通過Stream.iterate()和Stream.generate()方法可以創建無限流,什麼是無限流,我們來看一下代碼示例就明白了:

@Test
public void test4() {
    //Stream.iterate方法第一個方法表示開始值得,第二個參數需要提供一個一元操作函數,我們用lambda表達式傳遞給它
    Stream stream1 = Stream.iterate(0, (x) -> x + 2);
    stream1.forEach(System.out::println); //輸出的是0,2,4,6,8,10....將會一直循環下去,永遠不停息
    //Stream.generate需要一個供給型函數接口
    Stream stream2 = Stream.generate(() -> 1);
    stream2.forEach(System.out::println); //輸出無數個1,將會一直循環下去,永遠不停息
}

備註:實際運用中,我們肯定不會生成一個無限流,除非你想要死循環,我們會結合Stream的終止操作,如limit來獲取有指定個數元素的流:

@Test
public void test5(){
    //我們從0開始獲取前50個偶數
    Stream stream1 = Stream.iterate(0, (x) -> x + 2).limit(50);
    stream1.forEach(System.out::println);//輸出0,2,4,6,8。。。。98
}

四、Stream的中間操作:

Stream可以進行一系列的流水線式的中間操作,除非流水線上觸發終止操作,否則,這些中間操作不會進行任何處理,而在終止操作時一次性處理,這個我們叫做Stream的惰性求值。

記住,中間操作不管做多少次,都不會改變原來的流,只會返回一個新的流;

Stream的中間操作可以分爲以下幾類:

中間操作:篩選與切片

方法 描述 filter(Predicate d) 接受一個斷言型函數,對Stream流中的元素進行處理,過濾掉不滿足條件的元素 distinct 篩選元素,通過Stream元素中的hasCode和equals方法來去除重複元素 limit(long maxSize) 截斷流,使元素不超過manSize指定的數量 skip(Long n) 跳過元素,返回一個扔掉了前n個元素的流,若流中的元素不足n個,則會返回一個空流

中間操作:映射

方法 描述 map(Function f) 接受一個函數型接口作爲參數,該函數會對流中的每個元素進行處理,返回處理後的流 mapToDouble(ToDoubleFunction  f) 接口一個函數型接口作爲參數,該函數會對流中的每個元素進行處理,並返回一個Double值,最終得到一個Stream<Double> mapToInt(ToIntFunction f) 接口一個函數型接口作爲參數,該函數會對流中的每個元素進行處理,並返回一個Int值,最終得到一個Stream<Int> mapToLong(ToLongFunction f) 接口一個函數型接口作爲參數,該函數會對流中的每個元素進行處理,並返回一個Long值,最終得到一個Stream<Long> flatMap(Function f) 接受一個函數作爲參數,將流中的每個值都轉換成一個新流,最後再將這些流連接到一起

中間操作:排序

方法 描述 sorted 返回一個新流,流中的元素按照自然排序進行排序 sorted(Comparator comp) 返回一個新流,並且Comparator指定的排序方式進行排序

中間操作練習:

/**
 * Stream的中間操作練習
 */
public class StreamTest2 {
    List<Employee> emps = Arrays.asList(
            new Employee("張三", 18, 6666.66),
            new Employee("李四", 20, 7777.77),
            new Employee("王五", 36, 8888.88),
            new Employee("田七", 55, 11111.11),
            new Employee("趙六", 55, 9999.99),
            new Employee("趙六", 45, 12222.22)
    );
    /**
     * 篩選與切片 filter, distinct limit skip
     */
    @Test
    public void test1() {
        //1.過濾掉年齡小於25的員工
        emps.stream().filter((e) -> e.getAge() > 25).forEach(System.out::println);
        //2.過濾掉姓名重複的員工
        emps.stream().distinct().forEach(System.out::println);
        //3.獲取前三名員工
        emps.stream().limit(3).forEach(System.out::println);
        //4.獲取第三名以後的員工
        emps.stream().skip(3).forEach(System.out::println);
        //5.先獲取前3名員工,再獲取其中年齡大於25的員工。(中間操作可以任意次)
        emps.stream().limit(3).filter(e -> e.getAge() > 25);
    }
    /**
     * 映射操作 map, mapToDouble, mapToInt, mapToLong, flatMap
     */
    @Test
    public void test2() {
        //1. 獲取所有員工的姓名
        emps.stream().map(e -> e.getName()).forEach(System.out::println);
        //2. 獲取所有員工的工資,這裏工資是Double類型,我們可以用mapToDouble方法
        emps.stream().mapToDouble(e -> e.getSalary()).forEach(System.out::println);
        //3. 獲取所有員工的年齡,用mapToInt方法
        emps.stream().mapToInt(e -> e.getAge()).forEach(System.out::println);
        //4. 將員工年齡轉換成Long型並輸出,PS:這裏就算不用longValue系統也會自動將getAge轉換成Long類型
        emps.stream().mapToLong(e -> e.getAge().longValue()).forEach(System.out::println);
        /**
         * 5.將所有員工的 姓名,年齡,工資轉換成一個流並返回
         * 首先我們用map方法來處理,這種方式返回的是六個Stream對象,數據結構可以類似於:
         * [{"張三",18, 6666.66},{"李四",20, 7777.77}, {"王五", 36, 8888.88}, {"趙六", 24, 9999.99}, {"田七", 55, 11111.11}, {"趙六", 45, 12222.22}]
         * 然後我們用flatMap方法來處理,返回的是一個Stream對象,將所有元素連接到了一起,數據結構類似於:
         * ["張三",18, 6666.66,"李四",20, 7777.77, "王五", 36, 8888.88, "趙六", 24, 9999.99, "田七", 55, 11111.11, "趙六", 45, 12222.22]
         */
        emps.stream().map(e -> {
            return Stream.of(e.getName(), e.getAge(), e.getSalary());
        }).forEach(System.out::println);
        emps.stream().flatMap(e -> {
            return Stream.of(e.getName(), e.getAge(), e.getSalary());
        }).forEach(System.out::println);
    }
    /**
     * 排序操作 sorted
     */
    @Test
    public void test3() {
        //1.按照自然排序,注意,需要進行自然排序則對象必須實現Comparable接口
        emps.stream().sorted().forEach(System.out::println);
        //2.按照給定規則進行排序,(按照工資高低進行排序)
        emps.stream().sorted((x,y) -> Double.compare(x.getSalary(),y.getSalary())).forEach(System.out::println);
    }
}

 

五、Stream的終止操作:

Stream的終止操作用來獲取一系列流水線操作的最終結果,這個結果可以是任何值,例如boolean,List,Integer甚至可以是void,終止操作也分爲以下幾大類:

終止操作:查找與匹配

方法 描述 allMatch(Predicate p) 傳入一個斷言型函數,對流中所有的元素進行判斷,如果都滿足返回true,否則返回false。 anyMatch(Predicate p) 傳入一個斷言型函數,對流中所有的元素進行判斷,只要有一個滿足條件就返回true,都不滿足返回false。 noneMatch(Predicate p) 所有條件都不滿足,返回true,否則返回false。 findFirst() 返回流中的第一個元素。 findAny() 返回流中的任意一個元素。 count() 返回流中元素的個數。 max(Comparator c) 按照給定的排序規則進行排序後,返回流中最大值的元素 min(Comparator c) 按照給定的排序規則進行排序後,返回流中最小值的元素 forEach(Consumer c) 內部迭代。

終止操作:規約

方法 描述 reduce(T iden, BinaryOperator bo) 可以將流中的元素反覆結合起來,得到一個值,返回T reduce(BinaryOperator bo) 可以將流中的元素反覆結合起來,得到一個值,返回Optional。(Optional我們後面再聊)

終止操作:收集

方法 描述 collect(Collector c) 將流中的元素轉換成其他形式,接受一個Collector接口的實現,用於處理Stream流中的元素,將流轉換成其他形式的對象。

collector接口中方法的實現決定了如何對流執行收集操作(如收集到List,Set,Map)。java8中的Collectors類提供了很多靜態方法,可以非常方便的創建常用的收集器實例,具體方法與實例:

Collectors中創建收集器實例的方法
方法 返回類型 作用
toList List<T> 將流中的元素收集到List中,例如:list.stream.collect(Collectors.toList());
toSet Set<T> 將流中的元素收集到set中:Set<Employee> emps= list.stream().collect(Collectors.toSet())
toCollection Collection<T> 將流中的元素收集到創建的集合:list.stream().collect(Collectors.toCollection(ArrayList::new))
counting Integer 計算流中元素的個數:long count = list.stream().collect(Collectors.counting());
summingInt Long 對流中的整數屬性進行求和:inttotal=list.stream().collect(Collectors.summingInt(Employee::getSalary));
averagingInt Double 計算流中元素的平均值:doubleavg= list.stream().collect(Collectors.averagingInt(Employee::getSalary));
summarizingInt IntSummaryStatistics 返回一個IntSummaryStatistics,可以通過這個對象獲取統計值,如平均值:IntSummaryStatistics iss=list.stream().collect(Collectors.summarizingInt(Employee::getSalary))
joining String 連接流中的每個字符串:String str= list.stream().map(Employee::getName).collect(Collectors.joining());
maxBy Optional<T> 根據比較器選擇最大值:Optional<Employee> min = emps.stream().collect(Collectors.minBy((x ,y) -> Double.compare(x.getSalary(), y.getSalary())));
minBy Optional<T> 根據比較器選擇最小值:Optional<Employee> max = emps.stream().collect(Collectors.maxBy((x ,y) -> Double.compare(x.getSalary(), y.getSalary())));
reducing 規約產生的類型 從一個作爲累加器的初始值開始,利用BinaryOperator與流中元素逐個結合,從而歸約成單個值。Integer totalAge = emps.stream().collect(Collectors.reducing(0, Employee::getAge, Integer::sum));
colectingAndThen 轉換函數返回的類型 包裹一個收集器,對其結果進行轉換:int inthow= emps.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
groupingBy Map<K, List<T>> 根據某屬性對結果進行分組,屬性爲K,結果爲V:Map<String, List<Employee>> kv= emps.stream().collect(Collectors.groupingBy(Employee::getName));
partitioningBy Map<boolean, ListM<T>> 根據true或false進行分區:Map<Boolean,List<Employee>> vd = emps.stream().collect(Collectors.partitioningBy(e -> e.getAge() > 30));

終止操作練習:

/**
 * Stream的終止操作練習
 */
public class StreamTest3 {
    List<Employee> emps = Arrays.asList(
            new Employee("張三", 17, 6666.66),
            new Employee("李四", 20, 7777.77),
            new Employee("王五", 36, 8888.88),
            new Employee("田七", 55, 11111.11),
            new Employee("趙六", 55, 9999.99),
            new Employee("趙六", 45, 12222.22)
    );

    /**
     * 終止操作:查找與匹配 allMatch, anyMatch, noneMatch, findFirst, findAny, count, max, min ,forEach
     */
    @Test
    public void test1() {
        //1.查看是否有員工年齡是否都大於18
        boolean flag1 = emps.stream().allMatch(e -> e.getAge() > 18);
        System.out.println(flag1);   // false
        //2.先去掉張三,然後再判斷所有員工年齡是否都大於18
        boolean flag2 = emps.stream().filter(e -> !"張三".equals(e.getName())).allMatch(e -> e.getAge() > 18);
        System.out.println(flag2);  //true
        //3.是否有員工年齡大於50
        boolean flag3 = emps.stream().filter(e -> !"張三".equals(e.getName())).anyMatch(e -> e.getAge() > 50);
        System.out.println(flag3);  //true
        //4.沒有員工的年齡大於50?
        boolean flag4 = emps.stream().filter(e -> !"張三".equals(e.getName())).noneMatch(e -> e.getAge() > 50);
        System.out.println(flag4);  //false
        //5.先按照年齡進行排序,然後返回第一個員工。optional是java8用來包裝可能出現空指針的對象的對象
        Optional<Employee> op1 = emps.stream().sorted((x, y) -> Integer.compare(x.getAge(), y.getAge())).findFirst();
        System.out.println(op1.get());  //Employee{name='張三', age=17, salary=6666.66}
        //6. 查找任意一名員工的姓名,當使用順序流時,返回的是第一個對象,當使用並行流時,會隨機返回一名員工的姓名
        Optional<String> op2 = emps.parallelStream().map(e -> e.getName()).findAny();
        System.out.println(op2.get()); //會隨機獲取一名員工
        //7. 查詢員工人數
        Long count = emps.stream().count();
        System.out.println(count);
        //8.查詢員工工資最大的員工信息。PS: 這個也可以通過先按照工資排序,然後取第一個元素來實現
        Optional<Employee> maxSalary = emps.stream().max((x, y) -> Double.compare(x.getSalary(), y.getSalary()));
        System.out.println(maxSalary.get());
        //9.查詢員工最小年齡
        Optional<Employee> minAge = emps.stream().max((x, y) -> -Integer.compare(x.getAge(), y.getAge()));
        System.out.println(minAge.get());
        //10.循環輸出所有員工的信息
        emps.stream().forEach(System.out::println);
    }
    /**
     * 終止操作:規約 reduce
     * 規約操作兩個重載方法的區別是:
     * 一個未指定起始值,有可能返回null,所以返回的是一個Optional對象
     * 一個指定了起始值,不可能返回null,所以返回值可以確定是一個Employee
     */
    @Test
    public void test2() {
        //1.將所有員工的名字加上 -> 下一個員工的名字: 如 張三 -> 李四
        Optional<Employee> op1 = emps.stream().reduce((x,y) -> {x.setName(x.getName() + "->" + y.getName()); return x;});
        System.out.println(op1.get().getName());  //張三->李四->王五->田七->趙六->趙六
        //2.將所有員工的名字加上 -> 下一個員工的名字,並且開始以王八開始;
        // PS:測試時,請將例子1註釋掉,不然會影響emps對象
        Employee emp = emps.stream()
                .reduce(new Employee("王八", 65, 8888.88)
                        , (x,y) -> {
                            x.setName(x.getName() + "->" + y.getName());
                            return x;
                        });
        System.out.println(emp.getName());  //王八->張三->李四->王五->田七->趙六->趙六
    }

    /**
     * 終止操作:收集
     */
    @Test
    public void test3(){
        //1.按年齡排序後收集成一個list並返回
        List<Employee> list = emps.stream().sorted((x, y) -> Integer.compare(x.getAge(), y.getAge()))
                  .collect(Collectors.toList());
        list.forEach(System.out::println);
        //2.按工資高低排序後收集成一個Set返回
        Set<Employee> set = emps.stream().sorted((x, y) -> Integer.compare(x.getAge(), y.getAge()))
                .collect(Collectors.toSet());
        set.forEach(System.out::println);
        //3.按工資排序後收集到指定的集合中,這裏指定LinkedList
        LinkedList<Employee> linkedList = emps.stream().sorted((x, y) -> Integer.compare(x.getAge(), y.getAge()))
                .collect(Collectors.toCollection(LinkedList::new));
        linkedList.forEach(System.out::println);
        //4.計算流中元素的個數:
        long count = emps.stream().collect(Collectors.counting());
        System.out.println(count);
        //5.對所有員工的年齡求和:
        int inttotal = emps.stream().collect(Collectors.summingInt(Employee::getAge));
        System.out.println(inttotal);
        //6.計算所有員工工資的平均值:
        Double doubleavg= emps.stream().collect(Collectors.averagingDouble(Employee::getSalary));
        System.out.println(doubleavg);
        //7.返回一個IntSummaryStatistics,可以通過這個對象獲取統計值,如平均值:
        IntSummaryStatistics iss =emps.stream().collect(Collectors.summarizingInt(Employee::getAge));
        System.out.println(iss.getAverage());
        System.out.println(iss.getMax());
        System.out.println(iss.getMin());
        //8.連接所有員工的名字:
        String str= emps.stream().map(Employee::getName).collect(Collectors.joining());
        System.out.println(str);
        //9.相當於先按照工資進行排序,再取出排在第一位的員工
        Optional<Employee> min = emps.stream().collect(Collectors.minBy((x ,y) -> Double.compare(x.getSalary(), y.getSalary())));
        System.out.println(min);
        //10.相當於先按照工資進行排序,再取出排在最後一位的員工
        Optional<Employee> max = emps.stream().collect(Collectors.maxBy((x ,y) -> Double.compare(x.getSalary(), y.getSalary())));
        System.out.println(max);
        //11.從一個作爲累加器的初始值開始,利用BinaryOperator與流中元素逐個結合,從而歸約成單個值。
        Integer totalAge = emps.stream().collect(Collectors.reducing(0, Employee::getAge, Integer::sum));
        System.out.println(totalAge);
        //12.包裹一個收集器,對其結果進行轉換:
        int inthow = emps.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
        System.out.println(inthow);
        //13.根據某屬性對結果進行分組,屬性爲K,結果爲V:
        Map<String, List<Employee>> kv = emps.stream().collect(Collectors.groupingBy(Employee::getName));
        System.out.println(emps);
        //14.根據true或false進行分區,年齡大於30的分在true區,小於30的分在false區
        Map<Boolean,List<Employee>> vd = emps.stream().collect(Collectors.partitioningBy(e -> e.getAge() > 30));
        System.out.println(vd);
    }
}

總結:

Stream的總結其實就是一句話,記住Stream操作的三個步驟,創建流 -> 一系列中間操作 -> 終止操作拿到返回結果。

PS:上面練習的代碼放在了我的git倉庫中,感興趣的可以down下來再練練:https://github.com/caishi13202/jdk8.git

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