Java8 Stream API

概述

Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。

Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。

而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架(JSR166y)来拆分任务和加速处理过程。

Stream 的另外一大特点是,数据源本身可以是无限的。

获取一个数据源(source)→ 数据转换 → 执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道,如下图所示。

流的操作类型

流的操作类型分为两种:

  • Intermediate :一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
  • Terminal :一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

生成 Stream Source 的几种方式

Stream

1
2
3
4
5
6
7
8
9
10
11
12
java.util.stream.Stream.empty(T... values)
java.util.stream.Stream.of(T t)
java.util.stream.Stream.of(T... values)
java.util.stream.Stream.iterate(final T seed, final UnaryOperator<T> f)
java.util.stream.Stream.builder().build()
java.util.stream.Stream.concat(Stream<? extends T> a, Stream<? extends T> b)
java.util.stream.Stream.generate(Supplier<T> s)

// 以上大多数方法同样适用于
java.util.stream.IntStream
java.util.stream.LongStream
java.util.stream.DoubleStream

Collection

1
2
java.util.Collection.stream()
java.util.Collection.parallelStream()

Arrays

1
java.util.Arrays.stream(T[] array)

Reader

1
java.io.BufferedReader.lines()

Files

1
2
3
4
5
6
java.nio.file.Files.list(Path dir)
java.nio.file.Files.walk(Path start, int maxDepth, FileVisitOption... options)
java.nio.file.Files.walk(Path start, FileVisitOption... options)
java.nio.file.Files.find(Path start, int maxDepth, BiPredicate<Path, BasicFileAttributes> matcher, FileVisitOption... options)
java.nio.file.Files.lines(Path path, Charset cs)
java.nio.file.Files.lines(Path path)

Random

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java.util.Random.ints()
java.util.Random.ints(long streamSize)
java.util.Random.ints(int randomNumberOrigin, int randomNumberBound)
java.util.Random.ints(long streamSize, int randomNumberOrigin, int randomNumberBound)

java.util.Random.longs()
java.util.Random.longs(long streamSize)
java.util.Random.longs(long randomNumberOrigin, long randomNumberBound)
java.util.Random.longs(long streamSize, long randomNumberOrigin, long randomNumberBound)

java.util.Random.doubles()
java.util.Random.doubles(long streamSize)
java.util.Random.doubles(double randomNumberOrigin, double randomNumberBound)
java.util.Random.doubles(long streamSize, double randomNumberOrigin, double randomNumberBound)

其他

1
2
3
java.util.BitSet.stream()
java.util.regex.Pattern.splitAsStream(final CharSequence input)
java.util.jar.JarFile.stream()

Stream 常用操作举例

准备工作

首先创建 User 实体类

1
2
3
4
5
6
7
8
9
@Data
@AllArgsConstructor
public class User {

private Long id;
private String name;
private Integer age;
private BigDecimal money;
}

然后添加一些测试数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<User> userList=new ArrayList<>();

User user1=new User(1L,"user1",15, BigDecimal.valueOf(100.55));
User user2=new User(2L,"user2",16, BigDecimal.valueOf(200.14));
User user3=new User(3L,"user3",17, BigDecimal.valueOf(300.00));
User user4=new User(4L,"user4",18, BigDecimal.valueOf(400.89));
User user5=new User(5L,"user5",19, BigDecimal.valueOf(500.66));

userList.add(user1);
userList.add(user2);
userList.add(user3);
userList.add(user4);
userList.add(user5);

Stream<User> userStream = userList.stream();

排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void example(List<User> userList) {
List<User> users;

// 正序
users = userList.stream().sorted(Comparator.comparing(User::getAge))
.collect(Collectors.toList());
users.forEach(System.out::println);

// 倒序
users = userList.stream().sorted(Comparator.comparing(User::getAge).reversed())
.collect(Collectors.toList());
users.forEach(System.out::println);

// 多重排序
users = userList.stream().sorted(
Comparator.comparing(User::getAge)
.thenComparing(User::getId).reversed()
)
.collect(Collectors.toList());
users.forEach(System.out::println);
}

转换

1
2
3
4
5
6
7
8
9
10
11
public static void example(List<User> userList) {
List<String> names;

// 可以自由对 user 做转换
names = userList.stream().map(user -> user.getName()).collect(Collectors.toList());
names.forEach(System.out::println);

// 上面写法可以简写如下
names = userList.stream().map(User::getName).collect(Collectors.toList());
names.forEach(System.out::println);
}

处理

注意 peek 与 map 的区别是:
peek 操作 一般用于不想改变流中元素本身的类型或者只想操作元素的内部状态时;
而 map 则用于改变流中元素本身类型,即从元素中派生出另一种类型的操作。

1
2
3
4
5
6
7
public static void example(List<User> userList) {
List<String> names;

// 可以自由对 user 属性做修改
names = userList.stream().peek(user -> user.setName("小名")).collect(Collectors.toList());
names.forEach(System.out::println);
}

分组

可以针对对象的某一属性进行分组,比如根据年龄分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void example(List<User> userList) {
// lambda 表达式的返回值作为 key, List<User> 作为 value

//根据年龄转换为 map
Map<Integer, List<User>> map1 = userList.stream().collect(Collectors.groupingBy(User::getAge));

// 根据年龄段转换为 map
Map<String, List<User>> map2 = userList.stream().collect(Collectors.groupingBy(user -> {
if (user.getAge() <= 17) {
return "少年";
} else if (user.getAge() <= 40) {
return "青年";
} else if (user.getAge() <= 65) {
return "中年";
} else {
return "老年";
}
}));
}

转 Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void example(List<User> userList) {
Map<Integer, User> map;

map = userList.stream().collect(Collectors.toMap(user -> user.getAge(), user -> user));
// 上面写法可以简写如下
map = userList.stream().collect(Collectors.toMap(User::getAge, user -> user));

// 需要注意的是,以上写法可能会遇到 key 冲突问题
// 这里当 key 冲突时舍弃 id 小的, 取 id 大的
map = userList.stream().collect(Collectors.toMap(User::getAge, user -> user, (user1, user2) -> {
if (user1.getId() > user2.getId()) {
return user1;
}
return user2;
}));
}

过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void example(List<User> userList) {
List<User> users;

users = userList.stream().filter(user -> user.getAge() >= 18)
.collect(Collectors.toList());
users.forEach(System.out::println);

// 可以进行展开对 user 做复杂判断
users = userList.stream().filter(user -> {
if (user.getAge() > 60) {
return true;
}
return false;
}).collect(Collectors.toList());
users.forEach(System.out::println);
}

去重

1
2
3
4
5
6
7
public static void example(List<User> userList) {
// 根据 id 去重
List<User> unique = userList.stream().collect(
Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(User::getId))), ArrayList::new)
);
}

求和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void example(List<User> userList) {
// int 类型
int sumAge = userList.stream().mapToInt(User::getAge).sum();

// 对于高精度数值求和, 可以使用 reduce
Optional<BigDecimal> decimalOptional = userList.stream().map(User::getMoney)
.reduce(BigDecimal::add);

if (decimalOptional.isPresent()) {
BigDecimal sumMoney = decimalOptional.get();
System.out.println(sumMoney);
} else {
throw new IllegalArgumentException("参数错误: money 不能为 null");
}

// 设置初始值可以避免返回 Optional
BigDecimal sumMoney = userList.stream().map(User::getMoney)
.reduce(BigDecimal.ZERO, BigDecimal::add);
System.out.println(sumMoney);

}

查找最大/小值

1
2
3
4
5
6
7
public static void example(List<User> userList) {
Optional<User> maxAgeUser = userList.stream().max(Comparator.comparing(User::getAge));
maxAgeUser.ifPresent(System.out::println);

Optional<User> minAgeUser = userList.stream().min(Comparator.comparing(User::getAge));
minAgeUser.ifPresent(System.out::println);
}

取交集/并集/差集/去重并集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void example() {
List<Integer> list1 = Arrays.asList(1, 5, 6, 8, 9, 10);
List<Integer> list2 = Arrays.asList(2, 5, 6, 7, 9);

// 一般有filter 操作时,不用并行流parallelStream ,如果用的话可能会导致线程安全问题

List<Integer> list;
// 交集 拓展:list2里面如果是对象,则需要提取每个对象的某一属性组成新的list,多个条件则为多个list
list = list1.stream().filter(item -> list2.contains(item)).collect(Collectors.toList());
// 上面可简写如下
list = list1.stream().filter(list2::contains).collect(Collectors.toList());
System.out.println("---得到交集 intersection---");
list.parallelStream().forEach(System.out::println);

// 差集 (list1 - list2) 同上拓展
list = list1.stream().filter(item -> !list2.contains(item)).collect(Collectors.toList());
System.out.println("---得到差集 reduce (list1 - list2)---");
list.parallelStream().forEach(System.out::println);
}