Java 8 Stream 知识分享

本文原文

综述

Java 8 新增了 Stream APIStream API有点类似使用SQL 语句,可以将集合中的元素进行过滤。使用时,类似于从一个管道中抽取元素,并对他们进行操作。使用流的一个优点是,他可以使得我们的程序更小,并且更容易理解。

Stream API相关的接口有StreamIntStream, LongStream, DoubleStream(因为 Java 的泛型不支持基本数据类型,而又因频繁的装箱、拆箱存在效率问题,故额外有后三者)。

使用Stream操作时,我们通常使用链式操作,即将多条代码合并成一条代码(事例将在使用Supplier创建中给出)。

Java Collection 体系数据处理的演进

本小节用于测试的代码如下:

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, "王强", 80000)
);

通过不同方法来过滤不同数据

我们过滤数据首先想到的方法是针对各个需求来定义一个个的方法。

例如,产品经理给了你一个筛选出所有 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=王强, money=80000]
*/

第二天,产品经理要求你筛选出所有姓“王”的用户的需求,定义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=王强, money=80000]
*/

第三天,产品经理要求你开发所有钱大于 10000 的用户的需求,你瞅了瞅他,写出了如下代码:

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=王强, money=80000]
*/

此时此刻,你会发现我们似乎写了很多重复的方法...

使用接口来代替重复操作

在 Java 世界中,对于相似的操作我们通常使用接口定义,对于不同的操作我们相应的定义不同的实现类来实现不同的功能。

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);
    }
}

当然,我们也可以使用匿名内部类来实现同样的功能。

使用 Java 8 提供的Predicate接口

事实上,从 Java 8 开始,JDK 提供了一个名为Predicate接口,其作用与上方自己写的Judge接口类似。同时,因为它是函数式接口,我们可以很轻松地使用 Lambda 表达式。

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;
    }
}

总结

从最初编写一个一个独立的方法,到后面自行开发接口逐步地通用化,再到使用 Lambda 表达式,我们重复的工作被逐步逐步地简化。

事实上,在 Java 推出Predicate接口,开源世界早已对于集合操作有了简化。例如以Google Guava为代表的第三方框架,以及以GroovyScalaKotlin为代表的编程语言。

Steam 核心知识

创建 Stream

使用 Stream.of() 创建

最简单的方法是使用Stream.of()来创建 Stream:

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

以上代码创建了一个由 4 个编程语言组成的流,并使用forEach()方法将其打印出来(forEach方法的参数为Consumer<T>函数式接口,可直接使用 Lambda 表达式

使用数组创建

使用数组创建 Stream 可以使用Arrays.stream()方法来创建。

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

使用集合框架创建

同样,Stream 也可以基于集合框架来创建,Collection接口提供了stream()的抽象方法,使得SetListMap等集合拥有创建 Stream 的能力。

这里以 List为例:

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

使用Supplier创建

我们也可以通过Stream.generate(Supplier<? extends T> s)方法来创建 Stream。这里参数要求为Supplier,它同样是个函数式接口。

// 以下事例均使用`链式操作`
Stream.generate(() -> new Random().nextInt(100))
      .limit(10)   // 此处使用`limit`来闲置元素个数
      .forEach(System.out::println);

中间操作

中间操作是指调用方法以后,仍然返回Stream对象。Java Stream 中,允许有多个中间操作

map

Stream.map(Function<? super T,? extends R> mapper)是将一个某个操作映射到 Stream 中每个元素上。同样,map的参数为函数式接口。

例如,如下代码实现了对于每个元素进行平方:

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

map方法也可以对于元素中的对象进行操作,例如:

List.of("Java", "Kotlin", "JavaScript")
    .stream()
    .map(String::toUpperCase)     // 将元素转为大写
    .forEach(System.out::println);

filter

Stream.filter(Predicate<? super T> predicate)可以对于 Stream 中元素进行过滤。

例如,以下代码将一组数字中所有偶数打印出来:

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

如果 Stream 中元素为对象,同样可以进行过滤。例如,如下代码实现了将年龄为 18 岁以下的未成年人过滤:

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

List<Person> peoples = List.of(
    new Person("张三", 30),
    new Person("李四", 16),
    new Person("王五", 18),
    new Person("王强", 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=王强, age=22]
*/

parallel

通常情况下,对 Stream 的元素进行处理是单线程的,即一个一个元素进行处理。但是很多时候,我们希望可以并行处理 Stream 的元素,因为在元素数量非常大的情况,并行处理可以大大加快处理速度。

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

List<Person> peoples = List.of(
    new Person("张三", 30),
    new Person("李四", 16),
    new Person("王五", 18),
    new Person("王强", 22),
    new Person("小宋", 8)
);
peoples.stream()
       .parallel()  // 将普通 stream 转换为并行 stream
       .filter(it -> it.age() >= 18)    // 并行筛选
       .forEach(System.out::println);

sorted

Stream.sorted()可以实现对 Stream 中元素进行排序,所排序的元素必须实现Comparable。当然也可以在参数中填入自己的Comparator

如下代码对随机数进行从小到大的排序:

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

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

distinct

Stream.distinct()可以对于 Stream 中的元素进行去重:

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()可以对于 Stream 中前几个元素进行跳过

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()可以只保留前几个元素:

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()用于将两个 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

终结操作

终结操作是指调用方法后,返回非Stream的操作,包括void。Java Stream 中,只允许有一个终结操作

终结操作主要有如下方法:

  • forEach:对于Stream中每个元素进行遍历,常见用途如打印元素。

  • count/max/min:返回元素个数/最大值/最小值

  • anyMatch/allMatch/noneMatch任意一个符合/全部符合/都不符合给定的Predicate条件返回true

  • findFirst/findAny:返回流中第一个/任意一个元素。

  • 🌟collect几乎可以将一个Stream对象转换为任何内容,例如以下代码可以将姓王的用户筛选出来,并转换为 List 集合。

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

    因为collect方法较为复杂,有兴趣可以自行阅读 JDK 文档。

IDEA 流调试器

IDEA 中内置了一个名为Java Stream Debugger插件(如果没有请确保自己为最新版的 IDEA,或者尝试前往 IDEA 插件市场安装),该插件可以通过可视化的方式直观地看到 Stream 的处理过程。

使用方式:

  1. 在 Stream 流中打上断点;

  2. 启动 Debug 模式;

  3. 断点暂停后,点击 Debug 面板上的Trace Current Stream Chain按钮(如图所示)

    `Trace Current Stream Chain` Button

该插件可以分步地将 Stream 操作以可视化的形式呈现出来(当然也可以通过下方的Flat Mode按钮在同一个窗口中看到所有操作)

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, "王强", 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, "王强", 80000)
        );
        users.stream()
                .filter(it -> it.name().startsWith("王"))
                .map(User::name)
                .forEach(System.out::println);
    }
}
16373757675756.jpg
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351

推荐阅读更多精彩内容