学习jdk1.8的Lambda和Stream

个人感觉1.8新特性中Lambda和Stream算是一个很大的革新,当然默认方法新的日期时间 API等特性也是很有意义的。只不过在我工作使用较少就不在这里叙述。
1.Lambda表达式

个人的理解Lambda表达式是一种使用特定语法书写的代码,因此我一直将他称为Lambda语法(个人理解),这种语法并不是开辟了什么新的东西,只是将原有的我们编写代码的方法变得更为简洁高效。由编译器转换为常规的代码,一定程度上减少代码的臃肿,但是第一篇文章的大佬还是不建议乱用哈哈。

首先我们来看一下Lambda表达式的写法(语法):

(parameters) -> expression
或
(parameters) ->{ statements; }

(参数)-> 表达式 

  或者 

  (参数)->{ 执行的代码;}。

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

这里的参数不是必须的,同样也不限制个数。

可以通过简单的示例进行一个了解:

public class Test {
public static void main(String[]args){
SystemMassage systemMassage=(message)->System.err.println("接口实现输出:"+message);
  systemMassage.sayMessage("你好");
}
interface SystemMassage {
void sayMessage(String message);
}
}

执行方法输出:接口实现输出:你好
(是不是有点草率,手动黑脸)。
我们可以通过用lombda的forEach循环对比原来的for循环,加深我们的了解。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
//原有forEach循环
for(Integer integer:numbers){
System.err.println(integer);
}

//java 8新的forEach 配合lambda
numbers.forEach((integer)->System.err.println(integer));

更多的lambda表达式参考第一篇文章。

2.Stream

java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。

Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。

Stream API可以极大提供Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。

这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。

图 1. 流管道 (Stream Pipeline) 的构成(来自IBM Developer

 图片来自 IBM开发社区

有多种方式生成 Stream Source:

  • 从 Collection 和数组
    • Collection.stream()
    • Collection.parallelStream()
    • Arrays.stream(T array) or Stream.of()
    从 BufferedReader
    • java.io.BufferedReader.lines()
  • 静态工厂
  • java.util.stream.IntStream.range()
  • java.nio.file.Files.walk()
  • 自己构建
    • java.util.Spliterator
    其它
    • Random.ints()
    • BitSet.stream()
    • Pattern.splitAsStream(java.lang.CharSequence)
    • JarFile.stream()

流的操作类型分为两种:

  • Intermediate:一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
  • Terminal:一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

在对于一个 Stream 进行多次转换操作 (Intermediate 操作),每次都对 Stream 的每个元素进行转换,而且是执行多次,这样时间复杂度就是 N(转换次数)个 for 循环里把所有操作都做掉的总和吗?其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream 里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在 Terminal 操作的时候循环 Stream 对应的集合,然后对每个元素执行所有的函数。

还有一种操作被称为 short-circuiting。用以指:

  • 对于一个 intermediate 操作,如果它接受的是一个无限大(infinite/unbounded)的 Stream,但返回一个有限的新 Stream。
  • 对于一个 terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。

当操作一个无限大的 Stream,而又希望在有限时间内完成操作,则在管道内拥有一个 short-circuiting 操作是必要非充分条件。

先来一个示例操作简单了解一下:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
//实现当前集合大于10的值求和。
//原来的写法

int sum=0;
for(Integer integer:numbers){
if(integer>8){
sum+=integer;
}
}
//打印结果
System.err.println(sum);

//使用java8 的 Stream后的操作
 //获取Stream
Stream<Integer> integerStream=numbers.stream();
//当前数组大于8的值求和
int sum= integerStream.filter((x)->x>8).mapToInt(Integer::intValue).sum();
//打印结果
System.err.println(sum);

filter 和 mapToInt 为 intermediate 操作,进行数据筛选和转换,最后一个 sum() 为 terminal 操作,对符合条件的全部结果求和。

需要注意的是,对于基本数值型,目前有三种对应的包装类型 Stream:

IntStream、LongStream、DoubleStream。当然我们也可以用 Stream<Integer>、Stream<Long> >、Stream<Double>,但是 boxing 和 unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。

Java 8 中还没有提供其它数值型 Stream,因为这将导致扩增的内容较多。而常规的数值型聚合运算可以通过上面三种 Stream 进行。

所以上面的代码我们可以这样写

 IntStream integerStream= (IntStream) numbers.stream();
//当前数组大于8的值求和
int sum= integerStream.filter((x)->x>8).sum();
//打印结果
System.err.println(sum);

流的操作

接下来,当把一个数据结构包装成 Stream 后,就要开始对里面的元素进行各类操作了。常见的操作可以归类如下。

  • Intermediate:

map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered

  • Terminal:

forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

  • Short-circuiting:

anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

 流转换为其它数据结构
// 1. Array
String[] strArray1 = stream.toArray(String[]::new);
// 2. Collection
List<String> list1 = stream.collect(Collectors.toList());
List<String> list2 = stream.collect(Collectors.toCollection(ArrayList::new));
Set set1 = stream.collect(Collectors.toSet());
Stack stack1 = stream.collect(Collectors.toCollection(Stack::new));
// 3. String
String str = stream.collect(Collectors.joining()).toString();
一个 Stream 只可以使用一次,上面的代码为了简洁而重复使用了数次。

我们下面看一下 Stream 的比较典型用法。

map/flatMap

我们先来看 map。如果你熟悉 scala 这类函数式语言,对这个方法应该很了解,它的作用就是把 input Stream 的每一个元素,映射成 output Stream 的另外一个元素。(这段话是复制IBM的帖子,个人理解就是对值进行操作,可以通过代码详细了解,通过和原有的写法进行对比更容易理解,在写博客之前我也对它没理解。)

 //将数组所有值转换为大写
List<String> strings = Arrays.asList("a", "b", "c", "d", "E", "f", "h", "i");

//定义结果集合
List<String> UpperCaseStr = new ArrayList<>();

//原来的写法 通过for循环遍历当前数组
for (String str : strings) {

  //操作每一个元素并放入集合
UpperCaseStr.add(str.toUpperCase());
}

// 打印结果
System.err.println(UpperCaseStr);

//使用Stream的Map
List<String> UpperCaseStr =strings.stream().map(String::toUpperCase).collect(toList());

//下面的代码引自IBM的那篇文章
清单 8. 平方数
1
2
3
4
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
List<Integer> squareNums = nums.stream().
map(n -> n * n).
collect(Collectors.toList());

这段代码生成一个整数 list 的平方数 {1, 4, 9, 16}。

从上面例子可以看出,map 生成的是个 1:1 映射,每个输入元素,都按照规则转换成为另外一个元素。还有一些场景,是一对多映射关系的,这时需要 flatMap。

 flatMap没看明白先把概念放在这里,底下我再研究研究再来补充。评论区有大神的分享一下

Stream<List<Integer>> inputStream = Stream.of(
 Arrays.asList(1),
 Arrays.asList(2, 3),
 Arrays.asList(4, 5, 6)
 );
Stream<Integer> outputStream = inputStream.
flatMap((childList) -> childList.stream());

flatMap 把 input Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起,最终 output 的新 Stream 里面已经没有 List 了,都是直接的数字。

我先去研究暂时告一段落

//经过一番研习之后大致将他搞懂了继续为大家进行介绍

依旧是举例说明,在学习的过程中网上很多帖子都是用一个HelloWord数组进行的介绍,也有流程图比较详细我就不在这里进行复述,我只写自己的用法及理解。

为了方便能够清晰理解,我先将原数组结构按照图片格式展示在这里:可以看到我们每一个元素中包含一个新的集合,这个时候我们如果想要将每一个元素下的集合取出,并汇总成一个新的集合就需要用到 flatMap方法代码如下。

 //直接将JSON字符串转成我们的list集合
JSONArray jsonArray=JSONArray.parseArray( "[{"name":"张店区","areaCode":"370303","number":null,"areaGoodVos":[{"goodName":"丙烯腈,稳定的","goodNumber":12,"unno":"1093"},{"goodName":"丙烯","goodNumber":2,"unno":"1077"}]},{"name":"桓台县","areaCode":null,"number":null,"areaGoodVos":[]},{"name":"高新区","areaCode":null,"number":null,"areaGoodVos":[{"goodName":"丙烯腈,稳定的","goodNumber":12,"unno":"1093"},{"goodName":"丙烯","goodNumber":2,"unno":"1077"}]},{"name":"周村区","areaCode":null,"number":null,"areaGoodVos":[]},{"name":"淄川区","areaCode":null,"number":null,"areaGoodVos":[]},{"name":"经开区","areaCode":null,"number":null,"areaGoodVos":[]},{"name":"临淄区","areaCode":null,"number":null,"areaGoodVos":[]},{"name":"沂源县","areaCode":null,"number":null,"areaGoodVos":[]},{"name":"博山区","areaCode":null,"number":null,"areaGoodVos":[]},{"name":"高青县","areaCode":null,"number":null,"areaGoodVos":[]}]");
maps=jsonArray.toJavaList(BusinessVo.class);

//filter过滤掉为空的集合 flatMap将内部集合AreaGoodVos汇集成一个集合
List ma2= maps.stream().filter(BusinessVo::numberIsNotNull).flatMap((k)->k.getAreaGoodVos().stream()).collect(Collectors.toList());

//打印汇总后的新集合
System.err.println(ma2);
//[BusinessNumberVo(goodName=丙烯腈,稳定的, goodNumber=12), BusinessNumberVo(goodName=丙烯, goodNumber=2), BusinessNumberVo(goodName=丙烯腈,稳定的, goodNumber=12), BusinessNumberVo(goodName=丙烯, goodNumber=2)]


 flatMap大致情况我描述到这里,欢迎各位看官有更好的理解进行交流讨论。

filter

filter方法和他字面的意思一样,就是一个过滤器的意思,比较简单理解方便使用,在Stream的开端示例时就用了Filter方法进行了演示,这里就不再进行过多的赘述。

出于致敬还是放上IBM的代码进行参考

清单 11. 把单词挑出来
1
2
3
4
List<String> output = reader.lines().
 flatMap(line -> Stream.of(line.split(REGEXP))).
 filter(word -> word.length() > 0).
 collect(Collectors.toList());

这段代码首先把每行的单词用 flatMap 整理到新的 Stream,然后保留长度不为 0 的,就是整篇文章中的全部单词了。

forEach和peek

接下来我们要说的是 forEach和peekl两个方法,forEach 方法接收一个 Lambda 表达式,然后在 Stream 的每一个元素上执行该表达式,参考我们Stream开端的演示,依旧只为大家放上IBM的实例方法

清单 12. 打印姓名(forEach 和 pre-java8 的对比)
1
2
3
4
5
6
7
8
9
10
// Java 8
roster.stream()
 .filter(p -> p.getGender() == Person.Sex.MALE)
 .forEach(p -> System.out.println(p.getName()));
// Pre-Java 8
for (Person p : roster) {
 if (p.getGender() == Person.Sex.MALE) {
 System.out.println(p.getName());
 }
}

对一个人员集合遍历,找出男性并打印姓名。可以看出来,forEach 是为 Lambda 而设计的,保持了最紧凑的风格。而且 Lambda 表达式本身是可以重用的,非常方便。当需要为多核系统优化时,可以 parallelStream().forEach(),只是此时原有元素的次序没法保证,并行的情况下将改变串行时操作的行为,此时 forEach 本身的实现不需要调整,而 Java8 以前的 for 循环 code 可能需要加入额外的多线程逻辑。

但一般认为,forEach 和常规 for 循环的差异不涉及到性能,它们仅仅是函数式风格与传统 Java 风格的差别。(但是我在实际测试过程中发现forEach的效率是优于传统的for循环,也与使用场景有关 有兴趣的朋友也可以自己测试)

另外一点需要注意,forEach 是 terminal 操作,因此它执行后,Stream 的元素就被“消费”掉了,你无法对一个 Stream 进行两次 terminal 运算。下面的代码是错误的

 //获取一个流
Stream stream=maps.stream();
// 调用流的forEach方法输出当前集合
stream.forEach(System.out::println);
// 再次调用该流的forEach方法输出当前集合
stream.forEach(System.out::println);

这段代码在执行时会抛出  stream has already been operated upon or closed异常因此一定要牢记,你无法对一个 Stream 进行两次 terminal 运算。
但是在我们实际的应用场景中可能会出现,需要对一个集合循环操作多次的情况,为了解决这一问题我们的 peek方法 应运而生(也可能不是因为这个原因)。peek 对每个元素执行操作并返回一个新的 Stream。
 List<String> strs=Arrays.asList("a","b","c","d");
//获取一个流
Stream<String> stream=strs.stream();
// 调用流的forEach方法输出当前集合
List<String> str2= stream.peek(System.out::println).map(String::toUpperCase).peek(System.err::println).collect(Collectors.toList());

forEach 不能修改自己包含的本地变量值,也不能用 break/return 之类的关键字提前结束循环


 


原文地址:https://www.cnblogs.com/MQTimor/p/10889062.html