Java8新特性-Stream

Stream

最近疯狂补习 Java8 的特性, 今天学习了一下 Stream.

基本上是跟着廖雪峰官网的教程来学习的, 自己边看别写,将 Stream 整理了一下

原文链接: https://www.liaoxuefeng.com/wiki/1252599548343744/1322402873081889

介绍

Java8 引入了 Lambda 表达式,还引入了流式 API: Stream API

位于 java.util.stream 包

特点:

  • 提供了一套新的流式处理的抽象序列
  • 支持函数式变成和链式操作
  • 可以表示无限序列,并且大多数情况下是惰性求值的
    • 元素可能全部存储在内存,也有可能需要实时计算
    • 惰性计算: 真正的计算通常发生在最后的结果的获取

创建 Stream

多种方法

Stream.of()

是一个静态方法, 传入可变参数

1
2
Stream<String> stream = Stream.of("A","B");
stream.forEach(System.out.println);

方便测试

基于数组或 Collection

1
2
3
4
Stream<String> stream1 = Arrays.stream(new String[] {"A","B","C"});
Stream<String> stream2 = List.of("X","Y","Z").stream();
stream1.forEach(System.out.println);
stream2.forEach(System.out.println);

对于 Collection(List, Set, Queue...), 直接调用 stream()方法就可以获得 Stream

基于 Supplier

还可以通过Stream.generate()方法, 需要传入一个Supplier对象

Stream<String> s = Stream.generate(Supplier<String>sp);

基于Supplier创建的Stream会不断调用Supplier.get()方法来不断产生下一个元素, 这种Stream保存的不是元素, 而是算法, 可以用来表示无限序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class StreamTest {

public static void main(String[] args) {
Stream<Integer> natural = Stream.generate(new NatualSupplier());
natural.limit(20).forEach(System.out::println);
}
}

class NatualSupplier implements Supplier<Integer>{
int n = 0;

@Override
public Integer get() {
n++;
return n;
}
}

Supplier<Integer> 模拟了一个无限序列, 如果用 List 表示,即便在 int 范围内,也会占用很大内存, 而Stream几乎不占用空间,因为每个元素都是实时计算出来的,用的时候再算

其他方法

通过一些 API 提供的接口

基本类型

因为 Java 的泛型不支持基本类型

为了避免繁复的装箱拆箱,Java 提供了 IntStream, LongStream, DoubleStream 这三种使用基本类型的 Stream

1
2
IntStream is = Arrays.stream(new int[]{1,2,3});
LongStream ls = List.of("1","2","3").stream().mapToLong(Long::parseLong);

示例

利用 Stream 实现输出斐波那契序列:

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
// 方法一
Stream.iterate(new long[]{0, 1}, a -> new long[]{a[1], a[0] + a[1]})
.limit(100)
.map(a -> a[0] + ",")
.forEach(System.err::print);

// 方法二
public static void fi2(){
IntSupplier is = new IntSupplier() {
int pre = 0;
int current = 1;
@Override
public int getAsInt() {
int p = pre;
int next = pre+current;
pre = current;
current = next;
return p;
}
};
IntStream.generate(is).limit(10).forEach(System.out::println);
}

// 方法三
public static void fibonacci1() {
// 生成 整形数组,在通过flatmap 转换成一个集合输出
Stream.iterate(new Integer[]{0, 1}, t -> new Integer[]{t[0] + t[1], t[0] + t[1] + t[1]}) // 1
.flatMap(Arrays::stream) // flatMap 数据, 将数组元素转化成stream //2
.limit(10) // 默认是无线长度,所以要给出限制
.forEach(System.out::println);
}

常见操作

操作分为两类:

  • 转换操作: 把一个 Stream 转换为另一个 Stream; 不会触发任何计算
  • 聚合操作: 对 Stream 的每个元素进行计算,得到一个确定的结果; 立刻计算

map

Stream.map()Stream最常用的一个转换方法,它把一个Stream转换为另一个Stream

所谓map操作,就是把一种操作运算,映射到一个序列的每一个元素上。

map()方法接收一个Function接口对象, 其定义了一个apply()方法, 将 T 类型转换为 R 类型

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
List.of(" Apple ", " pear ", " ORANGE", " BaNaNa ")
.stream()
.map(String::trim) // 去空格
.map(String::toLowerCase) // 变小写
.forEach(System.out::println); // 打印
}
}

filter

所谓filter()操作,就是对一个Stream的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Stream

1
2
3
IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.filter(n -> n % 2 != 0)
.forEach(System.out::println);

reduce

map()filter()都是Stream的转换方法,而Stream.reduce()则是Stream的一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果。

1
2
3
// 累加
int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (acc, n) -> acc + n);
System.out.println(sum); // 47

reduce 是聚合方法,聚合方法会立刻对 Stream 进行计算

输出为 List

把 Stream 转换为 List 也是一个聚合操作,它会强制 Stream 输出每个元素

方法是调用collect()并传入Collectors.toList()对象

它实际上是一个Collector实例,通过类似reduce()的操作,把每个元素添加到一个收集器中(实际上是ArrayList)。

1
2
3
4
Stream<String> stream = Stream.of("Apple", "", null, "Pear", "Orange");
List<String> collect = stream.filter(s -> s != null && !s.isEmpty())
.collect(Collectors.toList());
System.out.println(collect);

类似的,collect(Collectors.toSet())可以把Stream的每个元素收集到Set中。

输出为数组

把Stream的元素输出为数组和输出为List类似,我们只需要调用toArray()方法,并传入数组的“构造方法”:

1
2
List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);

注意到传入的“构造方法”是String[]::new,它的签名实际上是IntFunction<String[]>定义的String[] apply(int),即传入int参数,获得String[]数组的返回值。

输出为Map

如果我们要把Stream的元素收集到Map中,就稍微麻烦一点。因为对于每个元素,添加到Map时需要key和value,因此,我们要指定两个映射函数,分别把元素映射为key和value:

1
2
3
4
5
6
7
8
Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
Map<String, String> map = stream
.collect(Collectors.toMap(
// 把元素s映射为key:
s -> s.substring(0, s.indexOf(':')),
// 把元素s映射为value:
s -> s.substring(s.indexOf(':') + 1)));
System.out.println(map);

分组输出

使用Collectors.groupingBy()

提供两个函数,一个是分组的key,这里使用s -> s.substring(0, 1),表示只要首字母相同的String分到一组,第二个是分组的value,这里直接使用Collectors.toList(),表示输出为List

1
2
3
4
5
6
7
List<String> list = List.of("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
Map<String, java.util.List<String>> groups = list.stream()
.collect(Collectors.groupingBy(
s -> s.substring(0, 1),
Collectors.toList()
));
System.out.println(groups);

运行结果为:

1
2
3
4
5
{
A=[Apple, Avocado, Apricots],
B=[Banana, Blackberry],
C=[Coconut, Cherry]
}

示例

按照对象的某一个字段分组

现有对象 Student 为:

1
2
3
4
5
6
7
8
@Data
@AllArgsConstructor
public class Student {
int gradeId; // 年级
int classId; // 班级
String name; // 名字
int score; // 分数
}

按照 gradeId 分组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Stream<Student> studentStream = Stream.of(new Student(1, 1, "A", 80),
new Student(1, 2, "B", 90),
new Student(1, 3, "C", 100),
new Student(2, 1, "D", 90),
new Student(2, 2, "E", 100),
new Student(2, 3, "F", 80),
new Student(3, 1, "G", 100),
new Student(3, 2, "H", 80),
new Student(3, 3, "I", 90));
Map<Integer, List<Student>> map = studentStream.collect(
Collectors.groupingBy(
s -> s.gradeId,
Collectors.toList()
)
);
System.out.println(map);

输出结果为:

1
{1=[Student(gradeId=1, classId=1, name=A, score=80), Student(gradeId=1, classId=2, name=B, score=90), Student(gradeId=1, classId=3, name=C, score=100)], 2=[Student(gradeId=2, classId=1, name=D, score=90), Student(gradeId=2, classId=2, name=E, score=100), Student(gradeId=2, classId=3, name=F, score=80)], 3=[Student(gradeId=3, classId=1, name=G, score=100), Student(gradeId=3, classId=2, name=H, score=80), Student(gradeId=3, classId=3, name=I, score=90)]}

其他操作

排序

1
2
3
4
List<String> list = List.of("Orange", "apple", "Banana")
.stream()
.sorted()
.collect(Collectors.toList());

去重

1
2
3
4
List.of("A", "B", "A", "C", "B", "D")
.stream()
.distinct()
.collect(Collectors.toList()); // [A, B, C, D]

截取

1
2
3
4
5
List.of("A", "B", "C", "D", "E", "F")
.stream()
.skip(2) // 跳过A, B
.limit(3) // 截取C, D, E
.collect(Collectors.toList()); // [C, D, E]

合并

1
2
3
4
5
Stream<String> s1 = List.of("A", "B", "C").stream();
Stream<String> s2 = List.of("D", "E").stream();
// 合并:
Stream<String> s = Stream.concat(s1, s2);
System.out.println(s.collect(Collectors.toList())); // [A, B, C, D, E]

flatMap

是把 Stream 的每个元素映射为 Stream,然后合并成一个新的 Stream

如果Stream的元素是集合:

1
2
3
4
Stream<List<Integer>> s = Stream.of(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9));

而我们希望把上述Stream转换为Stream<Integer>,就可以使用flatMap()

1
Stream<Integer> i = s.flatMap(list -> list.stream());

并行

通常情况下,对Stream的元素进行处理是单线程的,即一个一个元素进行处理。但是很多时候,我们希望可以并行处理Stream的元素,因为在元素数量非常大的情况,并行处理可以大大加快处理速度。

把一个普通Stream转换为可以并行处理的Stream非常简单,只需要用parallel()进行转换:

1
2
3
4
Stream<String> s = ...
String[] result = s.parallel() // 变成一个可以并行处理的Stream
.sorted() // 可以进行并行排序
.toArray(String[]::new);

经过parallel()转换后的Stream只要可能,就会对后续操作进行并行处理。我们不需要编写任何多线程代码就可以享受到并行处理带来的执行效率的提升。

其他聚合方法

除了reduce()collect()外,Stream还有一些常用的聚合方法:

  • count():用于返回元素个数;
  • max(Comparator<? super T> cp):找出最大元素;
  • min(Comparator<? super T> cp):找出最小元素。

针对IntStreamLongStreamDoubleStream,还额外提供了以下聚合方法:

  • sum():对所有元素求和;
  • average():对所有元素求平均数。

还有一些方法,用来测试Stream的元素是否满足以下条件:

  • boolean allMatch(Predicate<? super T>):测试是否所有元素均满足测试条件;
  • boolean anyMatch(Predicate<? super T>):测试是否至少有一个元素满足测试条件。

最后一个常用的方法是forEach(),它可以循环处理Stream的每个元素,我们经常传入System.out::println来打印Stream的元素:

1
2
3
4
Stream<String> s = ...
s.forEach(str -> {
System.out.println("Hello, " + str);
});

小结

Stream提供的常用操作有:

转换操作:map()filter()sorted()distinct()

合并操作:concat()flatMap()

并行处理:parallel()

聚合操作:reduce()collect()count()max()min()sum()average()

其他操作:allMatch(), anyMatch(), forEach()