Java Stream API 中文文档

发布于 2024-06-21 00:02:55 字数 25304 浏览 29 评论 0

Java8 的两个重大改变,一个是 Lambda 表达式,另一个就是本节要讲的 Stream API 表达式。Stream 是 Java8 中处理集合的关键抽象概念,它可以对集合进行非常复杂的查找、过滤、筛选等操作,在新版的 JPA 中,也已经加入了 Stream。

一. Stream 操作步骤

1.1 Stream 有如下三个操作步骤:

第一步:创建流

从一个数据源,如集合、数组中获取流。

第二步:进行中间操作

一个流后面可以跟 0 个或多个中间操作,多个中间操作可以连接起来形成一条流水线。中间操作通常在没有结束操作之前是不会被触发的。

第三步:获取结果(结束操作)

一个流只能有一个结束操作,当这个操作执行后,前面的中间操作会被触发,此时流就再无法被使用。Stream 中几乎所有返回是 void 方法都是结束操作。

1.2 Stream 的特征

第一步:创建流

  • 流不会存储值,通过管道的方式获取值。
  • 对流的操作会生成一个结果,不过并不会修改底层的数据源
  • 一个流只能使用一次

1.3 入门小案例

假设有一个 Person 类和一个 Person 列表,现在有两个需求:1)找到年龄大于 18 岁的人并输出;2)找出所有中国人的数量。

@Data
class Person {
    private String name;
    private Integer age;
    private String country;
    private char sex;

    public Person(String name, Integer age, String country, char sex) {
        this.name = name;
        this.age = age;
        this.country = country;
        this.sex = sex;
    }
}
List<Person> personList = new ArrayList<>();
personList.add(new Person("欧阳雪",18,"中国",'F'));
personList.add(new Person("Tom",24,"美国",'M'));
personList.add(new Person("Harley",22,"英国",'F'));
personList.add(new Person("向天笑",20,"中国",'M'));
personList.add(new Person("李康",22,"中国",'M'));
personList.add(new Person("小梅",20,"中国",'F'));
personList.add(new Person("何雪",21,"中国",'F'));
personList.add(new Person("李康",22,"中国",'M'));

在 JDK8 以前,我们可以通过遍历列表来完成。但是在有了 Stream API 后,可以这样来实现:

public static void main(String[] args) {

    // 1)找到年龄大于 18 岁的人并输出;
    personList.stream().filter((p) -> p.getAge() > 18).forEach(System.out::println);

    System.out.println("-------------------------------------------");

    // 2)找出所有中国人的数量
    long chinaPersonNum = personList.stream().filter((p) -> p.getCountry().equals("中国")).count();
    System.out.println("中国人有:" + chinaPersonNum + "个");
}

输出结果:

在这个例子中,personList.stream()是创建流,filter()属于中间操作,forEach、count()是终止操作。

二. 创建流

2.1 创建有限流

有限流的意思就是创建出来的流是基于一个已经存在的数据源,也就是说流在创建的时候数据源的个数已经基本确定了。

  • Stream.of() :Stream 类的静态方法。
  • Collection 实例.stream() :返回此集合作为数据源的流。
  • Collection 实例.parallelStream() :返回此集合作为数据源的并行流。
  • Arrays.stream() :数组转换为流。
List<String> list = new ArrayList<>();
Stream<String> stream1 = list.stream();

String[] arr=new String[15];
Stream<String> stream2 = Arrays.stream(arr);

Stream<String> stream3 = Stream.of("a", "b", "c");

2.2 创建无限流

无限流相对于有限流,并没有一个特定定的数据源,它是通过循环执行"元素生成器"来不断产生新元素的。这种方式在日常开发中相对少见。

  • Stream.iterate(T seed, UnaryOperator<T> f) :创建一个无限但有序的流 ,seed 传入的是迭代器的种子(起始值),f 传入的是迭代函数。
//这个流从 0 开始,每一个元素加 3
Stream<Integer> stream4 = Stream.iterate(0, i -> i + 3);
//循环打印流中的元素(如果不停止运行,程序会无限输出下去)
stream4.forEach(System.out::println);

//----------上述 Lambda 表达式实际上是下列语法的一个简化写法,后文不再提供 Lambda 表达式转换,如果看不懂的话可以先去熟悉 Lambda 表达式的用法------
Stream.iterate(0, new UnaryOperator<Integer>() {
    @Override
    public Integer apply(Integer i) {
        return i + 3;
    }
});
  • Stream.generate(Supplier<T> s) :传入的是一个“元素生成器”,Supplier 是一个函数式接口,它用于在无输入参数的情况下,返回一个结果值。
//创建一个无限流,这个流中每一个元素都是 Math.random() 方法生成的
Stream<Double> stream5 = Stream.generate(() -> Math.random());
stream5.forEach(System.out::println);

//----------上述 Lambda 表达式由于只调用了一个 Math.random() 方法,实际上能够更加精简----
Stream<Double> stream5 = Stream.generate(Math::random);  

三. Stream 中间操作

3.1 筛选与切片

  • filter:从流中排除某些操作;
  • limit(n):截断流,使其元素不超过给定对象
  • skip(n):跳过元素,返回一个扔掉了前 n 个元素的流,若流中元素不足 n 个,则返回一个空流,与 limit(n) 互补
  • distinct:筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素。

3.1.1 filter

//保留流中 person.getAge()==20 的元素
personList.stream().filter(person -> person.getAge() == 20).forEach(System.out::println);

输出结果为:

Person(name=向天笑, age=20, country=中国, sex=M)
Person(name=小梅, age=20, country=中国, sex=F)

3.1.2 limit

personList.stream().limit(2).forEach(System.out::println);

输出结果为:

Person(name=欧阳雪, age=18, country=中国, sex=F)
Person(name=Tom, age=24, country=美国, sex=M)

3.1.3 skip

personList.stream().skip(1).forEach(System.out::println);

输出结果为:

Person(name=Harley, age=22, country=英国, sex=F)
Person(name=向天笑, age=20, country=中国, sex=M)
Person(name=李康, age=22, country=中国, sex=M)
Person(name=小梅, age=20, country=中国, sex=F)
Person(name=何雪, age=21, country=中国, sex=F)
Person(name=李康, age=22, country=中国, sex=M)

3.1.4 distinct

personList.stream().distinct().forEach(System.out::println);

输出结果为:

Person(name=欧阳雪, age=18, country=中国, sex=F)
Person(name=Tom, age=24, country=美国, sex=M)
Person(name=Harley, age=22, country=英国, sex=F)
Person(name=向天笑, age=20, country=中国, sex=M)
Person(name=李康, age=22, country=中国, sex=M)
Person(name=小梅, age=20, country=中国, sex=F)
Person(name=何雪, age=21, country=中国, sex=F)

3.2 映射

3.2.1 map

map 映射是接收一个 Function 接口的实例,它将 Stream 中的所有元素依次传入进去, Function.apply 方法将原数据转换成其他形式的数据

例一:

假如,我们需要将 List<String> 所有元素转化为大写,我们可以这么做:

Stream<String> stream = Stream.of("aaa", "bbb", "ccc", "ddd");
stream.map(new Function<String, String>() {
    @Override
    public String apply(String s) {
        return s.toUpperCase();
    }
}).forEach(System.out::println);

//上面的匿名内部类可以精简为 Lambda 表达式形式
stream.map(s -> s.toUpperCase()).forEach(System.out::println);

//采用方法引用,更进一步精简 Lambda 表达式
stream.map(String::toUpperCase).forEach(System.out::println);

流中的每一个元素都经过了 Function 实例 apply 方法的处理,将其转换为了大写。

例二:

假如我们需要取出 List<Person> 中每一个 Person 中的姓名取出来并打印出来:

List<Person> personList = new ArrayList<>();
personList.add(new Person("欧阳雪", 18, "中国", 'F'));
personList.add(new Person("Tom", 24, "美国", 'M'));
personList.add(new Person("Harley", 22, "英国", 'F'));
personList.add(new Person("向天笑", 20, "中国", 'M'));
personList.add(new Person("李康", 22, "中国", 'M'));
personList.add(new Person("小梅", 20, "中国", 'F'));
personList.add(new Person("何雪", 21, "中国", 'F'));
personList.add(new Person("李康", 22, "中国", 'M'));

personList.stream().map(new Function<Person, String>() {
            @Override
            public String apply(Person person) {
                return person.getName();
            }
        }).forEach(System.out::println);

//上面的匿名内部类可以精简为 Lambda 表达式形式
personList.stream().map(person -> person.getName()).forEach(System.out::println);

//采用方法引用,更进一步精简 Lambda 表达式
stream.map(Person::getName).forEach(System.out::println);

3.2.2 flatMap

flatMap 接收一个函数作为参数,将流中的每一个值转换成另一个流,然后把所有流合并在一起

例一

我们需要将 List<String> 中的每一个字符按顺序打印出来。我们先试着用 map 映射达到需求:

//使用 map 也能实现这个需求
@Test
public void testFlatMap() {
    Stream<String> stream = Stream.of("aaa", "bbb", "ccc", "ddd");
    Stream<Stream<Character>> streamStream = stream.map(s -> toCharacterStream(s));
    streamStream.forEach(s->s.forEach(System.out::println));
}

public static Stream<Character> toCharacterStream(String str) {
    List<Character> list = new ArrayList<>();
    for (char c : str.toCharArray()) {
        list.add(c);
    }
    return list.stream();
}

我们通过 map() 将原先流中每一个 String 映射为 Stream<Character> 然后重新放入原先的流中,此时流中的每一个元素都是一个 Stream<Character> ,然后我们遍历外层流得到每一个子流,然后子流 forEach 输出每一个字符。

可以看到使用 map 映射的时候,如果返回 Stream ,那这些 Stream 仍然是相互独立的;但是 flatMap 映射会将返回的流合并为一个流

@Test
public void testFlatMap2() {
    Stream<String> stream = Stream.of("aaa", "bbb", "ccc", "ddd");
    //重点对比这一行,map 返回的是 Stream<Stream<Character>>,flatMap 返回的是 Stream<Character>
    Stream<Character> characterStream = stream.flatMap(s -> toCharacterStream(s));
    characterStream.forEach(System.out::println);
}

public static Stream<Character> toCharacterStream(String str) {
    List<Character> list = new ArrayList<>();
    for (char c : str.toCharArray()) {
        list.add(c);
    }
    return list.stream();
}

你品,你细细的品!!

3.3 排序

3.3.1 自然排序

自然排序需要流中的实例实现了 Comparable 接口。

personList.stream().sorted().forEach(System.out::println);

3.3.2 定制排序

定制排序,需要传入一个 Comparator

personList.stream().sorted((o1, o2) -> o1.getAge()-o2.getAge()).forEach(System.out::println);
personList.stream().sorted(Comparator.comparing(Person::getAge)) //使用 Comparator 静态方法构造

Comparator 的更多用法参考: JDK8 中 Comparator 默认方法

四. 终止操作

4.1 查找与匹配

  • allMatch:检查是否匹配所有元素,返回 boolean
  • anyMatch:检查是否至少匹配一个元素,返回 boolean
  • noneMatch:检查是否没有匹配所有元素,返回 boolean
  • findFirst:返回第一个元素
  • findAny:返回当前流中任意元素
  • count:返回元素中元素的总个数
  • max:返回流中的最大值
  • min:返回流中的最小值
  • forEach:循环

4.2 规约(reduce)

reduce 是将流中的元素反复结合起来,得到一个最终值:

//没有初始值的规约
Optional<T> reduce(BinaryOperator<T> accumulator);

//identity 是初始值,accumulator 是一个二元运算
T reduce(T identity, BinaryOperator<T> accumulator);

例一

数组中所有元素的求和:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
Integer sum = stream.reduce(0, (integer, integer2) -> integer + integer2);
//Integer sum = stream.reduce(0, Integer::sum);   与上面具有想用意义
System.out.println(sum);

例二

计算所有人的年龄总和:

Optional<Integer> reduce = personList.stream().map(person -> person.getAge()).reduce(Integer::sum);
System.out.println(reduce.get());

4.3 收集(collect)

collect() 将流转化为其他形式。接收一个 Collector (收集器)接口的实现,用于收集流中的元素。 Collector 接口中的方法决定了如何对流进行收集操作, Collectors 类提供了很多静态方法,可以方便地创建常用的收集器:

4.3.1 收集为数组

Person[] personArray = personList.stream().toArray(Person[]::new);

4.3.2 收集成集合

  • Collectors.toList() :将所有元素收集到一个 List 中
List<Person> persons = personList.stream().collect(Collectors.toList());
  • Collectors.toSet() :将所有元素收集到一个 Set 中
Set<Person> persons = personList.stream().collect(Collectors.toSet());
  • Collectors.toMap() :将所有元素收集到一个 Map 中
//将 Person 实例的 name 属性作为 Key,person 对象作为 value 放入 Map 中
Map<String, Person> collect = personList.stream().distinct().collect(Collectors.toMap(person -> person.getName(),o -> o));
  • Collectors.toCollection() :将所有元素放入集合
HashSet<Person> collect = personList.stream().collect(Collectors.toCollection(HashSet::new));

4.3.3 收集聚合信息(类似于 SQL 中的聚合函数)

  • Collectors.averagingInt() :收集所有元素求平均值。
//获取所有 Person 的平均年龄
Double average = personList.stream().collect(Collectors.averagingInt(Person::getAge));
  • Collectors.counting() :计算元素总数量
Long count = personList.stream().collect(Collectors.counting());
  • Collectors.summing() :计算元素总和
//求出年龄的总和
Integer sum = personList.stream().collect(Collectors.summingInt(Person::getAge));
  • Collectors.maxBy() :最大值(与 max() 方法效果一样)
  • Collectors.minBy() :最小值
//求出年龄最大的人
Optional<Person> max = personList.stream().collect(Collectors.maxBy((o1, o2) -> o1.getAge() - o2.getAge()));
  • Collectors.summarizingInt :可以获取所有的聚合信息。
//获取年龄的所有聚合信息
IntSummaryStatistics summary = personList.stream().collect(Collectors.summarizingInt(Person::getAge));
System.out.println(summary.getMax());
System.out.println(summary.getAverage());
System.out.println(summary.getCount());
System.out.println(summary.getMin());

4.3.4 分组收集(类似于 SQL 中的 group by 语句)

//分类器函数将输入元素映射到键(单级分组)
groupingBy(Function<? super T, ? extends K> classifier)
    
//多级分组。downstream 实现下级分组
groupingBy(Function<? super T, ? extends K> classifier,Collector<? super T, A, D> downstream)

//多级分组,并且指定当前分组创建 Map 的方法
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
                                  Supplier<M> mapFactory,
                                  Collector<? super T, A, D> downstream)

例如我们需要按照年龄进行分组:

//按照年龄进行分组
Map<Integer, List<Person>> collect = personList.stream().collect(Collectors.groupingBy(person -> person.getAge()));
Map<Integer, List<Person>> collect = personList.stream().collect(Collectors.groupingBy(Person::getAge, HashMap::new, Collectors.toCollection(ArrayList::new)));
{
	18: [{
		"age": 18,
		"country": "中国",
		"name": "欧阳雪",
		"sex": "F"
	}],
	20: [{
		"age": 20,
		"country": "中国",
		"name": "向天笑",
		"sex": "M"
	}, {
		"age": 20,
		"country": "中国",
		"name": "小梅",
		"sex": "F"
	}],
	...
}

多级分组

我们可以通过传入下级分组器,来实现多级分组。例如我们需要先按照年龄进行分组,然后再按照国籍进行分组:

Map<Integer, Map<String, List<Person>>> collect = personList.stream().collect(
                Collectors.groupingBy(person -> person.getAge(), Collectors.groupingBy(o -> o.getCountry())));

//或者这么写也可以
Map<Integer, Map<String, List<Person>>> collect = personList.stream().collect(
                Collectors.groupingBy(Person::getAge, Collectors.groupingBy(Person::getCountry)));

如果我们每一级的集合对象都需要自定义,我们可以这样做:

HashMap<Integer, TreeMap<String, LinkedList<Person>>> collect = personList.stream().collect(
                Collectors.groupingBy(Person::getAge,
                        HashMap::new, Collectors.groupingBy(Person::getCountry,TreeMap::new,
                                Collectors.toCollection(LinkedList::new))));
{
	18: {
		"中国": [{
			"age": 18,
			"country": "中国",
			"name": "欧阳雪",
			"sex": "F"
		}]
	},
	22: {
		"中国": [{
			"age": 22,
			"country": "中国",
			"name": "李康",
			"sex": "M"
		}, {
			"age": 22,
			"country": "中国",
			"name": "李康",
			"sex": "M"
		}],
		"英国": [{
			"age": 22,
			"country": "英国",
			"name": "Harley",
			"sex": "F"
		}]
	}
}

4.3.5 分区收集

分区收集能将符合条件的放在一个集合中,不符合条件的放在另一个集合中

//将年龄大于等于 20 的分为一组,将年龄小于 20 的分为一组(实际上用 groupBy 也能实现)
Map<Boolean, List<Person>> collect = personList.stream().collect(Collectors.partitioningBy(person -> person.getAge() >= 20));

4.3.6 拼接字符串

Collectors.joining 收集器按顺序将输入元素连接到一个字符串中:

//将姓名拼成字符串中间用逗号隔开,首尾用大括号括起来
String str = personList.stream().map(Person::getName).collect(Collectors.joining(",","{","}"));
{欧阳雪,Tom,Harley,向天笑,李康,小梅,何雪,李康}

4.4 循环

  • foreach
personList.stream().forEach(System.out::println);

五. 并行流

前面我么所将的流的操作都建立在串行流的基础上,在数据量小的情况下没有任何问题,但是一旦数据量多起来,单线程的效率问题就会凸显。

不同于串行流,并行流底层使用了 Fork-Join 框架,将任务分派到多个线程上执行,这样可以大大提高 CPU 资源的利用率。

5.1 如何创建并行流

在创建时直接创建并行流

Collection实例.parallelStream();

将串行流转化为并行流

Stream实例.parallel();

将并行流转化为串行流

Stream实例.sequential();

5.2 串行流和并行流的对比

我们分别使用串行流和并行流进行一百亿个数的累加:

串行流

long start = System.currentTimeMillis();
LongStream stream = LongStream.rangeClosed(0, 10000000000L);
long reduce = stream.reduce(0, (left, right) -> left + right);
long end = System.currentTimeMillis();
System.out.println("付费:" + (end - start) );

通过测试我们发现,串行流需要 55s,通过任务管理器也能发现 CPU 资源并没有充分利用。

并行流

long start = System.currentTimeMillis();
LongStream stream = LongStream.rangeClosed(0, 100000000000L);
long reduce = stream.parallel().reduce(0, (left, right) -> left + right);
long end = System.currentTimeMillis();
System.out.println("付费:" + (end - start) );

并行流花费:20S,并行流在执行时能够充分利用 CPU 资源。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

文章
评论
25 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文