Stream 流 是java 8 中处理集合的关键抽象概念。它可以指定你希望对集合进行的操作。我们使用 Java 8 尽量从以往迭代器转为使用Stream操作。它与集合的区别如下:
- Stream 自己不会存储元素。元素可能被存储在底层的集合中,或者根据需要产生出来
- Stream 操作符不会改变源对象。相反,它们会返回一个持有结果的新Stream
- Stream 操作符可能是延迟执行的。这意味着它们会等到需要结果的时候才执行。如下面的一个例子,可以看出第6行 如果非延迟执行,第一个值是不可能输出的。
@Test
public void test4() {
//生成一个无限的序列,forEach会停不下来。
Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE)).forEach(System.out::println);
//可以输出第一个值
Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE)).findFirst();
}
- Stream 的可读性要更好一些。而且可以很容易进行并行执行。如
long count = words.parallelStream().filter(w -> w.length() > 12).count();
只需要Stream修改为parallelStream.
当使用Stream 时,你会通过三个阶段来建立一个操作流水线:
- 创建一个Stream。
- 在一个或者多个步骤中,将初始的Stream转为另一个Stream的中间操作。
- 使用一个终止操作来产生一个结果。该操作会强制它之前的延迟操作立即执行。这和Spark中的终止操作很像。
下面我们来一步步看怎么进行操作
创建Stream
创建Stream一般有以下几种方式
- Collection接口中新添加的stream 方法,可以将任何集合转为一个Stream。如:Arrays.asList("sb","e").stream()
- 如果是一个数组,可以通过Stream的静态方法Stream.of
方法将它转为一个Stream 。如:Stream<String> words = Stream.of(contexts.split(","));
- of 方法可以接受可变的参数,因此你可以构造一个含有任意个参数的Stream Stream<String> s = Stream.of("a","b","c");
- 使用Arrays.stream(array,from,to)
方法将数组的一部分转为Stream。
- 创建一个空Stream。Stream.empty();
- 创建无限Stream。有人说创建一个无限Stream有啥用,前面说过,创建无限时是不会执行的,只有在终止操作时才会执行,在终止操作前可以进行一些限制从而创建出一些符合条件的Stream。如下
@Test
public void test5() {
List<String> list = Arrays.stream("a,b,c,d,e,f".split(",")).collect(Collectors.toList());
System.err.println(JSON.toJSONString(list));
int size = list.size();
List<String> result = IntStream.iterate(size-1, n -> n-1).limit(size).mapToObj(list::get).collect(Collectors.toList());
System.err.println(JSON.toJSONString(result));
}
看出来这个例子是做什么的吗?其实第6行就是将list进行反转。输出结果如下:
["a","b","c","d","e","f"]
["f","e","d","c","b","a"]
IntStream.iterate
就是会产生一个 该方法第一个参数是一个初始值,后面的一个函数是对第一个初始值,进行应用该函数,会一直执行下去,但这个动作是延迟的,而类似limit
之类的操作保证了只执行次数。
- 还有Files.lines
会返回一个包含文件中所有行的Stream。Stream接口有一个父接口AutoCloseable。当在某个Stream上调用close 方法时,底层的文件也会被关闭。
- 还有Pattern.complie("[\\P{L}]+").splitAsStream(contexts)
可以按正则表达式对对象进行分隔。
- 其它方法。
转换Stream
转换Stream其实就是把一个Stream通过某些行为转换成一个新的Stream。Stream这种操作很多,只能选择几个常用的进行说一下。
- filter: 对于Stream中包含的元素使用给定的过滤函数进行过滤操作,新生成的Stream只包含符合条件的元素;
Stream<T> filter(Predicate<? super T> predicate)
filter接受一个Predicate (断言)函数式接口
Predicate 中有三个默认方法一个default(and, negate,or)法,一个默认方法(isEqual),一个抽象方法(test).
具体分析可查看Interface Predicate 中的介绍,测试代码如下:
Predicate<String> a = x -> x != null;
Predicate<String> b = a.and(x -> !"".equals(x));
Stream.of("a","","b",null).filter(b.negate()).forEach(System.out::println);
filter方法示意图:
- map: 对于Stream中包含的元素使用给定的转换函数进行转换操作,新生成的Stream只包含转换生成的元素。这个方法有三个对于原始类型的变种方法,分别是:mapToInt,mapToLong和mapToDouble。这三个方法也比较好理解,比如mapToInt就是把原始Stream转换成一个新的Stream,这个新生成的Stream中的元素都是int类型。之所以会有这样三个变种方法,可以免除自动装箱/拆箱的额外消耗;
<R> Stream<R> map(Function<? super T,? extends R> mapper)
map接受一个Function函数接口。表示接受一个参数并产生结果的函数。
它有二个 default方法(andThen, compose)一个静态方法(identity)
Function<Integer, Integer> f = x -> x * 3;
System.err.println(f.andThen(x -> x + 4).apply(6)); //结果 22 先执行6*3 再进行 +4操作 6*3+4
System.err.println(f.compose(x -> Integer.valueOf(x.toString()) + 4).apply(6)); //结果为30:
先将x+4执行*3操作(x+4)*3=3x+12 再将6执行上面的操作 3*6+12=30
map方法示意图:
- flatMap:和map类似,不同的是其每个元素转换得到的是Stream对象,会把子Stream中的元素压缩到父集合中;
flatMap方法示意图:
String[] words = new String[]{"Hello","World"};
List<String[]> a = Arrays.stream(words)
.map(word -> word.split(""))
.distinct()
.collect(toList());
a.forEach(System.out::print);
String[] words = new String[]{"Hello","World"};
List<String> a = Arrays.stream(words)
.map(word -> word.split(""))
.flatMap(Arrays::stream)
.distinct()
.collect(toList());
a.forEach(System.out::print);
使用flatMap方法的效果是,各个数组并不是分别映射一个流,而是映射成流的内容,所有使用map(Array::stream)时生成的单个流被合并起来,即扁平化为一个流。
- peek: 生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数;
peek方法示意图:
- limit: 对一个Stream进行截断操作,获取其前N个元素,如果原Stream中包含的元素个数小于N,那就获取其所有的元素;
limit方法示意图:
- skip: 返回一个丢弃原Stream的前N个元素后剩下元素组成的新Stream,如果原Stream中包含的元素个数小于N,那么返回空Stream;
skip方法示意图:
distinct: 对于Stream中包含的元素进行去重操作(去重逻辑依赖元素的equals方法),新生成的Stream中没有重复的元素;
distinct方法示意图
sorted: 遍历整个流,并在产生任何元素之前对它进行排序。
Arrays.stream("a,w,d,e,f,b,c,j,h".split(",")).sorted().forEach(System.out::println); Arrays.stream("a,we,deee,eff,fw,b,c,j,h".split(",")).sorted(Comparator.comparing(String::length).reversed()).forEach(System.out::println);
- 连续多次操作
List<Integer> nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10);
System.out.println(“sum is:”+nums.stream().filter(num -> num != null).
distinct().mapToInt(num -> num * 2).
peek(System.out::println).skip(2).limit(4).sum());
这段代码演示了上面介绍的所有转换方法(除了flatMap),简单解释一下这段代码的含义:给定一个Integer类型的List,获取其对应的Stream对象,然后进行过滤掉null,再去重,再每个元素乘以2,再每个元素被消费的时候打印自身,在跳过前两个元素,最后去前四个元素进行加和运算(解释一大堆,很像废话,因为基本看了方法名就知道要做什么了。这个就是声明式编程的一大好处!)。大家可以参考上面对于每个方法的解释,看看最终的输出是什么。
- 性能问题
有些细心的同学可能会有这样的疑问:在对于一个Stream进行多次转换操作,每次都对Stream的每个元素进行转换,而且是执行多次,这样时间复杂度就是一个for循环里把所有操作都做掉的N(转换的次数)倍啊。其实不是这样的,转换操作都是lazy的,多个转换操作只会在汇聚操作(见下节)的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在汇聚操作的时候循环Stream对应的集合,然后对每个元素执行所有的函数。
聚合操作(Reduce)
- reduce:这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。例如 Stream 的 sum 就相当于
Integer sum = integers.reduce(0, (a, b) -> a+b); //或
Integer sum = integers.reduce(0, Integer::sum);
也有没有起始值的情况,这时会把 Stream 的前面两个元素组合起来,返回的是 Optional。
- collect :该方法功能比较强大,能将流收集成很多形式。collect方法接收的多个参数中主要有一个Collector接口,该接口的实现类Collectors提供很多静态方法便于多种形式的收集,比较强大。
1. 归约
1.1.计数
long count = list.stream() .collect(Collectors.counting());
long count = list.stream().count(); //两个相同
1.2. 最值
Optional<Person> oldPerson = list.stream().collect(Collectors.maxBy(Comparator.comparingInt(Person::getAge)));
1.3.求平均值
double avg = list.stream().collect(Collectors.averagingInt(Person::getAge));
1.4.求和
int summing = list.stream().collect(Collectors.summingInt(Person::getAge));
1.5.连接字符串
String names = list.stream().collect(Collectors.joining());
String names = list.stream().collect(Collectors.joining(", "));
//每个字符串默认分隔符为空格,若需要指定分隔符,则在joining中加入参数即可:
1.6.一般性的归约操作
若你需要自定义一个归约操作,那么需要使用Collectors.reducing函数,该函数接收三个参数:
- 第一个参数为归约的初始值
- 第二个参数为归约操作进行的字段
- 第三个参数为归约操作的过程
如求合
Optional<Integer> sumAge = list.stream().collect(Collectors.reducing(0,Person::getAge,(i,j)->i+j));
//相比于 1.4更一般性一些
上面例子中,reducing函数一共接收了三个参数:
- 第一个参数表示归约的初始值。我们需要累加,因此初始值为0
- 第二个参数表示需要进行归约操作的字段。这里我们对Person对象的age字段进行累加。
- 第三个参数表示归约的过程。这个参数接收一个Lambda表达式,而且这个Lambda表达式一定拥有两个参数,分别表示当前相邻的两个元素。由于我们需要累加,因此我们只需将相邻的两个元素加起来即可。
Collectors.reducing方法还提供了一个单参数的重载形式。
你只需传一个归约的操作过程给该方法即可(即第三个参数),其他两个参数均使用默认值。
- 第一个参数默认为流的第一个元素
- 第二个参数默认为流的元素
求合。
Optional<Integer> sumAge = list.stream().collect(Collectors.reducing((i,j)->i+j));
2.分组
分组就是将流中的元素按照指定类别进行划分,类似于SQL语句中的GROUPBY。
2.1.一级分组
例:将所有人分为老年人、中年人、青年人
Map<String,List<Person>> result = list.stream()
.collect(Collectors.groupingby((person)->{
if(person.getAge()>60)
return "老年人";
else if(person.getAge()>40)
return "中年人";
else
return "青年人";
}));
groupingby函数接收一个Lambda表达式,该表达式返回String类型的字符串,groupingby会将当前流中的元素按照Lambda返回的字符串进行分组。
分组结果是一个Map< String,List< Person>>,Map的键就是组名,Map的值就是该组的Perosn集合。
2.2.多级分组
多级分组可以支持在完成一次分组后,分别对每个小组再进行分组。
使用具有两个参数的groupingby重载方法即可实现多级分组。
- 第一个参数:一级分组的条件
- 第二个参数:一个新的groupingby函数,该函数包含二级分组的条件
例:将所有人分为老年人、中年人、青年人,并且将每个小组再分成:男女两组。
Map<String,Map<String,List<Person>>> result = list.stream()
.collect(Collectors.groupingby((person)->{
if(person.getAge()>60)
return "老年人";
else if(person.getAge()>40)
return "中年人";
else
return "青年人";},groupingby(Person::getSex)));
此时会返回一个非常复杂的结果:Map< String,Map< String,List< Person>>>。
2.3.对分组进行统计
拥有两个参数的groupingby函数不仅仅能够实现多几分组,还能对分组的结果进行统计。
例:统计每一组的人数
Map<String,Long> result = list.stream()
.collect(Collectors.groupingby((person)->{
if(person.getAge()>60)
return "老年人";
else if(person.getAge()>40)
return "中年人";
else
return "青年人";
},
counting()));
此时会返回一个Map< String,Long>类型的map,该map的键为组名,map的值为该组的元素个数。
3.分片
参考:
书:《写给大忙人看的Java SE 8》
并发编程网
jdk-1.8-google