前言

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. Light oj-1100 - Again Array Queries,又是这个题,上次那个题用的线段树,这题差点就陷坑里了,简单的抽屉原理加暴力就可以了,真是坑~~

                                                                              1100 - Again Array Queries ...

  2. windows开启远程

    windows开启远程桌面超级简单,跟linux相比太简单了. 补充:有瑕疵,应该是远程中的远程桌面属性打钩,但是W8.1没有这个选项,W7可以,其次创建一个管理员账户,身份是管理员,不是标准用户,要 ...

  3. 封装HttpURLConnection

    package com.pingyijinren.test; import java.io.BufferedReader; import java.io.InputStream; import jav ...

  4. [bzoj1188][HNOI2007]分裂游戏_博弈论

    分裂游戏 bzoj-1188 HNOI-2007 题目大意:题目链接. 注释:略. 想法: 我们发现如果一个瓶子内的小球个数是奇数才是有效的. 所以我们就可以将问题变成了一个瓶子里最多只有一个球球. ...

  5. VMware配置从U盘启动

    很遗憾,VMware的BIOS不能识别USB启动设备,即使已经把USB设备连接上去. 解决这一问题的做法是直接添加硬盘,硬盘指向物理硬盘,即USB设置. 注意:Ubuntu下要设置这一功能需要使用su ...

  6. 怎样删除Tomcat下已经部署的项目

    lz说的是把web项目部署到tomcat之中,要把它删除..很简单,找到webapps文件(tomcat的根目录)下把它删除即可.. 2.Tomcat 6.0\webapps\项目名 只要在把这个目录 ...

  7. java编程思想-多态

    java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是动态绑定即运行时绑定. public class test { private void ...

  8. hdu 5386 Cover (暴力)

    hdu 5386 Cover Description You have an matrix.Every grid has a color.Now there are two types of oper ...

  9. Redis缓存数据库安全加固指导(二)

    背景 在众多开源缓存技术中,Redis无疑是目前功能最为强大,应用最多的缓存技术之一,参考2018年国外数据库技术权威网站DB-Engines关于key-value数据库流行度排名,Redis暂列第一 ...

  10. Android lollipop 更新问题

    非常多朋友都说lollipop出来想试用一下,结果在网官下载的android studio 都是20版本号,也没有看见更新到android 5.0. 我也在网上狂了一下,收集到一个代理地址目測能够用, ...