前言

Java Stream API借助于Lambda表达式,为Collection操作提供了一个新的选择。如果使用得当,可以极大地提高编程效率和代码可读性。

本文将介绍Stream API包含的方法,并通过示例详细展示其用法。


一、Stream特点

Stream不是集合元素,它不是数据结构也不保存数据,而更像一个高级版本的迭代器(Iterator)。Stream操作可以像链条一样排列,形成Stream Pipeline,即链式操作。

Stream Pipeline由数据源的零或多个中间(Intermediate)操作和一个终端(Terminal)操作组成。中间操作都以某种方式进行流数据转换,将一个流转换为另一个流,转换后元素类型可能与输入流相同或不同,例如将元素按函数映射到其他类型或过滤掉不满足条件的元素。 终端操作对流执行最终计算,例如将其元素存储到集合中、遍历打印元素等。

Stream特点:

  • 无存储。Stream不是一种数据结构,也不保存数据,数据源可以是一个数组,Java容器或I/O Channel等。

  • 为函数式编程而生。对Stream的任何修改都不会修改数据源,例如对Stream过滤操作不会删除被过滤的元素,而是产生一个不包含被过滤元素的新Stream。

  • 惰性执行。Stream上的中间操作并不会立即执行,只有等到用户真正需要结果时才会执行。

  • 一次消费。Stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。

注意:没有终端操作的流管道是静默无操作的,所以不要忘记包含一个终端操作。

二、用法示例

以下将基于《Java 8 Optional类使用的实践经验》一文中的Person类,展示Stream API的用法。考虑到代码简洁度,示例中尽量使用方法引用。

2.1 Stream创建

2.1.1 通过参数序列创建Stream

对于可变参数序列,通过Stream.of()创建Stream,而不必先创建Array再创建Stream。

IntStream stream = IntStream.of(10, 20, 30, 40, 50); // 不要使用Stream<Integer>
Stream<String> colorStream = Stream.of("Red", "Pink", "Purple");
Stream<Person> personStream = Stream.of(
new Person("mike", "male", 10),
new Person("lucy", "female", 4),
new Person("jason", "male", 5)
);

2.1.2 通过数组创建Stream

不用区分基础数据类型,但参数只能是数组。

int[] intNumbers = {10, 20, 30, 40, 50};
IntStream stream = IntStream.of(intNumbers);

2.1.3 通过集合(Collection子类)创建Stream

调用parallelStream()或stream().parallel()方法可创建并行Stream。

Stream<Integer> numberStream = Arrays.asList(10, 20, 30, 40, 50).stream();

2.1.4 通过生成器创建Stream

· 通常用于随机数、元素满足固定规则的Stream,或用于生成海量测试数据的场景。

· 应配合limit()、filter()使用,以控制Stream大小,否则stream长度无限。

Stream.generate(Math::random).limit(10)
Stream.generate(() -> (int) (System.nanoTime() % 100)).limit(5)

2.1.5 通过iterate创建Stream

· 重复对给定种子值(seed)调用指定的函数来创建Stream,其元素为seed, f(seed), f(f(seed))...无限循环。

· 通常用于随机数、元素满足固定规则的Stream,或用于生成海量测试数据的场景。

· 应配合limit()、filter()使用,以控制Stream大小,否则stream长度无限。

// 按行依次输出:0、5、10、15、20
Stream.iterate(0, n -> n + 5).limit(5).forEach(System.out::println);

2.1.6 通过区间创建整数序列Stream

用于IntStream、LongStream,range()不包含尾元素,rangeClosed()包含尾元素。

LongStream longRange = LongStream.range(-100L, 100L); // 生成[-100, 100)区间的元素序列

2.1.7 通过IO方式创建Stream

· 适用于从文本文件中逐行读取数据、遍历文件目录等场景。

· 通常配合try ... with resources语法使用,以安全而简洁地关闭资源。

try (Stream<String> lines = Files.lines(Paths.get("./file.txt"), StandardCharsets.UTF_8)) {
// 跳过第一行,输出第2~4共计三行
lines.skip(1).limit(3).forEach(System.out::println);
} catch (IOException e){
System.out.println("Oops!");
}

2.2 Stream操作

常见的操作可以归类如下:

Intermediate:Stream经过此类操作后,结果仍为Stream

map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered

Terminal:Stream里包含的内容按照某种算法汇聚为一个值

forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

基本的Stream用法格式为Stream.Intermediate.Terminal(SIT)Java8特性详解 lambda表达式 Stream以图示形式直观描述了这种格式及若干Intermediate操作。

本节主要介绍常用操作及代码示例。为便于演示,首先定义如下集合对象:

List<Person> persons = Arrays.asList(
new Person("mike", "male", 10).setLocation("China", "Nanjing"),
new Person("lucy", "female", 4),
new Person("jason", "male", 5).setLocation("China", "Xian")
);

2.2.1 map + sum + filter + reduce

只有IntStream、LongStream和DoubleStream支持sum()方法。

// 计算年龄总和:totalAge = 19
int totalAge = persons.stream().mapToInt(Person::getAge).sum();
// 并行计算年龄总和,此处不建议使用reduce(针对复杂的规约操作)
persons.stream().parallel().mapToInt(Person::getAge).reduce(0, Integer::sum);
// 计算男生年龄总和:totalAge = 15
persons.stream().filter(person -> "male".equals(person.getGender())).mapToInt(Person::getAge).sum();

2.2.2 map + average + max

average()返回OptionalDouble,max()/min()返回OptionalInt或Optional。

// 计算年龄均值,输出6.333333333333333
persons.stream().mapToInt(Person::getAge).average().ifPresent(System.out::println);
// 计算字典序最大的人名,输出mike
persons.stream().map(Person::getName).max(String::compareToIgnoreCase).ifPresent(System.out::println);

2.2.3 map + forEach

// 输出每个学生姓名的大写形式,按行输出:MIKE、LUCY、JASON
persons.stream()
.map(Person::getName) // 将Person对象映射为String(姓名)
.map(String::toUpperCase) // 将姓名转换大写
.forEach(System.out::println); // 按行输出List元素

2.2.4 collect

· collect操作可将Stream元素转换为不同的数据类型,如字符串、List、Set和Map等。

· Java 8通过Collectors类支持各种内置收集器,以简化collect操作。

// 得到字符串:Colors: Red&Pink&Purple!
colorStream.collect(Collectors.joining("&", "Colors: ", "!"));
// 得到ArrayList,元素为:Red, Pink, Purple
// 注意,Stream转换为数组的格式形如stream.toArray(String[]::new)
colorStream.collect(Collectors.toList());
// 得到HashSet,元素为:Red, Pink, Purple
colorStream.collect(Collectors.toSet());
// 得到LinkedList,toCollection()用于指定集合类型
colorStream.collect(Collectors.toCollection(LinkedList::new));
// 得到HashMap,{mike=Person{name='mike'}, jason=Person{name='jason'}, lucy=Person{name='lucy'}}
personStream.collect(Collectors.toMap(Person::getName, Function.identity()));

collect收集器还提供summingInt()、averagingInt()和summarizingInt()等计算方法。

// 返回流中整数属性求和,即19
persons.stream().collect(Collectors.summingInt(Person::getAge))
// 计算流中Integer属性的平均值,即6.333333333333333
persons.stream().collect(Collectors.averagingInt(Person::getAge))
// 收集流中Integer属性的统计值,即IntSummaryStatistics{count=3, sum=19, min=4, average=6.333333, max=10}
persons.stream().collect(Collectors.summarizingInt(Person::getAge))

2.2.5 sorted + collect

// 按照年龄升序排序:sortedpersons = [Person{name='lucy'}, Person{name='jason'}, Person{name='mike'}]
List<Person> sortedPersons = persons.stream()
.sorted(Comparator.comparingInt(Person::getAge)) // 按照年龄排序
.collect(Collectors.toList()); // 汇聚为一个List对象
// 按照姓名长度升序排序,按行输出:mike: 4、lucy: 4、jason: 5
persons.stream()
.sorted(Comparator.comparingInt(p -> p.getName().length()))
.map(Person::getName)
.map(name -> name + ": " + name.length())
.forEach(System.out::println);

2.2.6 map + anyMatch

// 判断是否存在名为jason的人:existed = true
boolean existed = persons.stream()
.map(Person::getName)
.anyMatch("jason"::equals); // 任意匹配项是否存在

2.2.7 groupingBy + map + reduce

// 将所有人按照性别分组并计数,输出:{female=1, male=2}
Map<String, Long> groupBySex = persons.stream().collect(groupingBy(Person::getGender, counting()));
System.out.println(groupBySex);
// 将所有人按照性别分组并计算各组最大年龄,输出:Person{name='mike'}
Map<String, Optional<Person>> groupBySexAge = persons.stream().collect(
groupingBy(Person::getGender, maxBy(Comparator.comparingInt(Person::getAge))));
System.out.println(groupBySexAge.get("male").get());
// 将所有人按照性别分组,按行输出:female: lucy、male: mike,jason
persons.stream().collect(groupingBy(Person::getGender))
.forEach((k, v) ->System.out.println(k + ": "
+ v.stream().map(Person::getName)
.reduce((x, y) -> x + "," + y).get()));

注意,本例采用import static java.util.stream.Collectors.*;这种静态导入的方式简化Collectors.groupingBy()的调用,代码更简洁易读。此外,不推荐示例中forEach()的用法。

2.2.8 maps + collect

// 计算身高比例分布:agePercentages = [52.63%, 21.05%, 26.32%]
List<String> agePercentages = persons.stream()
.mapToInt(Person::getAge) // 将Person对象映射为年龄整型值
.mapToDouble(age -> age / (double)totalAge * 100) // 计算年龄比例
.mapToObj(new DecimalFormat("##.00")::format) // DoubleStream -> Stream<String>
.map(percentage -> percentage + "%") // 添加百分比后缀 .collect(Collectors.toList());
// 若元素数目较多,可先定义formator = new DecimalFormat("##.00"),再调用mapToObj(formator::format)

2.2.9 flatMap

flatMap()将Stream中的集合实例内的元素全部拍平铺开,形成一个新的Stream,从而到达合并的效果。

// 传统写法(注意两层循环)
private static int countPrefix(List<List<String>> nested, String prefix) {
int count = 0;
for (List<String> element : nested) {
if (element != null) {
for (String str : element) {
if (str.startsWith(prefix)) {
count++;
}
}
}
}
return count;
}
// Stream写法
private static int countPrefixWithStream(List<List<String>> nested, String prefix) {
return (int) nested.stream()
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.filter(str -> str.startsWith(prefix))
.count();
} List<List<String>> lists = Arrays.asList(
Arrays.asList("Jame"),
Arrays.asList("Mike", "Jason"),
Arrays.asList("Jean", "Lucy", "Beth")
);
System.out.println("以J开头的人名数:" + countPrefixWithStream(lists, "J"));

三、规则总结

使用Stream时,需注意以下规则:

  1. 避免重用Stream。

    Java 8 Stream一旦被Terminal操作消费,将不能够再使用,必须为待执行的每个Terminal操作创建新的Stream链。在实际开发时,将共用的Stream实例定义为成员变量时,尤其容易犯错。

    重用Stream将报告stream has already been operated upon or closed的异常。

    若需要多次调用,可利用Stream Supplier实例来创建已构建所有中间操作的新Stream。例如:

    Supplier<Stream<String>> streamSupplier =
    () -> Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> s.startsWith("a"));
    streamSupplier.get().anyMatch(s -> true); // 每次调用get()构造一个新stream
    streamSupplier.get().noneMatch(s -> true);

    注意,anyMatch()方法接受Predicate引元,通常无需使用filter,此处仅为示例方便。

  2. 避免创建无限流。

    通过iterate或生成器创建Stream时,应配合limit()使用,以控制Stream大小。

    distinct()limit()共用时,应特别注意去重后元素数目是否满足limit限制。例如:

    IntStream.iterate(0, i -> (i + 1) % 2) // 生成0和1的整数序列
    .distinct() // 去重后为0和1两个元素
    .limit(10) // limit(10)限制得不到满足,从而变成无限流
    .forEach(System.out::println);
  3. 注意Stream操作顺序,尽可能提前通过filter()等操作降低数据规模

    以下面一段简单的代码为例:

    Stream.of("a1", "b2", "c3", "d4", "e5").map(s -> {
    System.out.println("map: " + s);
    return s.toUpperCase();
    }).filter(s -> {
    System.out.println("filter: " + s);
    return s.startsWith("A");
    }).forEach(s -> System.out.println("forEach: " + s));

    运行输出如下:

    map: a1
    filter: A1
    forEach: A1
    map: b2
    filter: B2
    map: c3
    filter: C3
    map: d4
    filter: D4
    map: e5
    filter: E5

    可见,流中的每个字符串都被调用5次map()filter(),而forEach()只调用一次。

    再改变操作顺序,将filter()移到Stream操作链的头部:

    Stream.of("a1", "b2", "c3", "d4", "e5").filter(s -> {
    System.out.println("filter: " + s);
    return s.startsWith("a");
    }).map(s -> {
    System.out.println("map: " + s);
    return s.toUpperCase();
    }).forEach(s -> System.out.println("forEach: " + s));

    运行输出如下:

    filter: a1
    map: a1
    forEach: A1
    filter: b2
    filter: c3
    filter: d4
    filter: e5

    可见,map()只被调用一次。虽然Stream惰性计算的特性使得操作顺序并不影响最终结果,但合理地安排顺序可以减少实际执行次数。数据规模较大时,性能会有较明显的提升。

  4. 注意Stream操作的副作用。

    大多数Stream操作必须是无干扰、无状态的。

    “无干扰”是指在流操作的过程中,不去修改流的底层数据源。例如,遍历流时不能通过添加或删除集合中的元素来修改集合。

    “无状态”是指Lambda表达式的结果不能依赖于流管道执行过程中,可能发生变化的外部作用域的任何可变变量或状态。

    以下代码试图在操作流时添加和移出元素,运行时均会抛出java.util.ConcurrentModificationException异常:

    List<String> strings = new ArrayList<>(Arrays.asList("one", "two"));
    String concatenatedString = strings.stream()
    // 不要这样做,干扰发生在这里
    .peek(s -> strings.add("three"))
    .reduce((a, b) -> a + " " + b)
    .get();
    List<Integer> list = IntStream.range(0, 10)
    .boxed() // 流元素装箱为Integer类型
    .collect(Collectors.toCollection(ArrayList::new));
    list.stream()
    .peek(list::remove) // 不要这样做,干扰发生在这里
    .forEach(System.out::println);

    以下代码对并行Stream使用了有状态的Lambda表达式:

    Integer[] intArray = {1, 2, 3, 4, 5, 6, 7, 8};
    List<Integer> listOfIntegers = new ArrayList<>(Arrays.asList(intArray));
    List<Integer> parallelStorage = new ArrayList<>();
    //List<Integer> parallelStorage = Collections.synchronizedList(new ArrayList<>());
    listOfIntegers.parallelStream()
    // 不要这样做,此处使用了有状态的Lambda表达式
    .map(e -> { parallelStorage.add(e); return e; })
    .forEachOrdered(e -> System.out.print(e + " "));
    System.out.println(": 1st");
    parallelStorage.stream().forEachOrdered(e -> System.out.print(e + " "));
    System.out.println(": 2nd");

    运行结果可能出现以下几种:

    // 并行执行流时,map()添加元素的顺序和随后的forEachOrdered()元素打印顺序不同
    1 2 3 4 5 6 7 8 : 1st
    1 6 3 2 7 8 5 4 : 2nd
    // 多线程可能同时读取到相同的下标n进行赋值,导致元素数量少于预期(采用synchronizedList可解决该问题)
    1 2 3 4 5 6 7 8 : 1st
    1 5 8 3 6 : 2nd

    《Effective Java 第三版》中指出,不要尝试并行化流管道,除非有充分的理由相信它将保持计算的正确性并提高其速度。 不恰当地并行化流的代价可能是程序失败或性能灾难。

  5. 避免过度使用Stream,否则可能使代码难以阅读和维护。

    常见的问题是Lambda表达式过长,可通过抽取方法等手段,尽量将Lambda表达式限制在几行之内。

Java 8 Stream API的使用示例的更多相关文章

  1. Stream API的代码示例.md

    一.代码实例: package com.TestMain; import com.alibaba.fastjson.JSON; import java.util.*; import java.util ...

  2. Java 8 Stream API详解--转

    原文地址:http://blog.csdn.net/chszs/article/details/47038607 Java 8 Stream API详解 一.Stream API介绍 Java8引入了 ...

  3. Java 8 Stream API Example Tutorial

    Stream API Overview Before we look into Java 8 Stream API Examples, let’s see why it was required. S ...

  4. Java 8 Stream API

    Java 8 Stream API JDK8 中有两大最为重要的改变.第一个是 Lambda 式:另外 Stream API(java.util.stream.*) Stream 是 JDK8 中处理 ...

  5. Java 8 Stream API具体解释

    Java 8 Stream API具体解释 一.Stream API介绍 Java 8引入了全新的Stream API,此Stream与Java I/O包里的InputStream和OutputStr ...

  6. Java 8 Stream Api 中的 peek 操作

    1. 前言 我在Java8 Stream API 详细使用指南[1] 中讲述了 [Java 8 Stream API]( "Java 8 Stream API") 中 map 操作 ...

  7. Spring WebFlux 学习笔记 - (一) 前传:学习Java 8 Stream Api (3) - Stream的终端操作

    Stream API Java8中有两大最为重要的改变:第一个是 Lambda 表达式:另外一个则是 Stream API(java.util.stream.*). Stream 是 Java8 中处 ...

  8. Spring WebFlux 学习笔记 - (一) 前传:学习Java 8 Stream Api (2) - Stream的中间操作

    Stream API Java8中有两大最为重要的改变:第一个是 Lambda 表达式:另外一个则是 Stream API(java.util.stream.*). Stream 是 Java8 中处 ...

  9. Spring WebFlux 学习笔记 - (一) 前传:学习Java 8 Stream Api (1) - 创建 Stream

    影子 在学习Spring WebFlux之前,我们先来了解JDK的Stream,虽然他们之间没有直接的关系,有趣的是 Spring Web Flux 基于 Reactive Stream,他们中都带了 ...

随机推荐

  1. <转> 二分图多重匹配问题

    在二分图最大匹配中,每个点(不管是X方点还是Y方点)最多只能和一条匹配边相关联,然而,我们经常遇到这种问题,即二分图匹配中一个点可以和多条匹配边相关联,但有上限,或者说,Li表示点i最多可以和多少条匹 ...

  2. 关于OPENSSL的EVP函数的使用

    4月份没什么做,就是做了OPENSSL的 加密和解密的应用,现在公开一下如何调用OPENSSL对字符串进行加密和解密,当中也学会了对加密数据进行BASE64编码,现在公开一下代码,在这感谢GITHUB ...

  3. noip模拟赛 星空

    分析:非常神的一道题.迭代加深搜索+rand可以骗得20分.状压n的话只有24分,必须对问题进行一个转化. 在爆搜的过程中,可以利用差分来快速地对一个区间进行修改,把一般的差分改成异或型的差分: b[ ...

  4. cdq分治入门--BZOJ1492: [NOI2007]货币兑换Cash

    n<=100000天,一开始有s块钱,每天股票A价格ai,B价格bi,每天可以做的事情:卖出股票:按A:B=RTi的比例买入股票.问最后的最大收益.股票可以为浮点数,答案保留三位. 用脚指头想想 ...

  5. NOIP2013提高组D2T3 华容道

    n<=30 * m<=30 的地图上,0表示墙壁,1表示可以放箱子的空地.q<=500次询问,每次问:当空地上唯一没有放箱子的空格子在(ex,ey)时,把位于(sx,sy)的箱子移动 ...

  6. msp430入门编程00

    msp430单片机最小系统 msp430入门学习 msp430入门编程

  7. test markdown 写博客

    欢迎使用 Cmd Markdown 编辑阅读器 我们理解您需要更便捷更高效的工具记录思想,整理笔记.知识,并将其中承载的价值传播给他人,Cmd Markdown 是我们给出的答案 -- 我们为记录思想 ...

  8. ArcEngine 打开AutoCAD文件的几种方法

    方法一. IWorkspaceFactory pWorkspaceFactory; IFeatureWorkspace pFeatureWorkspace; IFeatureLayer pFeatur ...

  9. Codeforces Round #271 (Div. 2) D. Flowers (递推 预处理)

    We saw the little game Marmot made for Mole's lunch. Now it's Marmot's dinner time and, as we all kn ...

  10. WebForms UnobtrusiveValidationMode 须要“jquery”ScriptResourceMapping。

    一.问题产生的背景: 在敲牛腩新闻公布系统的后台登录页面的时候,我们用到了RequiredFieldValidator控件(验证非空控件),该控件的作用是禁止输入规定的内容,RequiredField ...