Java8中的StreamAPI

你还在为大批量数据操作而发愁吗? 你还在因看到层层for循环而感到膈应吗? 你还在为写并行代码频频出bug而懊恼吗? 快去使用Java8的Stream吧.

为啥需要用Stream

  1. 方便, 高效的处理集合对象
  2. 处理过程更加清晰, 提高可读性.
  3. 配合Lambda表达式, 少写代码, 提高编程效率.
  4. 方便编写高性能的并发程序.

应用场景

Java7写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
List<UserInfo> userInfos = new ArrayList<>();
userInfos.add(new UserInfo(1, "1", true, 1d));
userInfos.add(new UserInfo(2, "2", false, 2d));
userInfos.add(new UserInfo(3, "3", true, 3d));
userInfos.add(new UserInfo(4, "4", false, 4d));

// 过滤
Iterator<UserInfo> iterator = userInfos.iterator();
while (iterator.hasNext()) {
UserInfo userinfo = iterator.next();
if (userinfo.isSex()) {
iterator.remove();
}
}
// 排序
userInfos.sort(new Comparator<UserInfo>() {
@Override
public int compare(UserInfo o1, UserInfo o2) {
if (o1.getScore() > o2.getScore()) {
return 1;
} else if (o1.getScore() < o2.getScore()) {
return -1;
} else {
return 0;
}
}
});
// 取id
List<Integer> ids = new ArrayList<>(userInfos.size());
for (UserInfo userInfo : userInfos) {
ids.add(userInfo.getId());
}
// use ids do something

Java8写法

1
2
3
4
5
6
7
8
9
10
11
12
List<UserInfo> userInfos = new ArrayList<>();
userInfos.add(new UserInfo(1, "1", true, 1d));
userInfos.add(new UserInfo(2, "2", false, 2d));
userInfos.add(new UserInfo(3, "3", true, 3d));
userInfos.add(new UserInfo(4, "4", false, 4d));

List<Integer> ids = userInfos.stream()
.filter(s -> !s.isSex()) //过滤
.sorted(Comparator.comparingDouble(UserInfo::getScore)) //排序
.map(UserInfo::getId) // 取id
.collect(Collectors.toList());
// use ids do something

简单的对比可以看出两者的不同, 当然是stream更胜一筹!!!

Stream详细介绍

Stream有三部分组成, 分别是数据源, 中间操作, 结束操作.

数据源生成

数据源生成常用的有如下方式:

  • 从 Collection 数组
    • Collection.stream()
    • Collection.parallelStream()
    • Arrays.stream(T array) 或者 Stream.of()
  • 从 BufferdReader
    • java.io.BufferedReader.lines()
  • 静态工厂
    • java.util.stream.IntStream.range()
    • java.nio.file.Files.walk()
  • 其他
    • Random.ints()
    • BitSet.stream()
    • Pattern.splitAsStream(java.lang.CharSequence)
    • JarFile.stream()
  • 等等

中间操作

Stream的中间操作只是一个标记, 一种声明, 只有在结束操作的时候才会进行真正的计算, 中间操作有两种分类:

  • 无状态:元素的处理不受前面元素的影响
    • unordered()
    • filter()
    • map()
    • mapToInt()
    • mapToLong()
    • mapToDouble()
    • flatMap()
    • flatMapToInt()
    • flatMapToLong()
    • flatMapToDouble()
    • peek()
  • 有状态:必须等到所有元素处理后才会知道最后结果
    • disinct()
    • sorted()
    • limit()
    • skip()

结束操作

结束操作也有两种分类

  • 非短路操作:需要处理完全部元素才能知晓结果
    • forEach()
    • forEachOrdered()
    • toArray()
    • reduce()
    • collect()
    • max()
    • min()
    • count()
  • 短路操作:不用处理全部元素就可能会返回结果
    • anyMatch()
    • allMatch()
    • noneMatch()
    • findFirst()
    • findAny()

常用的api

map/flatMap

把输入流的元素, 映射成另外一种元素.

对象转字符串

1
2
3
4
5
List<String> collect = userInfos.stream().map(userInfo -> {
int i = userInfo.getId();
String name = userInfo.getName();
return name + "-" + i;
}).collect(Collectors.toList());

平方数

1
2
3
4
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
List<Integer> squareNums = nums.stream().
map(n -> n * n).
collect(Collectors.toList());

flatMap用于把有层级的结构扁平化处理.

多List扁平化输出

1
2
3
List<Integer> collect1 = Stream.of(Arrays.asList(1, 2, 3), Arrays.asList(4, 5, 6), Arrays.asList(7, 8, 9))
.flatMap(Collection::stream)
.collect(Collectors.toList());

filter

对元素进行过滤, 通过的元素会留下来生成一个新的Stream.

过滤奇数

1
2
3
4
Integer[] sixNums = {1, 2, 3, 4, 5, 6};
List<Integer> collect2 = Stream.of(sixNums)
.filter(n -> n % 2 == 1)
.collect(Collectors.toList());

forEach/peek

遍历每个元素

遍历打印

1
2
3
Integer[] sixNums = {1, 2, 3, 4, 5, 6};
Stream.of(sixNums)
.forEach(System.out::println);

forEach是结束操作, 无法重复执行, 而peek可以做到重复执行.

1
2
3
4
5
6
7
8
9
10
11
Integer[] sixNums = {1, 2, 3, 4, 5, 6};
Stream.of(sixNums)
.peek(s -> {
System.out.println("1-peek" + s);
})
.filter(n -> n % 2 == 1)
.peek(s -> {
System.out.println("2-peek" + s);
}).forEach(s -> {
System.out.println("forEach" + s);
});

返回内容为:

1
2
3
4
5
6
7
8
9
10
11
12
1-peek1
2-peek1
forEach1
1-peek2
1-peek3
2-peek3
forEach3
1-peek4
1-peek5
2-peek5
forEach5
1-peek6

findFirst

short-circuiting 操作, 总是返回第一个元素, 或者为空

1
2
Optional<Integer> first = Stream.of(sixNums).findFirst();
Integer integer = first.get();

它返回一个Optional对象, 使用 Optional 代码的可读性更好, 而且它提供的是编译时检查, 能极大的降低 NPE 这种 Runtime Exception 对程序的影响, 或者迫使程序员更早的在编码阶段处理空值问题, 而不是留到运行时再发现和调试.

reduce

主要是用来吧元素组合起来. 需要一个起始位置和一个组合规则, sum/min/max/average都是一种reduce.

求最大

1
2
3
4
5
6
7
8
Integer[] sixNums = {1, 7, 3, 4, 5, 6};
Integer reduce = Stream.of(sixNums).reduce(0, (a, b) -> {
if (a > b) {
return a;
} else {
return b;
}
});

limit/skip

limit 返回前n个元素; skip 扔掉前n个元素

1
2
List<Integer> collect = Stream.of(sixNums).limit(1).collect(Collectors.toList());
List<Integer> collect1 = Stream.of(sixNums).skip(1).collect(Collectors.toList());

注意: 放到sorted后面会让short-circuiting失效, stream会遍历完所有的元素.

sorted

对元素排序, 使用要注意顺序, 可以先进行limit/skip, 然后对较短的集合进行排序

1
Stream.of(sixNums).limit(3).sorted(Integer::compareTo).forEach(System.out::println);

min/max/distinct

找最小/最大/过滤重复

某个英文小说的词汇

1
2
3
4
5
6
7
8
9
10
BufferedReader br = new BufferedReader(new FileReader("c:\\SUService.log"));
List<String> words = br.lines().
flatMap(line -> Stream.of(line.split(" "))).
filter(word -> word.length() > 0).
map(String::toLowerCase).
distinct().
sorted().
collect(Collectors.toList());
br.close();
System.out.println(words);

Match

匹配某些元素, 有三种方式:

  1. allMatch 全部匹配返回true, 有一个不满足就会短路, 返回false.
  2. anyMatch 只要有一个则返回true, 有一个满足就会短路, 返回true.
  3. noneMatch 没有一个则返回true, 有一个满足就会短路, 返回false.

通过名字找人

1
2
3
4
5
6
7
8
9
List<UserInfo> userInfos = new ArrayList<>();
userInfos.add(new UserInfo(1, "1", true, 1d));
userInfos.add(new UserInfo(2, "2", false, 2d));
userInfos.add(new UserInfo(3, "3", true, 3d));
userInfos.add(new UserInfo(4, "4", false, 4d));

boolean b = userInfos.stream().anyMatch(a -> {
return "1".equals(a.getName());
});

自己生成流

Stream.generate

随机数

1
2
3
4
5
6
Random seed = new Random();
Supplier<Integer> random = seed::nextInt;
Stream.generate(random).limit(10).forEach(System.out::println);
//Another way
IntStream.generate(() -> (int) (System.nanoTime() % 100)).
limit(10).forEach(System.out::println);

Stream.iterate

等差数列

1
2
Stream.iterate(0, n -> n + 3).limit(10)
.forEach(x -> System.out.print(x + " "));.

用 Collectors 来进行 reduction 操作

grooupingBy/partitioningBy/toMap

对数据分组

1
2
3
4
5
6
// 按照名字分组
Map<String, List<UserInfo>> collect = userInfos.stream().collect(Collectors.groupingBy(UserInfo::getName));
// 按照性别分组
Map<Boolean, List<UserInfo>> collect1 = userInfos.stream().collect(Collectors.partitioningBy(UserInfo::isSex));
// 根据id分组
Map<Integer, UserInfo> collect2 = userInfos.stream().collect(Collectors.toMap(UserInfo::getId, userInfo -> userInfo));

并行处理

  • stream() 串行流
  • parallelStream() 并行流

串行流和并行流差别就是单线程和多线程的执行. 任务量小或者机器是单核的选用串行流; 任务量大且机器是多核的, 考虑使用串行流.

通过parallel()sequential()能够实现并行流和串行流之间的切换.

并行流的使用需要符合两个规则

  1. 初始值必须为组合函数的恒定值
  2. 组合操作必须符合结合律