JAVA8-Java8的流式编程-初级篇

本文最后更新于:May 13, 2023 pm

JAVA8 是一个有里程碑的一个版本,提供了很多的新特性。Java 8 是oracle公司于2014年3月发布,可以看成是自Java 5 以来最具革命性的版本。Java 8为Java语言、编译器、类库、开发工具与JVM带来了大量新特性。

目录

预知

流的中间操作:如果Stream只有中间操作是不会执行的,当执行终端操作的时候才会执行中间操作,这种方式称为延迟加载或惰性求值。多个中间操作组成一个中间操作链,只有当执行终端操作的时候才会执行一遍中间操作链。

状态操作

  • 无状态操作:所谓无状态就是一次操作,不能保存数据,线程安全。如:filter、map、flatMap、peek 、unordered属于无状态操作。这些操作只是从输入流中获取每一个元素,并且在输出流中得到一个结果,元素和元素之间没有依赖关系,不需要存储数据。
  • 有状态操作:所谓有状态就是有数据存储功能,线程不安全。如:distinct、sorted、limit、skip属于有状态操作。这些操作需要先知道先前的历史数据,而且需要存储一些数据,元素之间有依赖关系。

创建流

流有两种形式,单列和双列。

单列流

主要是对于集合而言。

1
2
List<Integer> list = new ArrayList<>();
Stream<Integer> stream = list.stream();

数组流

主要是对于数组而言。有两种创建方式。

1
2
3
Integer[] a = {23,23,32,23,3,23};
Stream<Integer> stream = Arrays.stream(a);
Stream<Integer> a1 = Stream.of(a);

双列流

先转化为单列集合再创建流。

1
2
3
4
5
Map<String,Integer> map = new HashMap<>();
map.put("aaaa",13);
map.put("bbbb",23);
map.put("cccc",33);
Stream<Map.Entry<String, Integer>> stream = map.entrySet().stream();

中间操作

distinct(去重)

通过流所生成元素的 hashCode() 和 equals() 去除重复元素的Stream。

1
Stream<Stu> distinct = stus.stream().distinct();

filter(过滤)

对Stream中包含的元素使用给定的过滤函数进行过滤操作,新生成的Stream只包含符合条件的元素。

1
Stream<Stu> stuStream = stus.stream().filter(it -> it.getAge() > 20);

括号内为过滤条件。

sorted(排序)

通过比较器指定比较规则进行排序。

1
2
Stream<Stu> sorted = stus.stream().sorted(Comparator.comparing(Stu::getName)); //升序
Stream<Stu> sorted = stus.stream().sorted(Comparator.comparing(Stu::getName, Comparator.reverseOrder())); // 降序

limit(截断)

截断流,使其元素不超过给定数量。如果元素的个数小于maxSize,那就获取所有元素。

1
Stream<Stu> limit = stus.stream().limit(20);

skip(跳过)

跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补。

1
Stream<Stu> skip = stus.stream().skip(3);

map(转换流)

转换流,将原Stream转换成一个新的Stream。接收一个Function函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。

map方法有3个扩展方法:

  • mapToInt:转换成IntStream。
  • mapToLong:转换成LongStream。
  • mapToDouble:转换成DoubleStream。
1
Stream<Integer> integerStream = stus.stream().map(Stu::getAge);

flatMap(转换流并合并)

转换流,将原Stream中的每个元素都转换成另一个Stream,然后把所有Stream连接成一个新的Stream。接收一个Function函数作为参数。

1
2
3
4
class Stu{
String name;
List<String> roles = new ArrayList<>();
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
List<Stu> lists = new ArrayList<>();
Stu a = new Stu();
a.setName("aaaaaaa");
a.setRoles(Arrays.asList("admin","user","root"));
Stu b = new Stu();
b.setName("bbbbbbb");
b.setRoles(Arrays.asList("qwer","admin","root"));
lists.add(a);
lists.add(b);

List<String> collect = lists.stream().flatMap(it -> it.getRoles().stream()).collect(Collectors.toList());
collect.forEach(System.out::println);

输出:

1
2
3
4
5
6
admin
user
root
qwer
admin
root

peek(打印或修改)

生成一个包含源stream所有元素的新stream,同时会提供一个消费函数(consumer实例),新stream每个元素被消费的时候都会执行给定的消费函数。

peek主要被用在debug用途。可用于打印某中间操作后得到的元素信息。

1
2
3
4
5
6
class Stu{
String name;
int age;
long weight;
double money;
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
List<Stu> stus = new ArrayList<>();
Stu s1 = new Stu("bbbbb",12,24,23.24);
Stu s2 = new Stu("ddddd",1,124,133.124);
Stu s3 = new Stu("aaaaa",13,34,13.14);
Stu s4 = new Stu("ccccc",17,23,113.114);
Stu s5 = new Stu("eeeee",123,341,135.134);
stus.add(s1);
stus.add(s2);
stus.add(s3);
stus.add(s4);
stus.add(s5);


List<Stu> collect = stus.stream()
.peek(it -> System.out.println(it.getName() + " 的年龄为:"))
.filter(it -> it.getAge() > 10)
.peek(it -> System.out.println(it.getAge() + " 大于10 "))
.filter(it -> it.getAge() > 20)
.peek(it -> System.out.println(it.getAge() + " 大于20 ")).collect(Collectors.toList());

collect.forEach(System.out::println);

输出:

1
2
3
4
5
6
7
8
9
10
11
bbbbb 的年龄为:
12 大于10
ddddd 的年龄为:
aaaaa 的年龄为:
13 大于10
ccccc 的年龄为:
17 大于10
eeeee 的年龄为:
123 大于10
123 大于20
Stu{name='eeeee', age=123, weight=341, money=135.134}

这里可以看出,操作顺序是:每个元素完成所有中间操作后,下一个元素才开始完成所有中间操作。简单来说就是处理元素是一个一个的来的,上个元素完成后,下个元素才继续。

parallel(并行流)

将流转化为并行流。

流的并行操作的线程数默认是机器的CPU个数。默认使用的线程池是ForkJoinPool.commonPool线程池。

sequential(串行流)

将流转化为串行流。

多次调用parallel()和sequential(),以最后一次为准。

终止操作

forEach(遍历)

串行流的forEach会按顺序执行,但并行流的时候,并不会按顺序执行。

1
stus.stream().forEach(it-> System.out.println(it));

forEachOrdered(顺序遍历)

确保按照原始流的顺序执行(即使是并行流)。

1
stus.stream().parallel().forEachOrdered(it-> System.out.println(it));

toArray(转数组)

转化为数组。

  • toArray():将流转换成适当类型的数组。
  • toArray(generator):用于分配自定义的数组存储。
1
2
Object[] objects = stus.stream().toArray(); 
Stu[] stus1 = stus.stream().toArray(Stu[]::new);

findFirst(首元素)

返回一个含有第一个流元素的Optional类型的对象,如果流为空返回 Optional.empty。

findFirst() 无论流是串行流还是并行流,总是会选择流中的第一个元素(有顺序的)。

1
2
3
4
Optional<Integer> first = stus.stream().map(it -> it.getAge()).findFirst();
Optional<Integer> firstP = stus.stream().parallel().map(it -> it.getAge()).findFirst();
System.out.println(first.get());
System.out.println(firstP.get());

findAny(任意元素)

返回含有任意一个流元素的Optional类型的对象,如果流为空返回 Optional.empty。

对于串行流,findAny()和findFirst() 效果相同,选择流中的第一个元素。
对于并行流,findAny()也会选择流中的第一个元素(但因为是并行流,从定义上来看是选择任意元素)。

1
2
3
4
Optional<Integer> firstP = stus.stream().parallel().map(it -> it.getAge()).findAny();
Optional<Integer> first = stus.stream().map(it -> it.getAge()).findAny();
System.out.println(first.get());
System.out.println(firstP.get());

⚠️注意:自己在测试时,发现:如果将并行流放在串行流下面,那么两个的结果就是定了的(也许是我的问题,但我跑了很多遍发现都是一样的)。只有是上述代码才能保持每一次的结果都能让并行流的结果不一样。

allMatch(全部匹配)

Stream中全部元素符合传入的Predicate,返回 true,只要有一个不满足就返回false。

1
boolean b = stus.stream().allMatch(it -> it.getAge() > 0); // true

anyMatch(任意匹配)

Stream中只要有一个元素符合传入的 Predicate,返回 true。否则返回false。

1
boolean b = stus.stream().anyMatch(it -> it.getAge() > 100); //true

noneMatch(不匹配)

Stream中没有一个元素符合传入的 Predicate(即全部不满足条件),返回 true。只要有一个满足就返回false。

1
boolean b = stus.stream().noneMatch(it -> it.getAge() < 0); //true

reduce(组合流元素)

把Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。字符串拼接、sum、min、max、average 都是特殊的reduce。

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
// 求和:age 有6个,分别为1、2、3、4、5、6

// 串行流
Integer reduce = stus.stream().map(it -> it.getAge()).reduce(0, (a1, a2) -> a1 + a2);
System.out.println(reduce); // 21

Integer reduce = stus.stream().map(it -> it.getAge()).reduce(1, (a1, a2) -> a1 + a2);
System.out.println(reduce); // 22

// 并行流
Integer reduce = stus.stream().map(it -> it.getAge()).parallel().reduce(0, (a1, a2) -> a1 + a2);
System.out.println(reduce);// 21

Integer reduce = stus.stream().map(it -> it.getAge()).parallel().reduce(1, (a1, a2) -> a1 + a2);
System.out.println(reduce); // 27


// 求积:age 有6个,分别为1、2、3
// 串行流
Integer reduce = stus.stream().map(it -> it.getAge()).reduce(0, (a1, a2) -> a1 * a2);
System.out.println(reduce); // 0

Integer reduce = stus.stream().map(it -> it.getAge()).reduce(1, (a1, a2) -> a1 * a2);
System.out.println(reduce); // 1

Integer reduce = stus.stream().map(it -> it.getAge()).parallel().reduce(0, (a1, a2) -> a1 * a2);
System.out.println(reduce); // 0

Integer reduce = stus.stream().map(it -> it.getAge()).parallel().reduce(1, (a1, a2) -> a1 * a2);
System.out.println(reduce); // 6

总结

  • 串行流对总结果造成影响;并行流对每一个元素造成影响。
  • 串行流:初始值+值1+值2+值3…;并行流:(初始值+值1)+(初始值+值2)+(初始值+值3)…

max

求元素的最大值,如果流为空返回Optional.empty。示例见min示例。⚠️注意和min时的用法区别。

min

求元素的最小值,如果流为空返回Optional.empty。⚠️注意和max的使用区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
List<Stu> stus = new ArrayList<>();
Stu s1 = new Stu("bbbbb", 23, 24, 23.24);
Stu s2 = new Stu("ddddd", 11, 124, 133.124);
Stu s3 = new Stu("aaaaa", 43, 34, 13.14);
Stu s4 = new Stu("aaaaa", 13, 34, 13.14);
Stu s5 = new Stu("aaaaa", 33, 34, 13.14);
stus.add(s1);
stus.add(s2);
stus.add(s3);
stus.add(s4);
stus.add(s5);
Optional<Stu> max = stus.stream().max(((o1, o2) -> o1.getAge() - o2.getAge()));
System.out.println(max.get());
Optional<Stu> min = stus.stream().min(((o1, o2) -> o1.getAge() - o2.getAge()));
System.out.println(min.get());

输出:

1
2
Stu{name='aaaaa', age=43, weight=34, money=13.14}
Stu{name='ddddd', age=11, weight=124, money=133.124}

可以看见,不管对于max和min来说,都是使用了同样的比较器。但是结果求不一样。此比较器在正常的数组或集合中进行排序时,为默认升序排序。而在这里,相对于max来说,默认就是求最大值;相对于min来说,默认就是求最小值。所以,同一比较器对于不同的方法具有不同功能。(至少我自己时这样理解的)

count

流中的元素个数。

1
stus.stream().count()

sum

对所有流元素进行求和。

1
stus.stream().mapToInt(it -> it.getAge()).sum()

average

求取流元素平均值。

1
stus.stream().mapToInt(Stu::getAge).average()

collect(收集)

收集流元素转换成最后需要的结果。使用Collector 收集流元素到结果集中,主要是使用 Collectors(java.util.stream.Collectors)。具体的使用方式见高级篇中。

Collectors的方法如下:

方法名 描述
toList 将流中的元素放置到一个列表集合中。这个列表默认为ArrayList。
toSet 将流中的元素放置到一个无序集set中。默认为HashSet。
toCollection 将流中的元素全部放置到一个集合中,这里使用Collection,泛指多种集合。
toMap 根据给定的键生成器和值生成器生成的键和值保存到一个map中返回,键和值的生成都依赖于元素,可以指定出现重复键时的处理方案和保存结果的map。
toConcurrentMap 与toMap基本一致,只是它最后使用的map是并发Map:ConcurrentHashMap
joining 将流中的元素全部以字符序列的方式连接到一起,可以指定连接符,甚至是结果的前后缀。内部拼接使用的Java8的新类StringJoiner,可定义连接符和前缀后缀。
mapping 先对流中的每个元素进行映射,即类型转换,然后再将新元素以给定的Collector进行归纳。类似Stream.toMap。
collectingAndThen 在归纳动作结束之后,对归纳的结果进行再处理。
counting 元素计数
minBy 最小值的Optional
maxBy 最大值的Optional
summingInt 求和,结果类型为int
summingLong 求和,结果类型为long
summingDouble 求和,结果类型为double
averagingInt 平均值,结果类型int
averagingLong 平均值,结果类型long
averagingDouble 平均值,结果类型double
summarizingInt 汇总,结果包含元素个数、最大值、最小值、求和值、平均值,值类型都为int
summarizingLong 汇总,结果包含元素个数、最大值、最小值、求和值、平均值,值类型都为long
summarizingDouble 汇总,结果包含元素个数、最大值、最小值、求和值、平均值,值类型都为
reducing 统计,reducing方法有三个重载方法,其实是和Stream里的三个reduce方法对应的,二者是可以替换使用的,作用完全一致,也是对流中的元素做统计归纳作用。求和、平均值、最大值、最小值等其实就属于一种特殊的统计
groupingBy 分组,得到一个HashMap
groupingByConcurrent 分组,得到一个ConcurrentHashMap
partitioningBy 将流中的元素按照给定的校验规则的结果分为两个部分,放到一个map中返回,map的键是Boolean类型,值为元素的列表List

使用示例:

数据:

1
2
3
4
5
6
7
8
9
10
11
List<Stu> stus = new ArrayList<>();
Stu s1 = new Stu("bbbbb", 23, 24, 23.24);
Stu s2 = new Stu("ddddd", 11, 124, 133.124);
Stu s3 = new Stu("aaaaa", 43, 34, 13.14);
Stu s4 = new Stu("eeeee", 13, 34, 13.14);
Stu s5 = new Stu("ccccc", 33, 34, 13.14);
stus.add(s1);
stus.add(s2);
stus.add(s3);
stus.add(s4);
stus.add(s5);

测试:

1
2
3
4
5
6
// toList()
List<Stu> collect = stus.stream().collect(Collectors.toList());
// toSet()
Set<Stu> collect = stus.stream().collect(Collectors.toSet());
// toCollection()
LinkedList<Stu> collect = stus.stream().collect(Collectors.toCollection(LinkedList::new));

toMap(转换为Map)

有三种重载函数,两个参数、三个参数、四个参数。

1
2
3
4
5
6
7
8
// 两个参数。根据指定的函数映射key和value
Map<String, Integer> collect = stus.stream().collect(Collectors.toMap(it -> it.getName(), it -> it.getAge()));

// 三个参数。第三个参数表示:当key发生冲突时,是保留前者还是保留后者
Map<String, Integer> collect = stus.stream().collect(Collectors.toMap(it -> it.getName(), it -> it.getAge(),(k1,k2)->k1));

// 四个参数。第四个参数表示:指定返回的map类型
Map<String, Integer> collect = stus.stream().collect(Collectors.toMap(it -> it.getName(), it -> it.getAge(), (k1, k2) -> k1, TreeMap::new));

⚠️提示:上述方法同样适用于 toConcurrentMap,大同小异。

joining(拼接)

有三种重载函数,无参、一个参数、三个参数。

1
2
3
4
5
6
7
8
9
10
11
String collect1 = stus.stream().map(Stu::getName).collect(Collectors.joining());
String collect2 = stus.stream().map(Stu::getName).collect(Collectors.joining("[["));
String collect3 = stus.stream().map(Stu::getName).collect(Collectors.joining("\",\"", "[\"", "\"]")); // 最开始为[",中间字符串为"," ,最后为"]
System.out.println(collect1);
System.out.println(collect2);
System.out.println(collect3);

// 输出
bbbbbdddddaaaaaeeeeeccccc
bbbbb[[ddddd[[aaaaa[[eeeee[[ccccc
["bbbbb","ddddd","aaaaa","eeeee","ccccc"]
  • 无参:将字符串流直接进行拼接操作。
  • 一个参数(delimiter):自定义分隔符。实际上是调用了三个参数的方法,只是后两个参数传入的是null。
  • 三个参数(delimiter、prefix、suffix):delimiter表示分隔符,prefix表示第一个字符串前面的前缀,suffix表示最后一个串后的后缀。

mapping

将某值映射转换为另外一种形式。如:int -> String;String -> int。

1
2
3
4
List<String> collect1 = stus.stream().map(Stu::getAge).collect(Collectors.mapping(String::valueOf, Collectors.toList()));
collect1.forEach(System.out::println);
List<Integer> collect = Stream.of("1", "2", "3", "4", "5", "6", "7", "8", "9", "10").collect(Collectors.mapping(Integer::valueOf, Collectors.toList()));
collect.forEach(System.out::println);

在IDEA的提示下有另外一种方式:

1
2
stus.stream().map(Stu::getAge).map(String::valueOf).collect(Collectors.toList());
List<Integer> collect = Stream.of("1", "2", "3", "4", "5", "6", "7", "8", "9", "10").map(Integer::valueOf).collect(Collectors.toList());

其他方法

concat(合并流)

合并两个流成一个新的流。

1
Stream<Stu> concat = Stream.concat(stus.stream(), sss.stream());