初識Java8 Stream、流的基本操作

2021-12-24 碼農沉思錄

本文中的部分示例基於如下場景:餐廳點菜,Dish為餐廳中可提供的菜品,Dish的定義如下:

1public class Dish {
2    
3    private final String name;
4    
5    private final boolean vegetarian;
6    
7    private final int calories;
8    
9    private final Type type;
10
11    public Dish(String name, boolean vegetarian, int calories, Type type) {
12        this.name = name;
13        this.vegetarian = vegetarian;
14        this.calories = calories;
15        this.type = type;
16    }
17
18    public enum Type { MEAT, FISH, OTHER }
19
20    
21}

菜單的數據如下:

1List<Dish> menu = Arrays.asList(
2new Dish("pork", false, 800, Dish.Type.MEAT),
3new Dish("beef", false, 700, Dish.Type.MEAT),
4new Dish("chicken", false, 400, Dish.Type.MEAT),
5new Dish("french fries", true, 530, Dish.Type.OTHER),
6new Dish("rice", true, 350, Dish.Type.OTHER),
7new Dish("season fruit", true, 120, Dish.Type.OTHER),
8new Dish("pizza", true, 550, Dish.Type.OTHER),
9new Dish("prawns", false, 300, Dish.Type.FISH),
10new Dish("salmon", false, 450, Dish.Type.FISH) );

我們以一個簡單的示例來引入流:從菜單列表中,查找出是素食的菜品,並列印其菜品的名稱。

在Java8之前,我們通常是這樣實現該需求的:

1List<String> dishNames = new ArrayList<>();
2for(Dish d menu) {
3    if(d.isVegetarian()) {
4        dishNames.add(d.getName()); 
5    }
6}
7
8for(String n : dishNames) {
9    System.out.println(n);
10}

那在java8中,我們可以這樣寫:

1menu.streams() .filter( Dish::isVegetarian).map( Dish::getName) .forEach( a -> System.out.println(a) );

其運行輸出的結果:


怎麼樣,神奇吧!!!

在解釋上面的代碼之前,我們先對流做一個理論上的介紹。

流是什麼?

流,就是數據流,是元素序列,在Java8中,流的接口定義在 java.util.stream.Stream包中,並且在Collection(集合)接口中新增一個方法:

1default Stream<E> stream() {
2        return StreamSupport.stream(spliterator(), false);
3}

流的簡短定義:從支持數據處理操作的源生成的元素序列。例如集合、數組都是支持數據操作的數據結構(容器),都可以做為流的創建源,該定義的核心要素如下:


流是從一個源創建來而來,而且這個源是支持數據處理的,例如集合、數組等。

元素序列
流代表一個元素序列(流水線),因為是從根據一個數據處理源而創建得來的。

數據處理操作
流的側重點並不在數據存儲,而在於數據處理,例如示例中的filter、map、forEach等。

迭代方式
流的迭代方式為內部迭代,而集合的迭代方式為外部迭代。例如我們遍歷Collection接口需要用戶去做迭代,例如for-each,然後在循環體中寫對應的處理代碼,這叫外部迭代。相反,Stream庫使用內部迭代,我們只需要對流傳入對應的函數即可,表示要做什麼就行。

注意:流和迭代器Iterator一樣,只能遍歷一次,如果要多次遍歷,請創建多個流。

接下來我們將重點先介紹流的常用操作方法。

流的常用操作filter

filter函數的方法聲明如下:

1java.util.stream.Stream#filter
2Stream<T> filter(Predicate<? super T> predicate);

該方法接收一個謂詞,返回一個流,即filter方法接收的lambda表達式需要滿足 (  T  -> Boolean )。

示例:從菜單中選出所有是素食的菜品:

1List<Dish> vegetarianDishs = menu.stream().filter(  Dish::isVegetarian )    
2                                          .collect(toList());              

溫馨提示:流的操作可以分成中間件操作和終端操作。中間操作通常的返回結果還是流,並且在調用終端操作之前,並不會立即調用,等終端方法調用後,中間操作才會真正觸發執行,該示例中的collect方法為終端方法。

我們類比一下資料庫查詢操作,除了基本的篩選動作外,還有去重,分頁等功能,那java8的流API能支持這些操作嗎?
答案當然是肯定。

distinct

distinct,類似於資料庫中的排重函數,就是對結果集去重。
例如有一個數值numArr = [1,5,8,6,5,2,6],現在要輸出該數值中的所有奇數並且不能重複輸出,那該如何實現呢?

1Arrays.stream(numArr).filter(  a -> a % 2 == 0 ).distinict().forEach(System.out::println);

limit

截斷流,返回一個i不超過指定元素個數的流。
還是以上例舉例,如果要輸出的元素是偶數,不能重複輸出,並且只輸出1個元素,那又該如何實現呢?

1Arrays.stream(numArr).filter(  a -> a % 2 == 0 ).distinict().limit(1).forEach(System.out::println);

skip

跳過指定元素,返回剩餘元素的流,與limit互補。

Map

還是類比資料庫操作,我們通常可以只選擇一個表中的某一列,java8流操作也提供了類似的方法。
例如,我們需要從菜單中提取所有菜品的名稱,在java8中我們可以使用如下代碼實現:

1版本1:List<String> dishNames = menu.stream().map( (Dish d) -> d.getName() ).collect(Collectors.toList());
2版本2:List<String> dishNames = menu.stream().map( d -> d.getName() ).collect(Collectors.toList());
3版本3:List<String> dishNames = menu.stream().map(Dish::getName).collect(Collectors.toList());

文章的後續部分儘量使用最簡潔的lambda表達式。

我們來看一下Stream關於map方法的聲明:

1<R> Stream<R> map(Function<? super T, ? extends R> mapper)
2

接受一個函數Function,其函數聲明為:T -> R,接收一個T類型的對象,返回一個R類型的對象。

當然,java為了高效的處理基礎數據類型(避免裝箱、拆箱帶來性能損耗)也定義了如下方法:

1IntStream mapToInt(ToIntFunction<? super T> mapper)
2LongStream mapToLong(ToLongFunction<? super T> mapper)
3DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)

思考題:對於字符數值["Hello","World"] ,輸出字符序列,並且去重。
第一次嘗試:

1public static void test_flat_map() {
2    String[] strArr = new String[] {"hello", "world"};
3    List<String> strList = Arrays.asList(strArr);
4    strList.stream().map( s -> s.split(""))
5                    .distinct().forEach(System.out::println);
6}

輸出結果:


為什麼會返回兩個String[]元素呢?因為map(s -> s.split()) 此時返回的流為Stream,那我們是不是可以繼續對該Steam[String[]],把String[]轉換為字符流,其代碼如下:

1public static void test_flat_map() {
2    String[] strArr = new String[] {"hello", "world"};
3    List<String> strList = Arrays.asList(strArr);
4    strList.stream().map( s -> s.split(""))
5                    .map(Arrays::stream)
6                    .distinct().forEach(System.out::println);
7}

其返回結果:


還是不符合預期,其實原因也很好理解,再次經過map(Arrays:stream)後,返回的結果為Stream,即包含兩個元素,每一個元素為一個字符流,可以通過如下代碼驗證:

1public static void test_flat_map() {
2    String[] strArr = new String[] {"hello", "world"};
3    List<String> strList = Arrays.asList(strArr);
4    strList.stream().map( s -> s.split(""))
5                    .map(Arrays::stream)
6                    .forEach(  (Stream<String> s) -> {
7                        System.out.println("\n --start---");
8                        s.forEach(a -> System.out.print(a + " "));
9                        System.out.println("\n --end---");
10                    } );
11}

綜合上述分析,之所以不符合預期,主要是原數組中的兩個字符,經過map後返回的是兩個獨立的流,那有什麼方法將這兩個流合併成一個流,然後再進行disinic去重呢?

答案當然是可以的,flatMap方法閃亮登場:先看代碼和顯示結果:

1public static void test_flat_map() {
2    String[] strArr = new String[] {"hello", "world"};
3    List<String> strList = Arrays.asList(strArr);
4    strList.stream().map( s -> s.split(""))
5                    .flatMap(Arrays::stream)
6                    .distinct().forEach( a -> System.out.print(a +" "));
7}

其輸出結果:


符合預期。一言以蔽之,flatMap可以把兩個流合併成一個流進行操作。查找和匹配

Stream API提供了allMatch、anyMatch、noneMatch、findFirst和findAny方法來實現對流中數據的匹配與查找。

allMatch

我們先看一下該方法的聲明:

1boolean allMatch(Predicate<? super T> predicate);

接收一個謂詞函數(T->boolean),返回一個boolean值,是一個終端操作,用於判斷流中的所有元素是否與Predicate相匹配,只要其中一個元素不複合,該表達式將返回false。
示例如下:例如存在這樣一個List a,其中元素為 1,2,4,6,8。判斷流中的元素是否都是偶數。

1boolean result = a.stream().allMatch(  a -> a % 2 == 0 );  

anyMatch

該方法的函數聲明如下:

1boolean anyMatch(Predicate<? super T> predicate)
2

同樣接收一個謂詞Predicate( T -> boolean ),表示只要流中的元素至少一個匹配謂詞,即返回真。

示例如下:例如存在這樣一個List a,其中元素為 1,2,4,6,8。判斷流中的元素是否包含偶數。

1boolean result = a.stream().anyMatch(  a -> a % 2 == 0 );  

noneMatch

該方法的函數聲明如下:

1boolean noneMatch(Predicate<? super T> predicate);

同樣接收一個謂詞Predicate( T -> boolean ),表示只要流中的元素全部不匹配謂詞表達式,則返回true。

示例如下:例如存在這樣一個List a,其中元素為 2,4,6,8。判斷流中的所有元素都不式奇數。

1boolean result = a.stream().noneMatch(  a -> a % 2 == 1 );  // 將返回true。

findFirst

查找流中的一個元素,其函數聲明如下:

1Optional<T> findFirst();

返回流中的一個元素。其返回值為Optional,這是jdk8中引入的一個類,俗稱值容器類,其主要左右是用來避免值空指針,一種更加優雅的方式來處理null。該類的具體使用將在下一篇詳細介紹。

1public static void test_find_first(List<Dish> menu) {
2    Optional<Dish> dish = menu.stream().findFirst();
3    
4    dish.ifPresent(a -> System.out.println(a.getName()));  
5}

findAny

返回流中任意一個元素,其函數聲明如下:

1Optional<T> findAny();

reduce

reduce歸約,看過大數據的人用過會非常敏感,目前的java8的流操作是不是有點map-reduce的味道,歸約,就是對流中所有的元素進行統計分析,歸約成一個數值。
首先我們看一下reduce的函數說明:

1T reduce(T identity, BinaryOperator<T> accumulator)

1Optional<T> reduce(BinaryOperator<T> accumulator);

可以理解為沒有初始值的歸約,如果流為空,則會返回空,故其返回值使用了Optional類來優雅處理null值。

1<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

首先,最後的返回值類型為U。

U identity:累積函數的初始值。

BiFunction accumulator:累積器函數,對流中的元素使用該累積器進行歸約,在具體執行時accumulator.apply(  identity,  第二個參數的類型不做限制 ),只要最終返回U即可。

BinaryOperator< U> combiner:組合器。對累積器的結果進行組合,因為歸約reduce,java流計算內部使用了fork-join框架,會對流的中的元素使用並行累積,每個線程處理流中一部分數據,最後對結果進行組合,得出最終的值。

溫馨提示:對流API的學習,一個最最重點的就是要掌握這些函數式編程接口,然後掌握如何使用Lambda表達式進行行為參數化(lambda表達當成參數傳入到函數中)。

接下來我們舉例來展示如何使用reduce。
示例1:對集合中的元素求和

1List<Integer> goodsNumber = Arrays.asList(   3, 5, 8, 4, 2, 13 );
2java7之前的示例:
3int sum = 0;
4for(Integer i : goodsNumber) {
5sum += i;
6}
7System.out.println("sum:" + sum);

求和運算符: c = a + b,也就是接受2個參數,返回一個值,並且這三個值的類型一致。

故我們可以使用T reduce(T identity, BinaryOperator< T> accumulator)來實現我們的需求:

1public static void test_reduce() {
2    List<Integer> goodsNumber = Arrays.asList(   3, 5, 8, 4, 2, 13 );
3    int sum = goodsNumber.stream().reduce(0, (a,b) -> a + b);
4    
5    
6    System.out.println(sum);
7}

不知大家是否只讀(a,b)這兩個參數的來源,其實第一個參數為初始值T identity,第二個參數為流中的元素。

那三個參數的reduce函數主要用在什麼場景下呢?接下來還是用求和的例子來展示其使用場景。在java多線程編程模型中,引入了fork-join框架,就是對一個大的任務進行先拆解,用多線程分別並行執行,最終再兩兩進行合併,得出最終的結果。reduce函數的第三個函數,就是組合這個動作,下面給出並行執行的流式處理示例代碼如下:

1 public static void test_reduce_combiner() {
2
3    
4    List<Integer> nums = new ArrayList<>();
5    int s = 0;
6    for(int i = 0; i < 200; i ++) {
7        nums.add(i);
8        s = s + i;
9    }
10
11    
12    
13    int sum2 = nums.parallelStream().reduce(0,Integer::sum, Integer::sum);
14    System.out.println("和為:" + sum2);
15
16    
17
18    
19    AtomicInteger accumulatorCount = new AtomicInteger(0);
20
21    
22    AtomicInteger combinerCount = new AtomicInteger(0);
23
24    int sum = nums.parallelStream().reduce(0,(a,b) -> {
25                accumulatorCount.incrementAndGet();
26                return a + b;
27           }, (c,d) -> {
28                combinerCount.incrementAndGet();
29                return  c+d;
30        });
31
32    System.out.println("accumulatorCount:" + accumulatorCount.get());
33    System.out.println("combinerCountCount:" + combinerCount.get());
34}

從結果上可以看出,執行了100次累積動作,但只進行了15次合併。

流的基本操作就介紹到這裡,在此總結一下,目前接觸到的流操作:

1、filter

函數功能:過濾

操作類型:中間操作

返回類型:Stream

函數式接口:Predicate

函數描述符:T -> boolean

2、distinct

函數功能:去重

操作類型:中間操作

返回類型:Stream

3、skip

函數功能:跳過n個元素

操作類型:中間操作

返回類型:Stream

接受參數:long

4、limit

函數功能:截斷流,值返回前n個元素的流

操作類型:中間操作

返回類型:Stream

接受參數:long

5、map

函數功能:映射

操作類型:中間操作

返回類型:Stream

函數式接口:Function

函數描述符:T -> R

6、flatMap

函數功能:扁平化流,將多個流合併成一個流

操作類型:中間操作

返回類型:Stream

函數式接口:Function>

函數描述符:T -> Stream

7、sorted

函數功能:排序

操作類型:中間操作

返回類型:Stream

函數式接口:Comparator

函數描述符:(T,T) -> int

8、anyMatch

函數功能:流中任意一個匹配則返回true

操作類型:終端操作

返回類型:boolean

函數式接口:Predicate

函數描述符:T -> boolean

9、allMatch

函數功能:流中全部元素匹配則返回true

操作類型:終端操作

返回類型:boolean

函數式接口:Predicate

函數描述符:T -> boolean

10、 noneMatch

函數功能:流中所有元素都不匹配則返回true

操作類型:終端操作

返回類型:boolean

函數式接口:Predicate

函數描述符:T -> boolean

11、findAny

函數功能:從流中任意返回一個元素

操作類型:終端操作

返回類型:Optional

12、findFirst

函數功能:返回流中第一個元素

操作類型:終端操作

返回類型:Optional

13、forEach

函數功能:遍歷流

操作類型:終端操作

返回類型:void

函數式接口:Consumer

函數描述符:T -> void

14、collect

函數功能:將流進行轉換

操作類型:終端操作

返回類型:R

函數式接口:Collector

15、reduce

函數功能:規約流

操作類型:終端操作

返回類型:Optional

函數式接口:BinaryOperator

函數描述符:(T,T) -> T

16、count

函數功能:返回流中總元素個數

操作類型:終端操作

返回類型:long

由於篇幅的原因,流的基本計算就介紹到這裡了。

相關焦點

  • 【Java】Java8之流Stream
    1 匹配聚合操作 allMatch:接收一個 Predicate 函數,當流中每個元素都符合該斷言時才返回true,否則返回false noneMatch:接收一個 Predicate 函數,當流中每個元素都不符合該斷言時才返回true,否則返回false anyMatch:接收一個 Predicate 函數,只要流中有一個元素滿足該斷言則返回
  • 巧用Java8中的Stream,讓集合操作飛起來!
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫作者:堅持就是勝利juejin.im/post/5d5e2616f265da03b638b28a簡介
  • 7.8 W 字總結!Java 8—Java 10 特性詳解
    對核心類庫的改進主要包括集合類的API和新引入的流Stream。流使程式設計師可以站在更高的抽象層次上對集合進行操作。lambda表達式lambda表達式僅能放入如下代碼: 預定義使用了 @Functional 注釋的函數式接口,自帶一個抽象函數的方法,或者SAM(Single Abstract Method 單個抽象方法)類型。
  • Java 數組轉 List 的三種方式及對比
    ,在List的數據量巨大的情況下,優先使用,可以提高操作速度。流將3種基本類型數組轉為List如果JDK版本在1.8以上,可以使用流stream來將下列3種數組快速轉為List,分別是int[]、long[]、double[],其他數據類型比如short[]、byte[]、char[],在JDK1.8中暫不支持。
  • Java8 快速實現List轉map 、分組、過濾等操作
    ().map(Apple::getMoney).reduce(BigDecimal.ZERO, BigDecimal::add);System.err.println("totalMoney:"+totalMoney);  //totalMoney:17.485、查找流中最大 最小值Collectors.maxBy 和 Collectors.minBy
  • Java8 中用法優雅的 Stream,性能也「優雅」嗎?
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫ava8的Stream API可以極大提高Java程式設計師的生產力,讓程式設計師寫出高效率、乾淨、簡潔的代碼。那麼,Stream API的性能到底如何呢,代碼整潔的背後是否意味著性能的損耗呢?
  • 這些 Java 8 官方挖的坑,你踩過幾個?
    到了Java 8後,Base64編碼已經成為Java類庫的標準,內置了 Base64 編碼的編碼器和解碼器。基本類型是不能泛型化的,也就是說8個基本類型不能作為泛型參數,要想作為泛型參數就必須使用其所對應的包裝類型,那前面的例子傳遞了一個int類型的數組,為何程序沒有報編譯錯誤呢?在Java中,數組是一個對象,它是可以泛型化的,也就是說我們的例子是把一個int類型的數組作為了T的類型,所以在轉換後在List中就只有1個類型為int數組的元素了。
  • java集合【6】——— Iterable接口
    內部定義的方法 java集合最源頭的接口,實現這個接口的作用主要是集合對象可以通過迭代器去遍歷每一個元素。JamException in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909) at java.util.ArrayList
  • 巧用Java8中的Stream,讓集合操作6到飛起!!!
    Stream作為java8的新特性,基於lambda表達式,是對集合對象功能的增強,它專注於對集合對象進行各種高效、便利的聚合操作或者大批量的數據操作,提高了編程效率和代碼可讀性。Stream的原理:將要處理的元素看做一種流,流在管道中傳輸,並且可以在管道的節點上處理,包括過濾篩選、去重、排序、聚合等。元素流在管道中經過中間操作的處理,最後由最終操作得到前面處理的結果。
  • java之轉換流,OutputStreamWriter的簡單介紹
    java之轉換流的原理。這次,小編要介紹的是轉換流中的OutputStreamWriter,與及它的使用,具體如下:java.io.OutputStreamWriter extends WriterOutputStreamWriter:是字符通向字節流的橋梁:可以指定的
  • 內卷300秒,Java 9 到 Java 16 ,精選,新特性!
    JAVA 這幾年的更新實在是太太太……快了,JAVA 8 都還沒用多久,16 都已經了。自從 JAVA 8 了 Lambda 和 Stream 之後,JAVA 就像打了雞血一樣,半年一個版本的,生產隊的驢也沒這麼勤快。
  • 最通俗易懂的 Java 10 新特性講解 | 原力計劃
    10 就是這麼一個小版本,因為 Java 的後續版本基本都會包含之前新特性,所以還是把 Java 10 帶來的改變單獨寫一寫。var hashMap = new HashMap<String, String>();hashMap.put("wechat","wn8398");var string = "hello java 10";var stream = Stream.of(1, 2, 3, 4);var list = new ArrayList<String
  • java程式設計師常用的11個API,趕緊收藏
    此類包含用來操作數組的各種方法,比如排序和搜索等。(T[] array)返回數組的流 Stream,然後我們就可以使用 Stream 相關的許多方法了6.Math 類java.lang.Math 類包含用於執行基本數學運算的方法,如初等指數、對數、平方根和三角函數。
  • Java生成、解析二維碼(附源碼)
    目標:藉助Google提供的ZXing Core工具包,使用Java語言實現二維碼的生成和解析。步BLACK : WHITE); } } return image; } public static void writeToFile(BitMatrix matrix, String format, File file) throws IOException { BufferedImage
  • 面試官問我Java8~14的有哪些重要的新特性,我哭了~~~
    作為 Java8 之後 3 年半才發布的新版本,Java 9 帶 來了很多重大的變化其中最重要的改動是 Java 平臺模塊系統的引入,其他還有諸如集合、Stream 流Java 平臺模塊系統Java 平臺模塊系統,也就是 Project Jigsaw,把模塊化開發實踐引入到了 Java 平臺中。
  • Java基礎面試題簡單總結
    36、String是最基本的數據類型嗎答:基本數據類型包括byte、int、char、long、float、double、boolean和short。java.lang.String類是final類型的,因此不可以繼承這個類、不能修改這個類。
  • Java 中的十個 「 單行代碼編程 」 ( One Liner )
    翻譯 颯然Hang 連結 github.com/aruld/java-oneliners/wiki本文列舉了十個使用一行代碼即可獨立完成(不依賴其他代碼)的業務邏輯,主要依賴的是Java8中的Lambda和Stream等新特性以及try-with-resources、JAXB等。
  • Kotlin VS Java:基本語法差異
    ,歡迎加入Java和Android架構社區Android開發規範和架構總結5月18號,goole宣布Kotlin成為官方支持的開發語言以來,Kotlin語言社區,公眾號,qq群等全面轟炸,本文是一篇譯文,來自國外的一個用戶,將給大家介紹,基礎語法部分Kotlin和java
  • Java程式設計師必會的工具庫,讓你的代碼量減少90%!
    # Java自帶工具方法1.1 List集合拼接成以逗號分隔的字符串// 如何把list集合拼接成以逗號分隔的字符串 a,b,c List<String> list = Arrays.asList("a", "b", "c"); // 第一種方法,可以用stream流 String join = list.stream