1. 程式人生 > 其它 >Java8 新特性之集合的流式程式設計

Java8 新特性之集合的流式程式設計

技術標籤:javajavastreamjdk1.8

文章目錄

一、集合流的概述

1.1 集合的流式程式設計簡介

Stream 是 JDK1.8 之後出現的新特性,也是 JDK1.8 新特性中最值得學習的兩種新特性之一。(另外一個是 Lambda 表示式)

Stream 是對集合操作的增強,流不是集合的元素,不是一種資料結構,不負責資料的儲存。流更像是一個迭代器,可以單向的遍歷一個集合中的每個元素,並且不可迴圈。

在這裡插入圖片描述

1.2 集合的流式程式設計的優點

為什麼要用集合的流式程式設計呢?想必它一定有值得人們去關注的優點。

有些時候,對集合的元素進行操作的時候,需要使用到其他操作的結果。在這個過程中,集合的流式程式設計可以大幅度的簡化程式碼的數量。將資料來源中的資料讀取到一個流中,可以對這個流中的資料進行操作(刪除、過濾、對映…)。每次的操作結果也是一個流物件,可以對這個流再進行其他的操作。

1.3 使用流式程式設計的步驟

通常情況下,對集合中的資料使用流式程式設計,需要經過以下三步:

  1. 獲取資料來源,將資料來源中的資料讀取到流中;
  2. 對流中的資料進行各種各樣的處理;
  3. 對流中的資料進行整合處理。

在上述三個過程中:對於過程 2,有若干方法可以對流中的資料進行各種各樣的操作,並且返回流物件本身,這樣的操作,被稱為中間操作。對於過程 3,有若干方法可以對流中的資料進行各種處理,並且關閉流,這樣的操作,被稱為最終操作

在中間操作和最終操作中,基本上所有的方法引數都是函式式介面,可以使用 Lambda 表示式來實現。使用集合的流式程式設計來簡化程式碼量,需要對 Lambda 表示式做到熟練掌握。

二、資料來源的獲取

2.1 資料來源簡介

資料來源,顧名思義,就是流中的資料的來源。讀取資料來源是流式程式設計的第一步。注意:資料被讀取到流中之後,流中進行處理的資料與資料來源中的資料沒有關係。也就是說,中間操作對流中的資料進行處理、過濾、對映、排序等操作是不會影響到資料來源中的資料的。

2.2 資料來源的獲取

這個過程,實際上是將一個容器中的資料讀取到另一個流中。因此無論什麼容器作為資料來源,讀取到流中的方法的返回值一定是一個 Stream。

如下所示為資料來源的獲取的示例程式碼:

/**
  * @Description 將集合作為資料來源,讀取集合中的資料到一個流中
  */
private static void collectionDataSource(){
    // 1. 例項化一個集合
    ArrayList<Integer> list = new ArrayList<>();
    // 2. 填充元素到集合中
    Collections.addAll(list, 1, 2, 3, 4, 5);
    // 3. 讀取集合中的元素到流中
    Stream<Integer> stream = list.stream();
    System.out.println(stream);
}

/**
  * @Description 將陣列作為資料來源,讀取陣列中的資料到一個流中
  */
private static void arrayDataSource(){
    // 1. 例項化一個數組
    Integer[] arr = new Integer[]{1, 2, 3, 4, 5};
    // 2. 讀取陣列中的資料到流中
    Stream<Integer> stream = Arrays.stream(arr);
    System.out.println(stream);
}

三、最終操作

3.1 最終操作簡介

最終操作就是將流中的資料整合到一起,比如可以存入到一個集合中,也可以直接對流中的資料進行遍歷、統計等。通過最終操作,可以從流中提取出來我們想要的資訊。

Stream 流的最終操作主要有以下幾種:

【注意事項】

之所以叫最終操作,是因為在最終操作結束之後,會關閉這個流,流中所有的資料都會被銷燬。如果使用一個已經關閉了的流,會出現異常。

3.2 collect

collect() 方法的作用就是將流中的資料收集到一起,最常見的處理就是將流中的資料存入一個集合中。collect 方法的引數是一個 Collector 介面,而且這個介面並不是一個函式式介面,因此我們可以自定義類來實現這個介面。但是在絕大多數情況下,是不需要我們自定義的,我們藉助工具類 Collectors 即可。

【舉例說明】

  • 例一:將 List 集合中的資料讀取到流中,然後原封不動將流中資料存入 Set 集合中。

    public class StreamDemo_02 {
       
       public static void main(String[] args) {
           Stream<Integer> stream = getDataSource();
           Set<Integer> set = stream.collect(Collectors.toSet());	// 將 stream 中資料存入 set 集合中
           System.out.println(set);
       }
       /**
        * 獲取資料來源
        */
       private static Stream<Integer> getDataSource(){
           Integer[] list = new Integer[]{1, 2, 3, 4, 5};
           return Arrays.stream(list);
       }
    }
    

    執行結果如下:
    在這裡插入圖片描述

  • 例二:將 List 集合中的資料讀取到流中,然後原封不動將流中資料存入 Map 集合中。

    public class StreamDemo_02 {
    
        public static void main(String[] args) {
            Stream<Integer> stream = getDataSource();
            /**
             * 將資料存入 map 中,這裡用到了 lambda 表示式
             * 這裡的 Collectors.toMap 的函式原型是:
        	 *	   Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
             *                           		   Function<? super T, ? extends U> valueMapper) 
             * 通過函式原型可以看到,函式的引數都是函式式介面 Function。
             * 其中:引數一為 key 的對映,引數二為 value 的對映。
             * 因此,可以使用 lambda 表示式,將 key 對映為集合元素的字串形式,將 value 對映為集合元素本身
             */
            Map<String, Integer> map = 
                stream.collect(Collectors.toMap(x -> x.toString(), x -> x));   // 將stream中資料存入map中
            System.out.println(map);
        }
        /**
         * 獲取資料來源
         */
        private static Stream<Integer> getDataSource(){
            Integer[] list = new Integer[]{1, 2, 3, 4, 5};
            return Arrays.stream(list);
        }
    }
    

    執行結果如下:
    在這裡插入圖片描述

3.3 reduce

reduce() 方法的作用是將流中的資料按照一定的規則聚合起來。

【舉例說明】

public static void main(String[] args) {
    reduceUsage();
}
/**
 * @Description 將流中的資料按照一定的規則聚合起來
 */
private static void reduceUsage(){
    Stream<Integer> stream = getDataSource();
    Integer optional = stream.reduce((x, y) -> x + y).get();
    System.out.println(optional);
}
/**
 * @Description 將一個容器中的資料讀取到流中
 */
private static Stream<Integer> getDataSource(){
    Integer[] list = new Integer[]{1, 2, 3, 4, 5};
    return Arrays.stream(list);
}

執行結果如下:
在這裡插入圖片描述
分析:為什麼結果是 15 呢?我們不難發現,15=1+2+3+4+5。也就是說 reduce 方法中引數 Lambda 表示式方法體為 x+y 的情況下相當於是對 List 集合內的元素進行迭代累加

3.4 count

count() 方法的作用是統計流中元素的數量。

【舉例說明】

Stream<Integer> stream = getDataSource();
long count = stream.count();
System.out.println(count);

3.5 forEach

foreach() 方法的作用是遍歷流中的資料。

【舉例說明】

private static void forEachUsage(){
    Stream<Integer> stream = getDataSource();
    // 物件方法的引用:System.out::println <==> x->System.out.println(x) 
    stream.forEach(System.out::println);					// 遍歷流中的資料並列印
}

3.6 max & min

max() :該方法的作用是按照指定的物件比較的規則進行大小比較,得出流中最大的資料。

min() :該方法的作用是按照指定的物件比較的規則進行大小比較,得出流中最小的資料。

【舉例說明】

private static void maxAndMinUsage(){
    Stream<Integer> stream = getDataSource();
    // 獲取流中的最大值
    Integer max = stream.max(Integer::compareTo).get();			// 引用特殊方法
    System.out.println(max);
    // 獲取流中的最小值
    // Integer min = stream.min(Integer::compareTo).get();
    // System.out.println(min);
}

3.7 Matching

Matching 並不是一個方法,而是一組方法的合集。它包括以下三個方法:

allMatch():只有當流中所有的元素都匹配指定的規則時,才會返回 true。

anyMatch():只要流中存在一個數據滿足指定的規則,就會返回 true。

noneMatch():只有當流中所有的元素都不滿足指定的規則,才會返回 true。

【舉例說明】

/* 流中的元素為:[1, 2, 3, 4, 5] */

/* allMatch */
Stream<Integer> stream = getDataSource();
boolean match = stream.allMatch(x -> x > 0);	// 所有元素都大於0,因此將會返回 true

/* anyMatch */
Stream<Integer> stream = getDataSource();
boolean match = stream.anyMatch(x -> x > 4);	// 存在一個元素都大於4,因此將會返回 true

/* noneMatch */
Stream<Integer> stream = getDataSource();
boolean match = stream.noneMatch(x -> x > 2);	// 存在元素大於2,因此將會返回 false

3.8 Find

同樣,Find 也不是一個單獨的方法,而是一組方法的合集。他包括以下兩個方法:

findFirst():從流中獲取一個元素,一般是獲取開頭元素。

findAny():從流中獲取一個元素,一般也是獲取開頭元素。

【注意事項】

這兩個方法,在絕大多數情況下是完全一致的,但是在多執行緒的環境下,findAnyfindFirst 返回的結果可能不一樣。

【舉例說明】

private static void findUsage(){
    ArrayList<Integer> list = new ArrayList<>();
    Collections.addAll(list, 1, 2, 3, 4, 5);
	
    /* findFirst */
    Integer integer = list.stream().findFirst().get();		// 得到的是第一個元素
    System.out.println("findFirst = " + integer);
	
    /* 序列流 findAny */
    Integer integer1 = list.stream().findAny().get();		// 得到的是第一個元素
    System.out.println("serial findAny = " + integer1);

    /* 並行流 findAny */
    Integer integer2 = list.parallelStream().findAny().get();	// 得到第幾個元素不確定
    System.out.println("parallel findAny = " + integer2);
}

執行結果如下:
在這裡插入圖片描述

3.9 特殊的流

我們上面使用的 Stream 是通用的流,其實 Java 8 還提供了一些特殊的流,包括:

  • IntStream
  • LongStream
  • DoubleStream

從名字可以看出來,這幾個特殊的流是為了針對特定的型別的元素進行操作而誕生的。因此,它們對於自己對應型別的元素可以提供相對於普通流更加便捷、高效、功能更為強大的操作。

關於這幾個特殊流的相關操作,可以參考 jdk1.8 的API。

四、中間操作

4.1 中間操作簡介

中間操作就是資料從資料來源讀取到流中以後,對流中的資料進行的各種各樣的操作。中間操作的每一個操作的返回值都是 Stream 物件,因此中間操作可以是連續操作。

那麼從上面中間操作的定義我們可以看出來,中間操作和最終操作之間的核心區別是:最終操作返回一特定型別的結果,中間操作返回流本身

Stream 流的中間操作主要包括以下幾種:

4.2 filter

filter() 方法的作用是過濾出滿足指定條件的資料,不滿足條件的資料將會被刪除。

【舉例說明】

過濾出年齡大於 22 的學生。

public static void main(String[] args) {
    filterUsage();
}

/**
 * @Description 獲取流
 */
public static Stream<Student> getDataSource(){
    ArrayList<Student> list = new ArrayList<>();
    Collections.addAll(list,
                       new Student("蠻王", 13),
                       new Student("李青", 23),
                       new Student("亞索", 19),
                       new Student("趙信", 28));
    return list.stream();
}

/**
 * @Description 中間操作:filter
 *              可以將流中的滿足指定條件的資料保留,刪除不滿足指定條件的資料
 */
public static void filterUsage(){
    Stream<Student> stream = getDataSource();
    stream.filter(stu -> stu.age > 22)      	// 中間操作:過濾出年齡大於 22 的學生
        .forEach(System.out::println);  		// 最終操作:迭代列印資料
}

執行上面程式碼,結果如下:
在這裡插入圖片描述

可以看到,符合條件(年齡大於 22)的學生都已經被過濾出來並且迭代列印了。

4.3 distinct

distinct() 方法的作用是去除集合中重複的元素。該方法沒有引數,去重的規則與 HashSet 相同。

【注意事項】

HashSet 存入新元素時的去重規則如下圖所示:
在這裡插入圖片描述

  1. 在儲存元素時,首先呼叫 hashCode() 方法獲取雜湊值;
  2. 如果雜湊值不在雜湊表中,就說明沒有重複元素,直接插入;
  3. 如果雜湊值在雜湊表中,再呼叫 equals() 方法比較;
  4. 如果 equals() 比較的結果為一致,說明兩個元素相同,則丟棄元素;
  5. 如果 equals() 比較的結果不一致,說明兩個元素不同,則插入元素到 hashSet 集合中。

【舉例說明】

去除集合中重複的 Student 物件。此處所定義的重複指的是姓名與年齡的值相同的物件。

public class StreamDemo_03 {
    
    public static void main(String[] args) {
        distinctUsage();
    }

    /**
     * @Description 學生類,儲存於集合中的資料型別
     */
    public static class Student{
        private String name;
        private int age;

        // 重寫 equals 方法,IDEA 中右鍵選單可直接生成
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Student student = (Student) o;
            return age == student.age &&
                    Objects.equals(name, student.name);
        }
		// 重寫 hashCode 方法,IDEA 中右鍵選單可直接生成
        @Override
        public int hashCode() {
            return Objects.hash(name, age);
        }
		// getter and setter and toString
        ...
    }

    /**
     * @Description 獲取流
     */
    public static Stream<Student> getDataSource(){
        ArrayList<Student> list = new ArrayList<>();
        Collections.addAll(list,
                new Student("蠻王", 13),
                new Student("李青", 23),
                new Student("亞索", 19),
                new Student("亞索", 19),		// 重複物件
                new Student("趙信", 28));
        return list.stream();
    }

    /**
     * @Description 中間操作:distinct
     *              去除流中重複的資料,去重規則與 HashSet 相同。
     */
    public static void distinctUsage(){
        Stream<Student> stream = getDataSource();
        
        // 注:必須要重寫預設的 equals、hashCode 方法,相同姓名、年齡的物件才會被認為相同
        stream.distinct()						
                .forEach(System.out::println);
    }
}

執行結果如下:
在這裡插入圖片描述
可以看到,重複的物件(姓名為 “亞索”),已經被去除了一個。

4.4 sorted

sorted() 方法的作用是對流中的資料進行排序。sorted() 方法是一個過載方法,所有方法如下:

  • sorted():根據流中的元素對應的類中實現的 Comparable 介面定義的比較規則來排序
  • sorted(Comparator<T> comparator):將流中的資料按照引數介面指定的比較規則來排序

【舉例說明】

將流中的元素按照年齡大小從小到大排序輸出。

  • sorted() 方法

    public class StreamDemo_03 {
    
        public static void main(String[] args) {
            sortedUsage();
        }
    
        /**
         * @Description 學生類,儲存於集合中的資料型別
         */
        public static class Student implements Comparable<Student>{
            private String name;
            private int age;
    	
            // 實現 Comparable 介面方法,定義比較規則
            @Override
            public int compareTo(Student o) {
                return this.age - o.getAge();
            }
            // getter and setter and toString ...
        }
    
        /**
         * @Description 獲取流
         */
        public static Stream<Student> getDataSource(){
            ArrayList<Student> list = new ArrayList<>();
            Collections.addAll(list,
                    new Student("蠻王", 13),
                    new Student("李青", 23),
                    new Student("亞索", 19),
                    new Student("趙信", 28));
            return list.stream();
        }
        
        /**
         * @Description 中間操作:sorted
         */
        public static void sortedUsage(){
            Stream<Student> stream = getDataSource();
            stream.sorted()
                    .forEach(System.out::println);
        }
    }
    

    執行結果如下: 在這裡插入圖片描述
    可以看到,所有的元素都按照年齡從小到大排好序了。

  • sorted(Comparator<T> comparator) 方法

    /* 
    	使用有參的 sorted 方法無需讓 Student 類實現 Comparable 介面。
    	除了 Student 類和 sorted 方法呼叫部分程式碼外,其他程式碼與上面一致。
    */
    public static void sortedUsage(){
        Stream<Student> stream = getDataSource();
        stream.sorted((x,y)->x.getAge()-y.getAge())		// 在引數介面中自定義比較規則
            .forEach(System.out::println);
    }
    

    執行結果與上面無參方法結果一致。

4.5 limit & skip

limit():從流中第 1 個元素開始,擷取指定數量的元素。

skip():從流中第 1 個元素開始,跳過指定數量的元素,擷取剩餘的部分。

【舉例說明】

  • limit() 方法

    打印出年齡最小的兩個學生的資訊。

    public static void limitUsage(){
        Stream<Student> stream = getDataSource();
        stream.sorted((o1,o2)->o1.getAge()-o2.getAge())		// 首先將元素按照年齡從小到大排序
            .limit(2)										// 從第 1 開始,擷取兩個元素
            .forEach(System.out::println);
    }
    

    執行結果如下:
    在這裡插入圖片描述

  • skip() 方法

    打印出年齡第二小和第三小的兩個學生的資訊。

    public static void skipUsage(){
        Stream<Student> stream = getDataSource();
        stream.sorted((o1,o2)->o1.getAge()-o2.getAge())	// 首先將元素按照年齡從小到大排序
            .skip(1)									// 跳過年齡第一小的學生
            .limit(2)									// 擷取年齡第二小、第三小的學生
            .forEach(System.out::println);
    }
    
    

    執行結果如下: 在這裡插入圖片描述

    從上圖可以看到,年齡最小的元素被丟棄了,並且截取出了年齡第二、第三小的元素。

4.6 map & flatMap

map():根據自定義的對映規則,將流中的每個元素替換成新的元素。

flatMap():扁平化元素對映。一般是在 map() 對映完成之後,流中的每個元素可能都是一個容器,而我們需要對容器中的資料進行處理。此時使用扁平化元素對映操作,可以將流中的每個容器中的資料直接讀取到流中。

【舉例說明】

  • map() 方法

    將流中的每個元素替換成元素對應的學生的名字。

    public static void mapusage(){
        Stream<Student> stream = getDataSource();
        stream.map(Student::getName)        // Student::getName <==> o->o.getName()
            .forEach(System.out::println);
    }
    

    執行結果如下: 在這裡插入圖片描述

  • flatMap() 方法

    找出一個字串陣列中出現過的所有字母。

    private static void flatUsage() {
        String[] array = {"hello", "world"};
        Stream<String> stream = Arrays.stream(array);
        stream.map(s->s.split(""))		// 每個字串都對映成陣列:{"h","e","l","l","o"},{"w","o","r","l","d"}
            .flatMap(Arrays::stream)	// 等價 arr->Arrays.stream(arr),將每個字串陣列轉換成 stream 後歸併
            .distinct()					// 去重
            .forEach(System.out::println);
    }
    

    執行結果如下:
    在這裡插入圖片描述

五、Collectors 工具類

5.1 Collectors 工具類簡介

Collectors 是一個工具類,裡面封裝了許多方法。通過使用這些方法可以很方便的獲取到一個 Collector 介面的實現類物件,從而可以是使用 collect() 方法對流中的資料進行各種各樣的處理。

5.2 常用方法

Collectors 工具類封裝的常用方法如表格所示:

方法描述
toList()將流中的資料聚合到一個 List 集合中
toSet()將流中的資料聚合到一個 Set 集合中
toMap()將流中的資料聚合到一個 Map 集合中
maxBy()按照指定的規則,找到流中最大的元素
minBy()按照指定的規則,找到流中最小的元素
joining()將流中的資料拼接成一個字串(只能操作流中元素為 String 的資料)
summingInt()將流中的資料對映成 int 型別的資料,然後求和
averagingInt()將流中的資料對映成 int 型別的資料,然後求均值
summarizingInt()將流中的資料對映成 int 型別的資料,然後獲取描述資訊

5.3 使用示例

joining() 方法為例,演示將多個字串拼接起來。

String[] array = {"are", "you", "ok"};
Stream<String> stream = Arrays.stream(array);
String collect = stream.collect(Collectors.joining(",", "[", "]"));	// 拼接字串,以 , 為分隔,[、] 為前後綴
System.out.println(collect);

執行結果如下:
在這裡插入圖片描述

【參考內容】

千峰大資料——Java8 新特性