Java8中的Stream流式操作 - 入門篇

語言: CN / TW / HK

作者:湯圓

個人部落格:javalover.cc

前言

之前總是朋友朋友的叫,感覺有套近乎的嫌疑,所以後面還是給大家改個稱呼吧

因為大家是來看東西的,所以暫且叫做官人吧(靈感來自於民間流傳的四大名著之一《金瓶梅》)

官人們好啊,我是湯圓,今天給大家帶來的是《Java8中的Stream流式操作 - 入門篇》,希望有所幫助,謝謝

文章純屬原創,個人總結難免有差錯,如果有,麻煩在評論區回覆或後臺私信,謝啦

簡介

流式操作也叫做函式式操作,是Java8新出的功能

流式操作主要用來處理資料(比如集合),就像泛型也大多用在集合中一樣(看來集合這個小東西還是很關鍵的啊,哪哪都有它)

下面我們主要用例子來介紹下,流的基操(建議先看下lambda表示式篇,裡面介紹的lambda表示式函式式介面方法引用等,下面會用到)

先來看下目錄

目錄

  1. 流是什麼

  2. 老闆,上栗子

  3. 流的操作步驟

  4. 流的特點

  5. 流式操作和集合操作的區別

正文

1. 流是什麼

流是一種以宣告性的方式來處理資料的API

什麼是宣告性的方式?

就是隻宣告,不實現,類似抽象方法(多型性)

2. 老闆,上栗子

舉個栗子

下面我們舉個栗子,來看下什麼是流式操作,然後針對這個栗子,引出後面的相關概念

需求篩選年齡大於1的貓(貓的1年≈人的5年),並按年齡遞增排序,最後提取名字單獨存放到列表中


public class BasicDemo {
    public static void main(String[] args) {
      // 以下貓的名字均為真名,非虛構
        List<Cat> list = Arrays.asList(new Cat(1, "tangyuan"), new Cat(3, "dangdang"), new Cat(2, "milu"));
        // === 舊程式碼 Java8之前 ===
        List<Cat> listTemp = new ArrayList<>();
        // 1. 篩選
        for(Cat cat: list){
            if(cat.getAge()>1){
                listTemp.add(cat);
            }
        }
        // 2. 排序
        listTemp.sort(new Comparator<Cat>() {
            @Override
            public int compare(Cat o1, Cat o2) {
                // 遞增排序
                return Integer.compare(o1.getAge(), o2.getAge());
            }
        });
        // 3. 提取名字
        List<String> listName = new ArrayList<>();
        for(Cat cat: listTemp){
            listName.add(cat.getName());
        }
        System.out.println(listName);
        
        // === 新程式碼 Java8之後 ===
        List<String> listNameNew = list.stream()
          			// 函式式介面 Predicate的 boolean test(T t)抽象方法
                .filter(cat -> cat.getAge() > 1)
								// lambda表示式的方法引用
			          .sorted(Comparator.comparingInt(Cat::getAge))
          			// 函式式介面 Funtion的 R apply(T t)抽象方法
                .map(cat-> cat.getName())
             	  // 收集資料,把流轉為集合List
                .collect(Collectors.toList());
        System.out.println(listNameNew);
    }
}
class Cat{
    int age;
    String name;

    public Cat(int age, String name) {
        this.age = age;
        this.name = name;
    }
	// 省略getter/setter
}

可以看到,用了流式操作,程式碼簡潔了很多(秒哇)

Q:有的官人可能會想,這跟前面lambda表示式的組合操作有點像啊。

A:你說對了,確實只是像,區別還是很大的。因為lambda表示式的組合操作其實還是屬於直接針對集合的操作;

文末會講到直接操作集合和流式操作的區別,這裡先跳過

下面我們基於這個栗子,來分別介紹涉及到的知識點

3. 流的操作步驟

我們先忽略舊版的集合操作(後面介紹流和集合的區別時再說),先來介紹流的操作(畢竟流才是今天的主角嘛)

主角

流的操作分三步走:建立流、中間操作、終端操作

流程如下圖:

流的操作流程

這裡我們要關注一個很重要的點:

在終端操作開始之前,中間操作不會執行任何處理,它只是宣告執行什麼操作;

你可以想象上面這個流程是一個流水線:我們這裡做個簡化處理

  1. 目的:先告訴你,我們要加工瓶裝的水(先建立流,告訴你要處理哪些資料)
  2. 再針對這些瓶子和水,來搭建一個流水線:固定瓶子的夾具、裝水的水管、擰蓋子的爪子、裝箱的打包器(中間操作,宣告要執行的操作)
  3. 最後按下啟動按鈕,流水線開始工作(終端操作,開始根據中間操作來處理資料)

流水線

因為每一箇中間操作都是返回一個流(Stream),這樣他們就可以一直組合下去(我好像吃到了什麼東西?),但是他們的組合順序是不固定的,流會根據系統性能去選擇合適的組合順序

我們可以列印一些東西來看下:

List<Cat> list = Arrays.asList(new Cat(1, "tangyuan"), new Cat(3, "dangdang"), new Cat(2, "milu"));
List<String> listNameNew = list.stream()
  .filter(cat -> {
    System.out.println("filter: " + cat);
    return cat.getAge() > 1;
  })
  .map(cat-> {
    System.out.println("map:" + cat);
    return cat.getName();
  })
  .collect(Collectors.toList());

輸出如下:

filter: Cat{age=1}
filter: Cat{age=3}
map:Cat{age=3}
filter: Cat{age=2}
map:Cat{age=2}

可以看到,中間操作的filter和map組合到一起交叉執行了,儘管他們是兩個獨立的操作(這個技術叫作迴圈合併

這個合併主要是由流式操作根據系統的效能來自行決定的

既然講到了迴圈合併,那下面捎帶說下短路技巧

短路這個詞大家應該比較熟悉(比如腦子短路什麼的),指的是本來A->B->C是都要執行的,但是在B的地方短路了,所以就變成了A->C了

這裡的短路指的是中間操作,由於某些原因(比如下面的limit),導致只執行了部分,沒有全部去執行

我們來修改下上面的例子(加了一箇中間操作limit):

List<Cat> list = Arrays.asList(new Cat(1, "tangyuan"), new Cat(3, "dangdang"), new Cat(2, "milu"));
List<String> listNameNew = list.stream()
  .filter(cat -> {
    System.out.println("filter: " + cat);
    return cat.getAge() > 1;
  })
  .map(cat-> {
    System.out.println("map:" + cat);
    return cat.getName();
  })
  // 只加了這一行
  .limit(1)
  .collect(Collectors.toList());

輸出如下:

filter: Cat{age=1}
filter: Cat{age=3}
map:Cat{age=3}

可以看到,因為limit(1)只需要一個元素,所以filter過濾時,只要找到一個滿足條件的,就會停止過濾操作(後面的元素就放棄了),這個技巧叫做短路技巧

這個就很大程度上體現了中間操作的組合順序帶來的優點:需要多少,處理多少,即按需處理

4. 流的特點

特點有三個:

  • 宣告性:簡潔,易讀,程式碼行數大大減少(每天有程式碼行數要求的公司除外)
  • 可複合:更靈活,各種組合都可以(只要你想,只要流有)
  • 可並行:效能更好(不用我們去寫多執行緒,多好)

5. 流式操作和集合操作的區別:

現在我們再來回顧下開頭例子中的集合操作:篩選->排序->提取

List<Cat> listTemp = new ArrayList<>();
// 1. 篩選
for(Cat cat: list){
  if(cat.getAge()>1){
    listTemp.add(cat);
  }
}
// 2. 排序
listTemp.sort(new Comparator<Cat>() {
  @Override
  public int compare(Cat o1, Cat o2) {
    // 遞增排序
    return Integer.compare(o1.getAge(), o2.getAge());
		/**
    * Q:為啥不用減法 return o1.getAge() - o2.getAge()?
    * A:因為減法會有資料溢位的風險
    * 	 如果o1.getAge()為20億,o2.getAge()為-2億,那麼結果就會超過int的極限21億多
    **/ 
  }
});
// 3. 提取名字
List<String> listName = new ArrayList<>();
for(Cat cat: listTemp){
  listName.add(cat.getName());
}
System.out.println(listName);

可以看到跟流式操作不一樣的有兩點:

  1. 集合操作中有一個listTemp臨時變數(流式操作沒),
  2. 集合操作一直都在處理資料(而流式操作是直到最後一步的終端操作才會去處理資料),依次篩選->排序->提取名字,是順序執行的

下面我們用表格來列出區別,應該會直觀點

流式操作 集合操作
功能 處理資料為主 儲存資料為主
迭代方式 內部迭代(只迭代一次),只需宣告,不需要實現,流內部自己有實現) 外部迭代(可一直迭代)需要自己foreach
處理資料 直到終端操作,才會開始真正處理資料(按需處理) 一直都在處理資料(全部處理)

用生活中的例子來對比的話,可以用電影來比喻

流就好比線上觀看,集合就好本地觀看(下載到本地)

總結

  1. 流是什麼:
    • 流是一種以宣告性的方式來處理資料的API
    • 流是從支援資料處理操作生成的元素序列
      • 源:資料的來源,比如集合,檔案等(本節只介紹了集合的流式操作,因為用的比較多;後面有空再介紹其他的)
      • 資料處理操作:就是流的中間操作,比如filter, map
      • 元素序列:通過流的終端操作,返回的結果集
  2. 流的操作流程:
    • 建立流 -> 中間操作 -> 終端操作
    • 中間操作只是宣告,不真實處理資料,直到終端操作開始才會執行
  3. 迴圈合併:中間操作會自由組合(流根據系統自己來決定組合的順序)
  4. 短路技巧:如果中間操作處理的資料已經達到需求,則會立即停止處理資料(比如limit(1),則當處理完1個就會停止處理)
  5. 流式操作和集合操作的區別:
    • 按需處理,集合全處理
    • 流主攻資料處理,集合主攻資料儲存
    • 簡潔,集合
    • 內部迭代(只迭代一次,完後流會消失),集合外部迭代(可一直迭代)

後記

最後,感謝大家的觀看,謝謝

原創不易,期待官人們的三連喲

分享到: