Java 8 Stream 知識(shí)分享

本文原文

綜述

Java 8 新增了 Stream API割以。Stream API有點(diǎn)類似使用SQL 語(yǔ)句近她,可以將集合中的元素進(jìn)行過(guò)濾。使用時(shí)戚嗅,類似于從一個(gè)管道中抽取元素霸株,并對(duì)他們進(jìn)行操作相味。使用流的一個(gè)優(yōu)點(diǎn)是秽梅,他可以使得我們的程序更小,并且更容易理解拯辙。

Stream API相關(guān)的接口有Stream郭变、IntStream, LongStream, DoubleStream(因?yàn)?Java 的泛型不支持基本數(shù)據(jù)類型,而又因頻繁的裝箱涯保、拆箱存在效率問(wèn)題诉濒,故額外有后三者)。

使用Stream操作時(shí)夕春,我們通常使用鏈?zhǔn)讲僮?/strong>未荒,即將多條代碼合并成一條代碼(事例將在使用Supplier創(chuàng)建中給出)。

Java Collection 體系數(shù)據(jù)處理的演進(jìn)

本小節(jié)用于測(cè)試的代碼如下:

public record User(Integer id, String name, Integer money) { }

final var users = Arrays.asList(
      new User(1, "張三", 200),
      new User(2, "李四", 200),
      new User(3, "王五", 10000),
      new User(4, "趙六", 20000),
      new User(5, "王強(qiáng)", 80000)
);

通過(guò)不同方法來(lái)過(guò)濾不同數(shù)據(jù)

我們過(guò)濾數(shù)據(jù)首先想到的方法是針對(duì)各個(gè)需求來(lái)定義一個(gè)個(gè)的方法及志。

例如片排,產(chǎn)品經(jīng)理給了你一個(gè)篩選出所有 id 大于 3 用戶的需求寨腔,可以定義如下getIdGreaterThan3的方法。

public class CollectionStream {
    public static void main(String[] args) {
        // users 定義
        final var newUsers = getIdGreaterThan3(users);
        for (User user : newUsers) {
            System.out.println(user);
        }
    }
    public static List<User> getIdGreaterThan3(List<User> users) {
        final var newUsers = new ArrayList<User>();
        for (User user : users) {
            if (user.id() > 3) {
                newUsers.add(user);
            }
        }
        return newUsers;
    }
}

/*
  輸出:
  User[id=4, name=趙六, money=20000]
  User[id=5, name=王強(qiáng), money=80000]
*/

第二天率寡,產(chǎn)品經(jīng)理要求你篩選出所有姓“王”的用戶的需求迫卢,定義getAllWang方法:

public class CollectionStream {
    public static void main(String[] args) {
        // users 定義
        final var newUsers = getAllWang(users);
        for (User user : newUsers) {
            System.out.println(user);
        }
    }
    public static List<User> getAllWang(List<User> users) {
        final var newUsers = new ArrayList<User>();
        for (User user : users) {
            if (user.name().startsWith("王")) {
                newUsers.add(user);
            }
        }
        return newUsers;
    }
}

/*
  輸出:
  User[id=3, name=王五, money=10000]
  User[id=5, name=王強(qiáng), money=80000]
*/

第三天,產(chǎn)品經(jīng)理要求你開(kāi)發(fā)所有錢大于 10000 的用戶的需求冶共,你瞅了瞅他乾蛤,寫(xiě)出了如下代碼:

public class CollectionStream {
    public static void main(String[] args) {
        // users 定義
        final var newUsers = getRichPeople(users);
        for (User user : newUsers) {
            System.out.println(user);
        }
    }
    public static List<User> getRichPeople(List<User> users) {
        final var newUsers = new ArrayList<User>();
        for (User user : users) {
            if (user.money() > 10000) {
                newUsers.add(user);
            }
        }
        return newUsers;
    }
}

/*
  輸出:
  User[id=4, name=趙六, money=20000]
  User[id=5, name=王強(qiáng), money=80000]
*/

此時(shí)此刻,你會(huì)發(fā)現(xiàn)我們似乎寫(xiě)了很多重復(fù)的方法...

使用接口來(lái)代替重復(fù)操作

在 Java 世界中比默,對(duì)于相似的操作我們通常使用接口定義幻捏,對(duì)于不同的操作我們相應(yīng)的定義不同的實(shí)現(xiàn)類來(lái)實(shí)現(xiàn)不同的功能。

public class CollectionStream {
    public static void main(String[] args) {
        // users 定義
        for (User user : getUsers(users, new JudgeIdGreaterThan3())) {
            // 判斷ID是否大于3
            System.out.println(user);
        }

        for (User user : getUsers(users, new JudgeIsWang())) {
            // 判斷是否姓王
            System.out.println(user);
        }

        for (User user : getUsers(users, new JudgeIsRich())) {
            // 判斷是否有錢
            System.out.println(user);
        }
    }

    public static List<User> getUsers(List<User> users, Judge condition) {
        final var newUsers = new ArrayList<User>();
        for (User user : users) {
            if (condition.test(user)) {
                newUsers.add(user);
            }
        }
        return newUsers;
    }

    public static class JudgeIdGreaterThan3 implements Judge {
        @Override
        public boolean test(User user) {
            return user.id() > 3;
        }
    }

    public static class JudgeIsWang implements Judge {
        @Override
        public boolean test(User user) {
            return user.name().startsWith("王");
        }
    }

    public static class JudgeIsRich implements Judge {
        @Override
        public boolean test(User user) {
            return user.money() > 10000;
        }
    }

    public interface Judge {
        boolean test(User user);
    }
}

當(dāng)然命咐,我們也可以使用匿名內(nèi)部類來(lái)實(shí)現(xiàn)同樣的功能。

使用 Java 8 提供的Predicate接口

事實(shí)上谐岁,從 Java 8 開(kāi)始醋奠,JDK 提供了一個(gè)名為Predicate接口,其作用與上方自己寫(xiě)的Judge接口類似伊佃。同時(shí)窜司,因?yàn)樗?strong>函數(shù)式接口,我們可以很輕松地使用 Lambda 表達(dá)式航揉。

Predicate 接口
public class CollectionStream {
    public static void main(String[] args) {
        // users 定義
        for (User user : getUsers(users, user -> user.id() > 3)) {
            // 判斷ID是否大于3
            System.out.println(user);
        }

        for (User user : getUsers(users, user -> user.name().startsWith("王"))) {
            // 判斷是否姓王
            System.out.println(user);
        }

        for (User user : getUsers(users, user -> user.money() > 10000)) {
            // 判斷是否有錢
            System.out.println(user);
        }
    }

    public static List<User> getUsers(List<User> users, Predicate<User> condition) {
        final var newUsers = new ArrayList<User>();
        for (User user : users) {
            if (condition.test(user)) {
                newUsers.add(user);
            }
        }
        return newUsers;
    }
}

總結(jié)

從最初編寫(xiě)一個(gè)一個(gè)獨(dú)立的方法塞祈,到后面自行開(kāi)發(fā)接口逐步地通用化,再到使用 Lambda 表達(dá)式帅涂,我們重復(fù)的工作被逐步逐步地簡(jiǎn)化议薪。

事實(shí)上,在 Java 推出Predicate接口媳友,開(kāi)源世界早已對(duì)于集合操作有了簡(jiǎn)化斯议。例如以Google Guava為代表的第三方框架,以及以Groovy醇锚、Scala哼御、Kotlin為代表的編程語(yǔ)言。

Steam 核心知識(shí)

創(chuàng)建 Stream

使用 Stream.of() 創(chuàng)建

最簡(jiǎn)單的方法是使用Stream.of()來(lái)創(chuàng)建 Stream:

Stream<String> foo = Stream.of("Java", "Python", "Kotlin", "JavaScript");
foo.forEach(System.out::println);

以上代碼創(chuàng)建了一個(gè)由 4 個(gè)編程語(yǔ)言組成的流焊唬,并使用forEach()方法將其打印出來(lái)(forEach方法的參數(shù)為Consumer<T>函數(shù)式接口恋昼,可直接使用 Lambda 表達(dá)式

使用數(shù)組創(chuàng)建

使用數(shù)組創(chuàng)建 Stream 可以使用Arrays.stream()方法來(lái)創(chuàng)建。

String[] foo = new String[]{"Java", "Python", "Kotlin", "JavaScript"};
Stream<String> bar = Arrays.stream(foo);
bar.forEach(System.out::println);

使用集合框架創(chuàng)建

同樣赶促,Stream 也可以基于集合框架來(lái)創(chuàng)建液肌,Collection接口提供了stream()的抽象方法,使得Set芳杏、List矩屁、Map等集合擁有創(chuàng)建 Stream 的能力辟宗。

這里以 List為例:

List<String> foo = List.of("Java", "Python", "Kotlin", "JavaScript");
Stream<String> bar = foo.stream();
bar.forEach(System.out::println);

使用Supplier創(chuàng)建

我們也可以通過(guò)Stream.generate(Supplier<? extends T> s)方法來(lái)創(chuàng)建 Stream。這里參數(shù)要求為Supplier吝秕,它同樣是個(gè)函數(shù)式接口泊脐。

// 以下事例均使用`鏈?zhǔn)讲僮鱜
Stream.generate(() -> new Random().nextInt(100))
      .limit(10)   // 此處使用`limit`來(lái)閑置元素個(gè)數(shù)
      .forEach(System.out::println);

中間操作

中間操作是指調(diào)用方法以后,仍然返回Stream對(duì)象烁峭。Java Stream 中容客,允許有多個(gè)中間操作

map

Stream.map(Function<? super T,? extends R> mapper)是將一個(gè)某個(gè)操作映射到 Stream 中每個(gè)元素上约郁。同樣缩挑,map的參數(shù)為函數(shù)式接口。

例如鬓梅,如下代碼實(shí)現(xiàn)了對(duì)于每個(gè)元素進(jìn)行平方:

Stream.of(1, 2, 3, 4, 5)
      .map(i -> i * i)
      .forEach(System.out::println);   // 1, 4, 9, 16, 25

map方法也可以對(duì)于元素中的對(duì)象進(jìn)行操作供置,例如:

List.of("Java", "Kotlin", "JavaScript")
    .stream()
    .map(String::toUpperCase)     // 將元素轉(zhuǎn)為大寫(xiě)
    .forEach(System.out::println);

filter

Stream.filter(Predicate<? super T> predicate)可以對(duì)于 Stream 中元素進(jìn)行過(guò)濾。

例如绽快,以下代碼將一組數(shù)字中所有偶數(shù)打印出來(lái):

IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
         .filter(i -> i % 2 == 0)
         .forEach(System.out::println);

如果 Stream 中元素為對(duì)象芥丧,同樣可以進(jìn)行過(guò)濾。例如坊罢,如下代碼實(shí)現(xiàn)了將年齡為 18 歲以下的未成年人過(guò)濾:

record Person(String name, int age) { } // 需要使用 Java 16 及以上版本

List<Person> peoples = List.of(
    new Person("張三", 30),
    new Person("李四", 16),
    new Person("王五", 18),
    new Person("王強(qiáng)", 22),
    new Person("小宋", 8)
);
peoples.stream()
       .filter(it -> it.age() >= 18)
       .forEach(System.out::println);

/*
輸出:
Person[name=張三, age=30]
Person[name=王五, age=18]
Person[name=王強(qiáng), age=22]
*/

parallel

通常情況下续担,對(duì) Stream 的元素進(jìn)行處理是單線程的,即一個(gè)一個(gè)元素進(jìn)行處理活孩。但是很多時(shí)候物遇,我們希望可以并行處理 Stream 的元素,因?yàn)樵谠財(cái)?shù)量非常大的情況憾儒,并行處理可以大大加快處理速度询兴。

record Person(String name, int age) { } // 需要使用 Java 16 及以上版本

List<Person> peoples = List.of(
    new Person("張三", 30),
    new Person("李四", 16),
    new Person("王五", 18),
    new Person("王強(qiáng)", 22),
    new Person("小宋", 8)
);
peoples.stream()
       .parallel()  // 將普通 stream 轉(zhuǎn)換為并行 stream
       .filter(it -> it.age() >= 18)    // 并行篩選
       .forEach(System.out::println);

sorted

Stream.sorted()可以實(shí)現(xiàn)對(duì) Stream 中元素進(jìn)行排序,所排序的元素必須實(shí)現(xiàn)Comparable航夺。當(dāng)然也可以在參數(shù)中填入自己的Comparator蕉朵。

如下代碼對(duì)隨機(jī)數(shù)進(jìn)行從小到大的排序:

IntStream.of(5, 7, 3, 2, 6, 0, 9)
         .sorted()
         .forEach(System.out::println);

// 輸出:0 2 3 5 6 7 9

distinct

Stream.distinct()可以對(duì)于 Stream 中的元素進(jìn)行去重:

IntStream.of(5, 8, 3, 4, 5, 3, 6, 9, 5, 3, 7)
         .distinct()
         .forEach(System.out::println);

// 輸出:5 8 3 4 6 9 7

skip

Stream.skip()可以對(duì)于 Stream 中前幾個(gè)元素進(jìn)行跳過(guò)

IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
         .skip(3)
         .forEach(System.out::println);

// 輸出:4 5 6 7 8 9

limit

Stream.limit()可以只保留前幾個(gè)元素:

IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
         .limit(5)
         .forEach(System.out::println);

// 輸出:1 2 3 4 5

concat

Stream.concat()用于將兩個(gè) Stream 合并:

IntStream foo = IntStream.of(1, 2, 3);
IntStream bar = IntStream.of(4, 5, 6);
IntStream.concat(foo, bar)
         .forEach(System.out::println);

// 輸出:1 2 3 4 5 6

終結(jié)操作

終結(jié)操作是指調(diào)用方法后,返回非Stream的操作阳掐,包括void始衅。Java Stream 中,只允許有一個(gè)終結(jié)操作缭保。

終結(jié)操作主要有如下方法:

  • forEach:對(duì)于Stream中每個(gè)元素進(jìn)行遍歷汛闸,常見(jiàn)用途如打印元素。

  • count/max/min:返回元素個(gè)數(shù)/最大值/最小值艺骂。

  • anyMatch/allMatch/noneMatch任意一個(gè)符合/全部符合/都不符合給定的Predicate條件返回true诸老。

  • findFirst/findAny:返回流中第一個(gè)/任意一個(gè)元素。

  • ??collect幾乎可以將一個(gè)Stream對(duì)象轉(zhuǎn)換為任何內(nèi)容钳恕,例如以下代碼可以將姓王的用戶篩選出來(lái)别伏,并轉(zhuǎn)換為 List 集合蹄衷。

    final var ls = users.stream()
         .filter(it -> it.name().startsWith("王"))
         .collect(Collectors.toList());
    

    因?yàn)?code>collect方法較為復(fù)雜,有興趣可以自行閱讀 JDK 文檔厘肮。

IDEA 流調(diào)試器

IDEA 中內(nèi)置了一個(gè)名為Java Stream Debugger插件(如果沒(méi)有請(qǐng)確保自己為最新版的 IDEA愧口,或者嘗試前往 IDEA 插件市場(chǎng)安裝),該插件可以通過(guò)可視化的方式直觀地看到 Stream 的處理過(guò)程类茂。

使用方式:

  1. 在 Stream 流中打上斷點(diǎn)耍属;

  2. 啟動(dòng) Debug 模式;

  3. 斷點(diǎn)暫停后巩检,點(diǎn)擊 Debug 面板上的Trace Current Stream Chain按鈕(如圖所示)

    `Trace Current Stream Chain` Button

該插件可以分步地將 Stream 操作以可視化的形式呈現(xiàn)出來(lái)(當(dāng)然也可以通過(guò)下方的Flat Mode按鈕在同一個(gè)窗口中看到所有操作)

Split Mode
Flat Mode

演示 1 - filter

public class CollectionStream {
    public record User(Integer id, String name, Integer money) { }

    public static void main(String[] args) {
        final var users = Arrays.asList(
                new User(1, "張三", 200),
                new User(2, "李四", 200),
                new User(3, "王五", 10000),
                new User(4, "趙六", 20000),
                new User(5, "王強(qiáng)", 80000)
        );
        users.stream()
                .filter(it -> it.money() > 10000)
                .collect(Collectors.toList());
    }
}
16373731272913.jpg

演示 2 - distinct

public class CollectionStream {
    public static void main(String[] args) {
        final var list = Arrays.asList(1, 2, 3, 4, 5, 1, 2, 3, 4, 1, 2);
        list.stream()
                .distinct()
                .forEach(System.out::println);
    }
}
16373734336456.jpg

演示 3 - sorted

public class CollectionStream {
    public static void main(String[] args) {
        final var list = Arrays.asList(6, 4, 3, 5, 6, 7, 8, 2);
        list.stream()
                .sorted()
                .forEach(System.out::println);
    }
}
16373736982517.jpg

演示 4 - map

public class CollectionStream {
    public record User(Integer id, String name, Integer money) { }

    public static void main(String[] args) {
        final var users = Arrays.asList(
                new User(1, "張三", 200),
                new User(2, "李四", 200),
                new User(3, "王五", 10000),
                new User(4, "趙六", 20000),
                new User(5, "王強(qiáng)", 80000)
        );
        users.stream()
                .filter(it -> it.name().startsWith("王"))
                .map(User::name)
                .forEach(System.out::println);
    }
}
16373757675756.jpg
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末厚骗,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子兢哭,更是在濱河造成了極大的恐慌领舰,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,525評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迟螺,死亡現(xiàn)場(chǎng)離奇詭異提揍,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)煮仇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)谎仲,“玉大人浙垫,你說(shuō)我怎么就攤上這事≈E担” “怎么了夹姥?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,862評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)辙诞。 經(jīng)常有香客問(wèn)我辙售,道長(zhǎng),這世上最難降的妖魔是什么飞涂? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,728評(píng)論 1 294
  • 正文 為了忘掉前任旦部,我火速辦了婚禮,結(jié)果婚禮上较店,老公的妹妹穿的比我還像新娘士八。我一直安慰自己,他們只是感情好梁呈,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布婚度。 她就那樣靜靜地躺著,像睡著了一般官卡。 火紅的嫁衣襯著肌膚如雪蝗茁。 梳的紋絲不亂的頭發(fā)上醋虏,一...
    開(kāi)封第一講書(shū)人閱讀 51,590評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音哮翘,去河邊找鬼颈嚼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛忍坷,可吹牛的內(nèi)容都是我干的粘舟。 我是一名探鬼主播,決...
    沈念sama閱讀 40,330評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼佩研,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼柑肴!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起旬薯,我...
    開(kāi)封第一講書(shū)人閱讀 39,244評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤晰骑,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后绊序,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體硕舆,經(jīng)...
    沈念sama閱讀 45,693評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評(píng)論 3 336
  • 正文 我和宋清朗相戀三年骤公,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了抚官。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,001評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡阶捆,死狀恐怖凌节,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情洒试,我是刑警寧澤倍奢,帶...
    沈念sama閱讀 35,723評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站垒棋,受9級(jí)特大地震影響卒煞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜叼架,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評(píng)論 3 330
  • 文/蒙蒙 一畔裕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧碉碉,春花似錦柴钻、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,919評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春毫蚓,著一層夾襖步出監(jiān)牢的瞬間占键,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,042評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工元潘, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留畔乙,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,191評(píng)論 3 370
  • 正文 我出身青樓翩概,卻偏偏與公主長(zhǎng)得像牲距,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子钥庇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評(píng)論 2 355

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