Java8 Stream 入門看這篇就夠了

初識 Stream

Java 8 API添加了一個新的抽象稱為流 Stream忆首,可以讓你以一種聲明的方式處理數(shù)據(jù)盟蚣。使用一種類似用 SQL 語句從數(shù)據(jù)庫查詢數(shù)據(jù)的直觀方式來提供一種對 Java 集合運算和表達的高階抽象。

Stream API可以極大提高Java程序員的生產(chǎn)力笤虫,讓程序員寫出高效率、干凈、簡潔的代碼见坑。這種風格將要處理的元素集合看作一種流捏检, 流在管道中傳輸荞驴, 并且可以在管道的節(jié)點上進行處理熊楼, 比如篩選, 排序能犯,聚合等鲫骗。元素流在管道中經(jīng)過中間操作(intermediate operation)的處理,最后由最終操作(terminal operation)得到前面處理的結(jié)果枕磁。

圖上的流程轉(zhuǎn)換為 Java 代碼為:

List<Integer> transactionsIds = 
widgets.stream()  // Source
             .filter(b -> b.getColor() == RED)  // Operations-filter
             .sorted((x,y) -> x.getWeight() - y.getWeight())   // Operations-sorted
             .mapToInt(Widget::getWeight)   // Operations-map
             .sum();  // Results-sum

什么是 Stream?

Stream(流)是一個來自數(shù)據(jù)源的元素隊列并支持聚合操作

  • 無存儲:不是一種數(shù)據(jù)結(jié)構(gòu)术吝,它只是某種數(shù)據(jù)源的一個視圖计济,數(shù)據(jù)源可以是一個數(shù)組排苍,Java容器或I/O channel等。
  • 為函數(shù)式編程而生:對 stream 的修改都不會修改其數(shù)據(jù)源传藏,比如對 stream 執(zhí)行過濾操作并不會刪除被過濾的元素幔翰,而是會產(chǎn)生一個不包含被過濾元素的新 stream。 (peek 對可變對象可以修改)
  • 惰式執(zhí)行:stream 上的操作并不會立即執(zhí)行叫惊,只有等到用戶真正需要結(jié)果的時候才會執(zhí)行做修。
  • 可消費性:stream 只能被“消費”一次,一旦遍歷過就會失效蔗坯,就像容器的迭代器那樣燎含,想要再次遍歷必須重新生成。

Stream操作還有兩個基礎的特征:

  • Pipelining: 中間操作都會返回流對象本身绘梦。 這樣多個操作可以串聯(lián)成一個管道卸奉, 如同流式風格(fluent style)颖御。 這樣做可以對操作進行優(yōu)化, 比如延遲執(zhí)行(laziness)和短路( short-circuiting)疹鳄。
  • 內(nèi)部迭代: 以前對集合遍歷都是通過Iterator或者For-Each的方式, 顯式的在集合外部進行迭代, 這叫做外部迭代辑鲤。 Stream提供了內(nèi)部迭代的方式杠茬, 通過訪問者模式(Visitor)實現(xiàn)。

關(guān)于 Stream 的相關(guān)API實現(xiàn)原理, 感興趣的可以去看下: 淺析 Java8 Stream 原理

創(chuàng)建流

stream 常見的創(chuàng)建的方式主要有Arrays宁赤、Collection栓票、Stream 靜態(tài)方法等,這里代碼列舉其中最常見的幾種:

package com.company.designModel;


import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;

public class Java8StreamTest {

    public static void main(String[] args) {
        // 1.數(shù)組創(chuàng)建流
        Integer[] arrays = {9, 5, 2, 7};
        Stream<Integer> arraysStream = Arrays.stream(arrays);

        // 2.Collection 創(chuàng)建流
        ArrayList<Object> list = new ArrayList<>();
        list.add("Amy");
        list.add("Angst");
        Stream<Object> stream = list.stream();
        Stream<Object> parallelStream = list.parallelStream();  // 這里 parallelStream 創(chuàng)建的是一個并行流

        // 3. Stream.of
        Stream.of(1, 24, 8, 6, 10, 4, 8, 3, 2, 8, 6).skip(2).limit(5).forEach(System.out::println);
        // empty() 可以創(chuàng)建一個空的流
        Stream<Object> emptyStream = Stream.empty();

        // 4.Stream.generate()
        Stream.generate(Math::random).limit(10).skip(1).forEach(System.out::print);

        // 5.Stream.iterate()
        Stream.iterate(0, item -> ++ item).limit(10).forEach(System.out::print);

        // 6.Stream.builder()
        Stream<Object> buildStream = Stream.builder().add("Amy").add("Angst").build();

        // 7. Stream.concat() 合并創(chuàng)建一個新的流
        Stream<Object> concatStream = Stream.concat(stream, parallelStream);
    }
}

這里有幾個需要關(guān)注的點:

  • 查看 Stream 源碼的話,你會發(fā)現(xiàn) of() 方法內(nèi)部其實調(diào)用了 Arrays.stream() 方法继找。源碼注釋有提到 “從數(shù)組創(chuàng)建的流是安全的”
    /**
     * Returns a sequential ordered stream whose elements are the specified values.
     *
     * @param <T> the type of stream elements
     * @param values the elements of the new stream
     * @return the new stream
     */
    @SafeVarargs
    @SuppressWarnings("varargs")   // Creating a stream from an array is safe
    public static<T> Stream<T> of(T... values) {
        return Arrays.stream(values);
    }
  • Stream.generate(): 生成一個無限長度的Stream逃沿,其元素的生成是通過給定的Supplier(這個接口可以看成一個對象的工廠,每次調(diào)用返回一個給定類型的對象), 其中值是隨機的边臼。這個無限長度 Stream 是懶加載假消,一般都會配合Stream的limit()方法來做分頁使用富拗。
  • Stream.iterate(): 也是生成無限長度的Stream,和 generator 不同的是,其元素的生成是重復對給定的種子值(seed)調(diào)用用戶指定函數(shù)來生成的. 其中包含的元素可以認為是:seed葵擎,f(seed), f(f(seed))無限循環(huán)。

操作流

流的操作類型分為兩種:

  • 中間操作(Intermediate):一個流可以后面跟隨零個或多個intermediate操作签餐。其目的主要是打開流氯檐,做出某種程度的數(shù)據(jù)映射/過濾,然后返回一個新的流糯崎,交給下一個操作使用(鏈式操作)河泳。這類操作都是惰性化的(lazy),就是說薄霜,僅僅調(diào)用到這類方法纸兔,并沒有真正開始流的遍歷。

    • 有狀態(tài)(Stateless):指該操作只有拿到所有元素之后才能繼續(xù)下去
    • 無狀態(tài)(Stateful):指元素的處理不受之前元素的影響
  • 終止操作(Terminal):一個流只能有一個 terminal 操作崎坊,當這個操作執(zhí)行后负甸,流就被使用“光”了呻待,無法再被操作。所以,這必定是流的最后一個操作奏篙。Terminal 操作的執(zhí)行迫淹,才會真正開始流的遍歷,并且會生成一個結(jié)果肺稀,或者一個 side effect应民。

    • 非短路操作: 指必須處理所有元素才能得到最終結(jié)果
    • 短路操作: 指遇到某些符合條件的元素就可以得到最終結(jié)果,如 A || B繁仁,只要A為true黄虱,則無需判斷B的結(jié)果

代碼演示

Intermediate方法
  • filter(): 用于篩選數(shù)據(jù),返回由匹配的元素組成的流晤揣。
  • limit(): 切片限流可以當做分頁來使用默勾,返回被截斷的流母剥。
  • skip():要跳過前面元素n的數(shù)量,返回由該流的剩余元素組成的流习霹。
  • distinct():去重操作炫隶,返回由不同元素組成的流。
public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<>();
    list.add("Vivian");
    list.add("Amy");
    list.add("Amy");
    list.add("Angst");
    list.add("Zx");

    list.stream().  // 創(chuàng)建一個list流
            distinct().  // 去重操作
            skip(1).  // 跳過第一個元素
            filter(o -> o.startsWith("A")).  // 過濾以A開頭的元素
            limit(2).  // 切片選擇保留幾個
            forEach(System.out::println);  // 輸出結(jié)果

}

結(jié)果輸出:

Amy
Angst

Process finished with exit code 0
  • map():接收一個函數(shù)作為參數(shù)煞檩,該函數(shù)會被應用到每個元素上斟湃,并將其映射成一個新的元素檐薯。
  • flatMap():接收一個函數(shù)作為參數(shù),將流中的每個值都換成另一個流墓猎,然后把所有流連接成一個流赚楚。
public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<>();
    list.add("Vivian");
    list.add("Amy");
    list.add("Angst");

    // 1.map
    list.stream().map(String::toLowerCase).forEach(System.out::println);
    list.stream().mapToInt(String::length).forEach(System.out::println);
    list.stream().mapToDouble(String::length).forEach(System.out::println);
    list.stream().mapToLong(String::length).forEach(System.out::println);

    // 2. flatMap 同 map 作用類似宠页,區(qū)別就是將每個元素重新組成Stream膨俐,并將這些Stream 串行合并成一條Stream.
    list.stream().flatMap(o-> Stream.of(o.toLowerCase())).forEach(System.out::println);
    list.stream().flatMapToInt(o-> IntStream.of(o.length())).forEach(System.out::println);
    list.stream().flatMapToDouble(o-> DoubleStream.of(o.length())).forEach(System.out::println);
    list.stream().flatMapToLong(o-> LongStream.of(o.length())).forEach(System.out::println);

}

結(jié)果輸出:

vivian
amy
angst
6
3
5
6.0
3.0
5.0
6
3
5

vivian
amy
angst
6
3
5
6.0
3.0
5.0
6
3
5

Process finished with exit code 0
  • sorted():自然排序,流中元素需實現(xiàn)Comparable接口, 也可以自定義 Comparator
public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<>();
    list.add("Vivian");
    list.add("Amy");
    list.add("Angst");

    list.stream().sorted().forEach(System.out::println);  // 默認排序
    list.stream().sorted((x, y) -> y.length() - x.length()).forEach(System.out::println);  // 自定義排序
    
}

結(jié)果輸出:
Amy
Angst
Vivian

Vivian
Angst
Amy

Process finished with exit code 0
  • peek():如同于map()门烂,能得到流中的每一個元素兄淫。但map接收的是一個 Function 表達式,有返回值慨丐。而peek接收的是Consumer泄私,沒有返回值晌端。
public class Java8StreamTestDemo {

    public static void main(String[] args) {
        // 1.不可變對象
        ArrayList<String> arrayList = new ArrayList<>();
        arrayList.add("Vivian");
        arrayList.add("Amy");
        arrayList.add("Angst");

        // 修改元素首字母小寫,實際結(jié)果并沒有修改成功
        arrayList.stream().peek(String::toLowerCase).forEach(o -> System.out.println("toLowerCase:" + o));  


        // 2.可變對象
        ArrayList<Food> list = new ArrayList<>();
        list.add(new Food("果汁"));
        list.add(new Food("奶茶"));
        list.add(new Food("牛奶"));

        // peek()方法存在的主要目的是用調(diào)試蓬痒,通過 peek() 方法可以看到流中的數(shù)據(jù)經(jīng)過每個處理點時的狀態(tài)漆羔。
        list.stream().peek(o -> System.out.println("debugger: "+ o.getCoca())).count();

        // 除去用于調(diào)試演痒,peek()在需要修改元素內(nèi)部狀態(tài)的場景也非常有用,比如修改 Coca 的值 瓶埋,當然也可以使用map()和flatMap實現(xiàn).
        list.stream().peek(o->o.setCoca("飯前:"+ o.getCoca())).forEach(System.out::println);


    }
    
    // 定義一個可變對象
    static class Food {
        private String coca="飲料";
        public Food(String coca) { this.coca = coca; }
        public String getCoca() { return coca; }
        public void setCoca(String coca) { this.coca = coca; }

        @Override
        public String toString() {
            return "Food{" +
                    "coca='" + coca + '\'' +
                    '}';
        }
    }

}
結(jié)果輸出:

toLowerCase:Vivian
toLowerCase:Amy
toLowerCase:Angst
debugger: 果汁
debugger: 奶茶
debugger: 牛奶
Food{coca='飯前:果汁'}
Food{coca='飯前:奶茶'}
Food{coca='飯前:牛奶'}

Process finished with exit code 0

注意:map函數(shù)對Stream中元素執(zhí)行的是映射操作养筒,會以新的元素(map的結(jié)果)填充新的Stream端姚。嚴格的講map不是修改原來的元素。peek只能消費Stream中的元素巫湘,是否可以更該Stream中的元素,取決于Stream中的元素是否是不可變對象诀诊。如果是不可變對象阅嘶,則不可修改Stream中的元素;如果是可變對象抡蛙,則可以修改對象的值魂迄,但是無法修改對象的引用捣炬。


Terminal方法
  • max(): 返回此流的最大元素
  • min(): 返回此流的最小元素
  • count(): 返回此流中的元素計數(shù)
  • findFirst(): 返回此流中第一個元素
  • findAny(): 取任意一個元素,正常情況下一般會取第一個元素浴捆,在并行流的情況下會隨機取一個元素
public static void main(String[] args) {
    ArrayList<String> arrayList = new ArrayList<>();
    arrayList.add("Vivian");
    arrayList.add("Amy");
    arrayList.add("Angst");

    System.out.println(arrayList.stream().map(String::length).max(Integer::compareTo));
    System.out.println(arrayList.stream().map(String::length).min(Integer::compareTo));
    System.out.println(arrayList.stream().map(String::length).count());

    System.out.println(arrayList.stream().map(String::length).findFirst());
    // 在 parallel 流中存在每次輸出的結(jié)果不一致
    System.out.println(arrayList.stream().parallel().map(String::length).findAny());
}

結(jié)果輸出:

Optional[6]
Optional[3]
3

Optional[6]
Optional[3]

Process finished with exit code 0
  • andMatch(): 返回流中的元素是否與所提供的匹配
  • allMatch(): 返回流的所有元素是否與指定的條件匹配选泻。
  • noneMatch(): 返回流中是否沒有匹配謂詞的元素
public static void main(String[] args) {
    ArrayList<String> arrayList = new ArrayList<>();
    arrayList.add("Vivian");
    arrayList.add("Amy");
    arrayList.add("Angst");

    // 匹配成功返回 true, 否則 false
    System.out.println(arrayList.stream().noneMatch(o->o.equals("amy")));  // 空匹配
    System.out.println(arrayList.stream().anyMatch(o->o.equals("Amy")));  // 任意匹配
    System.out.println(arrayList.stream().allMatch(o->o.equals("Amy")));  // 全匹配
}

結(jié)果輸出:

true
true
false

Process finished with exit code 0
  • reduce(): 用于對 stream 中元素進行聚合求值
// 規(guī)約操作
System.out.println(Stream.of(1, 9, 5, 2, 7, -1).reduce(0, Integer::sum));


23

Process finished with exit code 0
  • forEach(): 遍歷元素
  • iterator(): 返回一個迭代器
public static void main(String[] args) {
    ArrayList<String> arrayList = new ArrayList<>();
    arrayList.add("Vivian");
    arrayList.add("Amy");
    arrayList.add("Angst");

    // 遍歷元素
    arrayList.stream().forEach(System.out::println);
    
    // 返回的是一個可迭代對象
    System.out.println(arrayList.stream().iterator());
}

結(jié)果輸出:


Vivian
Amy
Angst

java.util.Spliterators$1Adapter@7ba4f24f

Process finished with exit code 0
  • toArray(): 返回一個數(shù)組
  • collect(): 收集操作页眯,將一個對象的集合轉(zhuǎn)化成另一個對象的集合
// 把流轉(zhuǎn)換成數(shù)組
Object[] toArray = Stream.of(1, 9, 5, 2, 7, -1).toArray();

// 使用系統(tǒng)提供的收集器可以將最終的數(shù)據(jù)流收集到 List Set Map等容器中
List<Integer> list = Stream.of(1, 9, 5, 2, 7, -1).collect(Collectors.toList());

最后

以上就是本次的學習分享窝撵,Stream 的熟練使用可以讓你的代碼更加簡潔易于維護襟铭,以上案例中可能還涉及其他的知識點,比如函數(shù)式接口赐劣、lambda等哩都,感興趣的可以看下筆者其他的學習筆記。歡迎留言討論咐汞。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末化撕,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子锌仅,更是在濱河造成了極大的恐慌墙贱,老刑警劉巖贱傀,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件府寒,死亡現(xiàn)場離奇詭異,居然都是意外死亡剖淀,警方通過查閱死者的電腦和手機纤房,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進店門炮姨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人绅作,你說我怎么就攤上這事蛾派『檎В” “怎么了?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵役拴,是天一觀的道長钾埂。 經(jīng)常有香客問我科平,道長瞪慧,這世上最難降的妖魔是什么部念? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任儡炼,我火速辦了婚禮,結(jié)果婚禮上榜贴,老公的妹妹穿的比我還像新娘妹田。我一直安慰自己,他們只是感情好驶拱,可當我...
    茶點故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布蓝纲。 她就那樣靜靜地躺著房铭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪翁狐。 梳的紋絲不亂的頭發(fā)上凌蔬,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天砂心,我揣著相機與錄音,去河邊找鬼坎弯。 笑死,一個胖子當著我的面吹牛撩炊,可吹牛的內(nèi)容都是我干的崎脉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼灶体!你這毒婦竟也來了蝎抽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎狭吼,沒想到半個月后殖妇,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡疲吸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年摘悴,在試婚紗的時候發(fā)現(xiàn)自己被綠了舰绘。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片捂寿。...
    茶點故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蔓彩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情旷赖,我是刑警寧澤探膊,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布逞壁,位于F島的核電站,受9級特大地震影響绳瘟,放射性物質(zhì)發(fā)生泄漏姿骏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望嘲玫。 院中可真熱鬧,春花似錦抡诞、人聲如沸土陪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蹋肮。三九已至璧疗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間漆魔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留阿纤,地道東北人。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像藐窄,于是被迫代替她去往敵國和親荆忍。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 一叽唱、Stream流的特性 Stream流不是一種數(shù)據(jù)結(jié)構(gòu)尔觉,不保存數(shù)據(jù)芥吟,它只是在原數(shù)據(jù)集上定義了一組操作专甩。 這些操作...
    超天大圣JR閱讀 365評論 0 0
  • 什么是Stream? Stream 不是集合元素涤躲,它不是數(shù)據(jù)結(jié)構(gòu)并不保存數(shù)據(jù),它是有關(guān)算法和計算的种樱,它更像一個高級...
    盼旺閱讀 190評論 0 3
  • 1. 新時代的Java語言 新時代的Java是以JDK8的發(fā)布為分水嶺嫩挤,從JDK8開始岂昭,Java開發(fā)進入一個新的階...
    Mr_Chao3閱讀 611評論 0 1
  • Java8之Stream流(一)基礎體驗 Java8之Stream流(二)關(guān)鍵知識點 Java8之Stream...
    揭光智閱讀 6,961評論 1 8
  • 流簡介 1.流是什么? 流到底是什么呢佣赖?簡短的定義就是“ 從支持數(shù)據(jù)處理操作的源生成的元素序列 ” 元素序列:就像...
    嘀嘀Lemon閱讀 1,103評論 0 1