My studying notes for Java,Ruby,Ajax and other any interesting things.

星期四, 十二月 01, 2016

java8的Stream API

java8中增加了Stream API,在coding过程中经常使用到,每次用的时候总觉的有些新意,没有系统的学习和研究一下。

今天抽时间,研究一下看看stream api的具体功能和用法:


总览:
  • stream是java8的一个心功能,这个流与java.io中的流的概念不同,而是对Collection集合类的一个增强,专注与如何高效和便利的处理java集合(包括list,set,map...)
  • stream在处理的时候利用到了新的Lambda表达式(函数式),链式调用,实现了计算时加载,可以处理更大量的数据,同时代码的可读性增强,代码块变小;
  • 同时也支持并行和串行的两种汇聚操作模式,可以充分利用CPU多核优势;

集合的操作:

通常集合的操作有这样几种:遍历,过滤, 排序,group by,求最大(最小),求均值等操作。在java7及以前的代码,常用的做法是循环这个集合,获取到集合中的具体元素,针对集合元素进行处理和操作。
eg:
for(Type item: items){
   item.doSth();
   if(item.suitable){
      dosth();sort();
   }
}


在java8中可以这样写:

items.stream().filter(item->item.suitable()).map(Item::getXXX).collect(toList());

相比java7的代码来的更加简洁易懂;但是引入了好几个函数,比如stream(),filter(),map(),collect() 等方法,需要详细了解下其用处。

详解Stream:

Stream有点类似于Iterator,是单向的,不可以往复,数据只能遍历一次,遍历一次就用完了,无法再使用。同时Stream支持并行运算,这个比Iterator要更高效。并行API从java.1.4就开始引入(Thread),经历了concurent,phasers,fork/join等方法的发展以后,到了java8引入了Lambda,对stream起了很大的推进作用。

一个流的组成:
一个流通常分为三个过程,如下图:获取数据 —> 数据转换 —> 执行操作,每次转换的时候Stream对象不改变,但是会返回一个新的Stream对象,就可以向链式调用一样进行多次转换或者操作。

生成流有几种办法:

从Collection或者数组中来:
  Collection.stream();
  Collection.parallelStream()
  Arrays.stream(T array)   Stream.of()(java8)
  java.io.BufferedReader().lines
静态生成方法:
  java.uti.stream.IntStream.range()
  java.nio.file.Files.walk()
自己构建:
  java.util.Spliterator
  …



Stream的操作分为两种类型:
- Intermediate,一个流可以做零个或者多个这样的操作,目的是打开流,并进行数据的映射,过滤等操作,然后返回一个新的流;这些方法都是惰性(lazy)的,并不会真正的进行操作,只有进行一个Terminal操作的时候,才会被执行。
- Terminal , 一个流进行了Terminal操作,就被使用光了,无法再被操作。所以,这个操作是流的最后一个操作。做这个操作的时候,所有的Intermediate的操作也才会真正的发生,并产生一个结果。

还有一种:short-circurting操作,针对intermediate和terminal操作意义不同:
— 针对intermediate操作是接受一个无限大的Stream,返回一个有限的Stream;
— 针对terminal操作,是接受一个无限大的stream,返回一个有限

可以理解为,Intermediate操作只是注入了一个函数(比如过滤,循环等Lambda代码块)。

比如:
int sum = widgets.stream()
.filter(w -> w.getColor() == RED)
.mapToInt(w -> w.getWeight())
.sum();


这个代码块中:widgets.stream() 产生一个流,filter,map都是Intermediate操作,sum是一个Terminal操作;


流的使用:
  • 数值的流有IntStream,LongStream,DoubleStream,可以直接对Int,Long,Double等原始类型进行操作。

Intermediate操作有:

    map(maptoInt,floatMap)、filter、distinct、sorted、peek、limit、skip、parallel、sequential、unsorted
Terminal操作有:
    forEach,forEachOrdered,toArray,reduce,collect,min,max,count,anyMatch,allMatch,noneMatch,findFirst,findAny,iterator
Short-circuiting:
    anyMatch,allMatch,noneMatch,findFirst,findAny,limit

典型操作:

map/flatMap:
  把input stream的每个元素映射成为output stream的另外一个元素。
eg.1:

List<String> output = wordList.stream().map(String::toUpperCase).collect(Collectors.toList())


这段代码中的map做做了一个String::toUpperCase的操作,把inputStream的每个元素进行了一个大写操作,并作为一个outputStream输出,再进行了collect操作转化为一个List。

eg.2:

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

这个map操作将第一个List中的每个元素进行了平方操作。

Map生成的映射是1:1的,每个源stream都按照规则转换为另外一个目标steram中的元素。部分情况会产生一对多的关系,1个input stream的元素可能会产生多个output stream的元素,这时候需要是用到flatMap:

eg.3:

Stream<List<Integer>> inputSteram = Stream.of(
   Arrays.asList(1),
   Arrays.asList(2,3,),
   Arrays.asList(4,5,6)
 );
Stream<Integer> outputSteram = inputSteam.flatMap((childList)->childList.Stream());

这个例子中的flatMap就将input stream中的每一个childList都进行了扁平化,将元素抽取出来放在一个output stream中,这样output stream中就没有List了,只有一堆数字。

filter:
filter对input stream进行测试,测试通过的元素留下来生成一个新的Stream。

eg.4:
Integer[] sixNums = {1,2,3,4,5,6};
Integer[] evens = Stream.of(sixNums).filter(n->n/2==0).toArray(Integer[]::new);

这个操作就将sixNums中的偶数挑选出来了。

eg.5:
List<String> output = reader.lines().
       flatMap(line->Stream.of(line.split(REGEXP)).
       filter(word->word.length()>0).
       collect(Collectors.toList()); 

这段代码将reader中的每一行读取并进行flatMap拉平到一个新的stream,每个元素都是切割好的,并过滤出来长度大于0的单次,形成整篇文章的全部单词。

forEach: Terminal
forEach接收一个lambda表达式,对stream的每个元素都执行一遍表达式的内容。

eg.6:
workers.stream().
      filter(w->w.getGender() == Person.Sex.MALE).
      forEach(w->System.out.println(w->getName()));

pre java8 :
for ( Woker p : wokers) {
   if ( p.getGender() == Person.Sex.MALE){
      System.out.println(p.getName());
  }
}

可以明显看得出来,通过stream的操作代码变得更加简洁紧凑,更易阅读。而且当数据量较多的时候,可以使用parallelSteram.forEach来提升性能(但是要注意次序没有办法保证)。forEach 与 for循环严格意义上性能并没有太大差别,更多的在于风格差异。

此外,forEach() 是一个terminal操作,操作完毕后stream就被消费完毕,将无法再进行使用。peek() 是一个intermediate操作,可以实现forEach()的功能,但是会返回一个新的Stream。

eg.7.:
Stream.of("one","two","three","four").
    filter(e->e.length()>3).
    peek(e->System.out.println("Filtered value:"+e)).
    map(String::toUpperCase).
    peek(e->System.out.println("Mapped value:" +e)).
    collect(Collectors.toList())

forEach不能修改自己包含的本地变量只,也不能break和return。 

findFirst: terminal & short-circuiting

返回Stream的第一个元素,或者空。他的返回值是一个Optional,是模拟scala的一个概念,是一个容器,可能包含或者不包含。使用它可以避免NullPointerException。

Optional可以使代码的可读性更好,同时提供了编译时检查,可以降低NPE的概率, 在编码的时候就需要处理空值问题,而不是运行时发现和debug。Stream的findAny,max/min,reduce等方法都是返回Optional值。

reduce:terminal:
这个方法主要作用是把Stream的元素组合起来。提供一个初始值,然后按照一定的运算规则,与前面的第一个,第二个,第n个元素组合。比如字符串拼接,数值sum,min,max,average都是特殊的reduce操作。比如sum也可以写成:

Integer sum = integers.reduce(0,(a,b)->a+b)
or
Integer sum = integers.reduce(0,Integer::sum);

如果没有提供初始值,会把Stream的前两个元素组合起来,返回一个Optional的值。

eg.8:
//拼接
String concat = Stream.of("A","B","C","D","E").reduce("",Stream::concat);
//求最小值,最大值则相反
double minValue = Stream.of(-1.5,1.0,-3.0,-2.0).reduce(Double.MAX_VALUE,Double::min);
//数字求和,初始为0
int sumValue = Stream.of(1,2,3,4,5).reduce(0,Integer::sum);
//求和,无初始值
int sumValue = Stream.of(1,2,3,4,5).reduce(Integer::sum).get();
//过滤再拼接
concat = Stream.of("a","b","c","d","e","f").filter(x->x.compareTo("Z")>0).reduce("",Stream::concat));

limit/skip:
limit返回前n个元素,skip扔掉前n个元素;
需要注意:
1.短路操作,前n个元素直接跳过,执行的数量也会下降;
2.如果与sort集成,可能会导致无法短路;
3.并行的时候limit和skip的代价会增加

sorted:
对对象进行排序;
stream的排序比数组强大,可以进行map,filter,limit,distinct让元素减少以后再进行排序,可以明显缩短执行时间。
尽量把sort放在最后,这样代价会比较小。

max/min/distinct:
max和min的实现可以使用stream.sorted().findFirst,但是性能会更差。max/min的效率为O(n),sorted是O(n log n)。

eg.9:
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);


上述代码读入的代码进行lowerCase然后distinct排序并返回一个list;

Match方法:
判断元素中是否有符合条件的数据,返回一个boolean值。
有三个match的方法,allMatch,anyMatch,noneMatch。

自己生成Stream方法:

Stream.of:
  — 有两种,可以接收一个变长参数,也可以接受单一值
Stream.generate:
  — 生成一个无限长度的stream
Stream.generate(new Supplier<Double>(){
   @override
   public Double get(){
      return Math.random();
   }
});
Stream.generate(()->Math.random());
Stream.generate(Math::random);

Stream.iterate:生成无限长度的Stream,生成的时候可以根据种子来调用指定函数生成。可以认为是seed,f(seed),f(f(seed))的循环:

Stream.iterate(1,item->item+1).limit(10).forEach(System.out::println)



Refer:

印象笔记,让记忆永存。下载印象笔记

没有评论: