Java8在集合框架中添加了Stream API。Stream API的作用也是处理集合中的元素,但和传统的迭代器相比,应该说在性能上更具优势。Stream的使用分成3个步骤。

  1. 创建一个Stream
  2. 描述要做什么,即指定一个或多个将一个Stream转化为另一个Stream的中间操作
  3. 要求产生结果,即使用终止操作

创建Stream

Java8中,不单是所有集合的实现均提供了stream()方法,常用的还包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//使用Collection接口的stream方法
List<String> list = new ArrayList<>();
list.add("Hello World");
Stream<String> stream1 = list.stream();
//使用Stream.of静态创建流
Stream<String> stream2 = Stream.of("1", "2", "3", "4", "5", "6", "7", "8", "9");
//从数组转换Stream
String[] array = new String[]{"1", "2", "3", "4", "5", "6", "7", "8", "9"};
Stream<String> stream3 = Arrays.stream(array);
//创建空的Stream
Stream<String> stream4 = Stream.empty();
//创建无限长度的Stream(含有无限多个随机数)
Stream<Double> stream5 = Stream.generate(Math::random);
//创建无限长度的Stream(含有无穷递增数列)
Stream<Integer> stream6 = Stream.iterate(0, (val) -> val + 1);

也就是说,除了从集合转换,我们还可以静态创建,甚至直接生成可能长度都是无限的Stream。但是,Stream本身并不存储其中的元素,这也就是为什么可以有长度无限的Stream,真正的存储依赖其底层集合,或者动态产生。

Stream的中间操作

常用的Stream中间操作包括

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//过滤出包含某个特定条件的所有元素
Stream<String> filter = stream.filter(word -> word.length() > 5);
//对流中值进行某种形式的转化
Stream<Integer> map = stream.map(String::length);
//提取子流:裁剪到指定长度
Stream<String> limit = stream.limit(5);
//提取子流:跳过前面若干元素
Stream<String> skip = stream.skip(5);
//连接两个流
Stream<String> concat = Stream.concat(Stream.of("Hello"), Stream.of("World"));
//对流中的元素调用某个方法,然后返回同样的流
Stream<String> peek = stream.peek(System.out::println);
//对流中元素去重
Stream<String> distinct = stream.distinct();
//对流中元素排序
Stream<String> sorted = stream.sorted(Comparator.reverseOrder());

其中最为关键的2种操作当属filtermap方法。前者传入一个从T到boolean的方法,它会产生一个只包含符合特定条件之元素的新流;后者传入一个从T到R的方法,会在流中的每个元素上执行传入的代码以完成转化;与map方法类似的还有一个flatMap方法,它的背后另有一套复杂理论,不是Stream的专属,但在流中,它的作用是将一个包含多个流的流展开为一个流,即从Stream>到Stream

这里对应Stream的第2个特点:Stream操作不会改变原Stream,而是返回一个持有操作结果的新Stream。

Optional操作

在Stream终止操作之前呢,插一段关于Optional的。

所谓Optional,和它的字面意思一样,是对一个值的封装,既然是“可选的”,那么封装的这个值就有可能不存在,但是Optional不会返回null,因此相对来说更安全。为什么是相对来说呢?因为如果只是用get()和ifPresent()两个方法来取出Optional封装的值,以及判断Optional是否封装了值,未免太生硬了,而且还是避免不了NPE。

首先如何创建Optional呢?使用of()和empty()方法,但这段逻辑Java8已经帮我们封装出了一个ofNullable()方法。

1
2
3
4
5
private static Optional<String> getStringOptional(String str) {
//如果str为null,返回空的Optional,否则返回封装了str的Optional
//完全等价于 return Optional.ofNullable(str);
return str == null ? Optional.empty() : Optional.of(str);
}

其次如何使用Optional呢?使用ifPresent()和orElse(T)/orElseGet|Throw()方法。

1
2
3
4
5
6
7
//如果Optional中有值,就执行System.out::println
getStringOptional(null).ifPresent(System.out::println);
getStringOptional("ABC").ifPresent(System.out::println);
//如果Optional中没有值,就代换成空串/执行某个方法获取值/抛出异常
getStringOptional(null).orElse("");
getStringOptional(null).orElseGet(() -> String.valueOf(System.currentTimeMillis()));
getStringOptional(null).orElseThrow(NoSuchElementException::new);

另外Optional也提供了map()和flatMap()方法,用途和Stream中是一样的,我们可以把Optional想象成一个长度非0即1的Stream,这样理解起来就会容易有一些。

Stream的终止操作

Stream的终止操作相对前面两者要复杂许多,将分成几个部分。这里提前给出流的第3个特点:操作可能会被延迟执行,意思是说前边的中间操作,看上去像是调用了一个或多个方法,但它们并不是立即就被执行的,而是要等到终止操作到来时才会被执行。

聚合

所谓聚合操作,就是以某种形式,将流中的元素组合为一个,使用reduce()方法。

1
2
3
4
5
6
7
8
9
Stream<Integer> stream1 = Stream.iterate(1, val -> val + 1).limit(10);
//聚合流的第一种形式,等价于sum=1, sum+=2, sum+=3...
Optional<Integer> sum1 = stream1.reduce(Integer::sum);
//聚合流的第二种形式,等价于sum=0, sum+=1, sum+=2...
Integer sum2 = stream1.reduce(0, Integer::sum);
Stream<String> stream2 = Stream.of("0", "12", "345", "6789");
//聚合流的第三种形式
Integer sum3 = stream2.reduce(0, (sum, word) -> sum += word.length(), Integer::sum);

聚合最简单的实现就是从流的前两个元素开始,或者从一个给定的值和流的第一个元素开始,不断将聚合函数应用到流中的其他元素上。第三种形式相对特殊一些,本例是求String流中各个字符串的总长度,但sum+=word.length()不满足BinaryOperator的定义(一个满足(T, T) -> T的函数)。

Java8另外提供了几个简便,但是常用的reduce操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//返回流中元素个数
Long count = stream1.count();
//返回流中元素最小值
Optional<Integer> min = stream1.min(Integer::compareTo);
//返回流中元素最大值
Optional<Integer> max = stream1.max(Integer::compareTo);
//返回流中满足指定条件的第一个元素
Optional<Integer> findFirst = stream1.filter(val -> val > 5).findFirst();
//返回流中满足指定条件的任意一个元素(并行计算)
Optional<Integer> findAny = stream1.parallel().filter(val -> val > 5).findAny();
//返回返回流中是否有元素满足指定条件(并行计算)
Boolean anyMatch = stream1.parallel().anyMatch(val -> val > 5);
//返回返回流中是否全部元素满足指定条件
Boolean allMatch = stream1.allMatch(val -> val > 5);
//返回返回流中是否没有元素满足指定条件
Boolean noneMatch = stream1.noneMatch(val -> val > 100);

收集

收集结果一般用在对流进行一番处理之后再来访问流中的元素。除了传统的iterator()方法可以返回这个流的迭代器,以及前文中使用过的toArray()方法将这个流转换成一个数组,Stream提供了collect()方法用于将元素收集起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Stream<Integer> stream = Stream.iterate(1, val -> val + 1).limit(10);
//收集到ArrayList中
//完全等价于 stream.collect(Collectors.toCollection(ArrayList::new))
ArrayList<Integer> arrayList = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
//收集到Set和List中(不考虑Collection的具体实现)
Set<Integer> set = stream.collect(Collectors.toSet());
List<Integer> list = stream.collect(Collectors.toList());
//收集成一个String,元素之间以英文逗号分割
String string = stream.map(String::valueOf).collect(Collectors.joining(","));
//收集成一个特殊的函数,一次性取得count/min/max/avg/sum
IntSummaryStatistics statistics = stream.collect(Collectors.summarizingInt(Integer::intValue));
//将表示JVM属性的流收集成一个Map<Object, Object>
Stream<Map.Entry<Object, Object>> propertyStream = System.getProperties().entrySet().stream();
Map<Object, Object> propertyMap = propertyStream.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (existKey, newKey) -> newKey));

对于收集成Map的情况,toMap()方法需要3个参数,第1个是Map的Key,第2个是Map的Value,这两个比较基础,但第3个是一个函数,用于处置Key重复的情形,例子中直接舍弃了旧的留下新的,如果需要两者均保留,那么一是收集结果将变成类似Map>,二是上面第3个参数的逻辑将变成重新实例化一个Set将新旧Set中的值全部添加进去。

分组和分片

上一节中我们收集Map的行为,实际上就是这里的分组,我们大可比不那般简单粗暴,完全可以使用groupingBy()方法代替

1
2
3
4
5
Stream<Map.Entry<Object, Object>> propertyStream = System.getProperties().entrySet().stream();
//按Key分组
Map<Object, List<Map.Entry>> propertyMap = propertyStream.collect(Collectors.groupingBy(Map.Entry::getKey));
//按Value是否为空分组
Map<Boolean, List<Map.Entry>> propertyIsBlankMap = propertyStream.collect(Collectors.groupingBy(entry -> entry.getValue().toString().equals("")));

groupingBy()方法的参数是分类的依据,一般地,指定Key,如果指定了一个返回布尔值的函数,那么整个流将按照这个布尔值的真假分成2类。

而分类后的Map的Value部分,一般地,为原始流中的某个元素,如果我们要对它进行加工处理,需要指定一个叫做downstream的东西,也即groupingBy()方法的第2个参数,但实际上,在使用之前应当考虑是都真的有必要使用非常非常复杂的聚合表达式,这里只举最简单的例子(可能不是十分恰当)

1
2
3
4
//按每个属性名对应的属性值个数分组(本例中都是1:1)
Map<Object, Long> propertyCountMap = propertyStream.collect(Collectors.groupingBy(Map.Entry::getKey,Collectors.counting()));
//按每个属性名对应的属性值的长度的数学特征分组
Map propertyLengthMap = propertyStream.collect(Collectors.groupingBy(Map.Entry::getKey,Collectors.summarizingInt(entry -> entry.getValue().toString().length())));

原始类型流

之前我们需要包含原始数据类型的流时,用的都是他们的包装类型,诸如Stream,Stream等,尽管有对基本数据类型的自动装箱拆箱机制,但多少对性能有一定影响。Java8同期提供了3种基本数据类型——int、long、double对应的Stream,分别是IntStream、LongStream、DoubleStream,但不提供byte、short、char、boolean、float类型的原始类型流。

原始类型流的创建,以及其方法的调用和对象流有几分相似,另外还支持很对象流的相互转换。

1
2
3
4
5
6
7
8
9
//直接创建int、long、double三种原始类型流
IntStream stream1 = Arrays.stream(new int[]{1, 2, 3, 4, 5, 5, 6, 7, 8, 9});
LongStream stream2 = LongStream.rangeClosed(1, 100);
DoubleStream stream3 = new Random().doubles();
//从对象流转换
Stream<String> stream4 = Stream.of("0", "12", "345", "6789");
IntStream stream5 = stream4.mapToInt(String::length);
//包装回对象流
Stream<Integer> stream6 = stream1.boxed();

并行流

其实早在前面就提到过了并行操作,缺省的,当使用stream()方法获得一个流时,它是串行的,要得到并行流,应当使用parallelStream()方法,或者在串行流上调用parallel()方法。既然是并行,就不得不考虑并发错误,这里需要注意的点和从前是一样的。另外在并行条件下不得不提的是有序问题,一般地,并发执行意味着结果的不确定性,但Stream对此提供了一定的保证,如果我们没有保持有序的需求,可以调用unordered()让后续的操作更高效地并行。