Java Lambda表达式知多少

1. 匿名内部类实现

匿名内部类仍然是一个类,只是不须要程序员显示指定类名,编译器会自动为该类取名。所以若是有以下形式的代码,编译以后将会产生两个class文件:java

public class MainAnonymousClass {
    public static void main(String[] args) {
        new Thread(new Runnable(){
            @Override
            public void run(){
                System.out.println("Anonymous Class Thread run()");
            }
        }).start();;
    }
}

编译以后文件分布以下,两个class文件分别是主类和匿名内部类产生的:git

2-AnonymousClass.png

进一步分析主类MainAnonymousClass.class的字节码,可发现其建立了匿名内部类的对象:程序员

// javap -c MainAnonymousClass.class
public class MainAnonymousClass {
  ...
  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/Thread
       3: dup
       4: new           #3                  // class MainAnonymousClass$1 /*建立内部类对象*/
       7: dup
       8: invokespecial #4                  // Method MainAnonymousClass$1."<init>":()V
      11: invokespecial #5                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
      14: invokevirtual #6                  // Method java/lang/Thread.start:()V
      17: return
}

2. Lambda表达式实现

Lambda表达式经过invokedynamic指令实现,书写Lambda表达式不会产生新的类。若是有以下代码,编译以后只有一个class文件:github

public class MainLambda {
    public static void main(String[] args) {
        new Thread(
                () -> System.out.println("Lambda Thread run()")
            ).start();;
    }
}

编译以后的结果:编程

2-Lambda

经过javap反编译命名,咱们更能看出Lambda表达式内部表示的不一样:数组

// javap -c -p MainLambda.class
public class MainLambda {
  ...
  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/Thread
       3: dup
       4: invokedynamic #3,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable; /*使用invokedynamic指令调用*/
       9: invokespecial #4                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
      12: invokevirtual #5                  // Method java/lang/Thread.start:()V
      15: return

  private static void lambda$main$0();  /*Lambda表达式被封装成主类的私有方法*/
    Code:
       0: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #7                  // String Lambda Thread run()
       5: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

反编译以后咱们发现Lambda表达式被封装成了主类的一个私有方法,并经过invokedynamic指令进行调用。数据结构

3. Streams API(I)

你可能没意识到Java对函数式编程的重视程度,看看Java 8加入函数式编程扩充多少功能就清楚了。Java 8之因此费这么大功夫引入函数式编程,缘由有二:app

  1. 代码简洁函数式编程写出的代码简洁且意图明确,使用stream接口让你今后告别for循环。
  2. 多核友好,Java函数式编程使得编写并行程序从未如此简单,你须要的所有就是调用一下parallel()方法。

img

图中4种stream接口继承自BaseStream,其中IntStream, LongStream, DoubleStream对应三种基本类型(int, long, double,注意不是包装类型),Stream对应全部剩余类型的stream视图。为不一样数据类型设置不一样stream接口,能够1.提升性能,2.增长特定接口函数。ide

虽然大部分状况下stream是容器调用Collection.stream()方法获得的,但streamcollections有如下不一样:函数式编程

  • 无存储stream不是一种数据结构,它只是某种数据源的一个视图,数据源能够是一个数组,Java容器或I/O channel等。
  • 为函数式编程而生。对stream的任何修改都不会修改背后的数据源,好比对stream执行过滤操做并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新stream
  • 惰式执行stream上的操做并不会当即执行,只有等到用户真正须要结果的时候才会执行。
  • 可消费性stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须从新生成。

stream的操做分为为两类,中间操做(intermediate operations)和结束操做(terminal operations),两者特色是:

  1. 中间操做老是会惰式执行,调用中间操做只会生成一个标记了该操做的新stream,仅此而已。
  2. 结束操做会触发实际计算,计算发生时会把全部中间操做积攒的操做以pipeline的方式执行,这样能够减小迭代次数。计算完成以后stream就会失效。

若是你熟悉Apache Spark RDD,对stream的这个特色应该不陌生。

下表汇总了Stream接口的部分常见方法:

操做类型 接口方法
中间操做 concat() distinct() filter() flatMap() limit() map() peek() skip() sorted() parallel() sequential() unordered()
结束操做 allMatch() anyMatch() collect() count() findAny() findFirst() forEach() forEachOrdered() max() min() noneMatch() reduce() toArray()

区分中间操做和结束操做最简单的方法,就是看方法的返回值,返回值为stream的大都是中间操做,不然是结束操做。

flatMap()

img

函数原型为<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper),做用是对每一个元素执行mapper指定的操做,并用全部mapper返回的Stream中的元素组成一个新的Stream做为最终返回结果。提及来太拗口,通俗的讲flatMap()的做用就至关于把原stream中的全部元素都”摊平”以后组成的Stream,转换先后元素的个数和类型均可能会改变。

Stream<List<Integer>> stream = Stream.of(Arrays.asList(1,2), Arrays.asList(3, 4, 5));
stream.flatMap(list -> list.stream())
    .forEach(i -> System.out.println(i));

上述代码中,原来的stream中有两个元素,分别是两个List<Integer>,执行flatMap()以后,将每一个List都“摊平”成了一个个的数字,因此会新产生一个由5个数字组成的Stream。因此最终将输出1~5这5个数字。

截止到目前咱们感受良好,已介绍Stream接口函数理解起来并不费劲儿。若是你就此觉得函数式编程不过如此,恐怕是高兴地太早了。下一节对Stream规约操做的介绍将刷新你如今的认识。

多面手reduce()

reduce操做能够实现从一组元素中生成一个值,sum()max()min()count()等都是reduce操做,将他们单独设为函数只是由于经常使用。reduce()的方法定义有三种重写形式:

  • Optional<T> reduce(BinaryOperator<T> accumulator)
  • T reduce(T identity, BinaryOperator<T> accumulator)
  • <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

虽然函数定义愈来愈长,但语义未曾改变,多的参数只是为了指明初始值(参数identity),或者是指定并行执行时多个部分结果的合并方式(参数combiner)。reduce()最经常使用的场景就是从一堆值中生成一个值。用这么复杂的函数去求一个最大或最小值,你是否是以为设计者有病。其实否则,由于“大”和“小”或者“求和”有时会有不一样的语义。

需求:从一组单词中找出最长的单词。这里“大”的含义就是“长”。

// 找出最长的单词
Stream<String> stream = Stream.of("I", "love", "you", "too");
Optional<String> longest = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2);
//Optional<String> longest = stream.max((s1, s2) -> s1.length()-s2.length());
System.out.println(longest.get());

上述代码会选出最长的单词love,其中Optional是(一个)值的容器,使用它能够避免null值的麻烦。固然可使用Stream.max(Comparator<? super T> comparator)方法来达到同等效果,但reduce()自有其存在的理由。

img

需求:求出一组单词的长度之和。这是个“求和”操做,操做对象输入类型是String,而结果类型是Integer

// 求单词长度之和
Stream<String> stream = Stream.of("I", "love", "you", "too");
Integer lengthSum = stream.reduce(0, // 初始值 // (1)
        (sum, str) -> sum+str.length(), // 累加器 // (2)
        (a, b) -> a+b); // 部分和拼接器,并行执行时才会用到 // (3)
// int lengthSum = stream.mapToInt(str -> str.length()).sum();
System.out.println(lengthSum);

上述代码标号(2)处将i. 字符串映射成长度,ii. 并和当前累加和相加。这显然是两步操做,使用reduce()函数将这两步合二为一,更有助于提高性能。若是想要使用map()sum()组合来达到上述目的,也是能够的。

reduce()擅长的是生成一个值,若是想要从Stream生成一个集合或者Map等复杂的对象该怎么办呢?终极武器collect()横空出世!

终极武器collect()

不夸张的讲,若是你发现某个功能在Stream接口中没找到,十有八九能够经过collect()方法实现。collect()Stream接口方法中最灵活的一个,学会它才算真正入门Java函数式编程。先看几个热身的小例子:

// 将Stream转换成容器或Map
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(Collectors.toList()); // (1)
// Set<String> set = stream.collect(Collectors.toSet()); // (2)
// Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length)); // (3)

上述代码分别列举了如何将Stream转换成ListSetMap。虽然代码语义很明确,但是咱们仍然会有几个疑问:

  1. Function.identity()是干什么的?
  2. String::length是什么意思?
  3. Collectors是个什么东西?
接口的静态方法和默认方法

Function是一个接口,那么Function.identity()是什么意思呢?这要从两方面解释:

  1. Java 8容许在接口中加入具体方法。接口中的具体方法有两种,default方法和static方法,identity()就是Function接口的一个静态方法。
  2. Function.identity()返回一个输出跟输入同样的Lambda表达式对象,等价于形如t -> t形式的Lambda表达式。

上面的解释是否是让你疑问更多?不要问我为何接口中能够有具体方法,也不要告诉我你以为t -> tidentity()方法更直观。我会告诉你接口中的default方法是一个无奈之举,在Java 7及以前要想在定义好的接口中加入新的抽象方法是很困难甚至不可能的,由于全部实现了该接口的类都要从新实现。试想在Collection接口中加入一个stream()抽象方法会怎样?default方法就是用来解决这个尴尬问题的,直接在接口中实现新加入的方法。既然已经引入了default方法,为什么再也不加入static方法来避免专门的工具类呢!

方法引用

诸如String::length的语法形式叫作方法引用(method references),这种语法用来替代某些特定形式Lambda表达式。若是Lambda表达式的所有内容就是调用一个已有的方法,那么能够用方法引用来替代Lambda表达式。方法引用能够细分为四类:

方法引用类别 举例
引用静态方法 Integer::sum
引用某个对象的方法 list::add
引用某个类的方法 String::length
引用构造方法 HashMap::new

咱们会在后面的例子中使用方法引用。

收集器

相信前面繁琐的内容已完全打消了你学习Java函数式编程的热情,不过很遗憾,下面的内容更繁琐。但这不能怪Stream类库,由于要实现的功能自己很复杂。

img

收集器(Collector)是为Stream.collect()方法量身打造的工具接口(类)。考虑一下将一个Stream转换成一个容器(或者Map)须要作哪些工做?咱们至少须要两样东西:

  1. 目标容器是什么?是ArrayList仍是HashSet,或者是个TreeMap
  2. 新元素如何添加到容器中?是List.add()仍是Map.put()

若是并行的进行规约,还须要告诉collect() 3. 多个部分结果如何合并成一个。

结合以上分析,collect()方法定义为<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner),三个参数依次对应上述三条分析。不过每次调用collect()都要传入这三个参数太麻烦,收集器Collector就是对这三个参数的简单封装,因此collect()的另外一定义为<R,A> R collect(Collector<? super T,A,R> collector)Collectors工具类可经过静态方法生成各类经常使用的Collector。举例来讲,若是要将Stream规约成List能够经过以下两种方式实现:

https://objcoding.com/2019/03...


本篇文章由一文多发平台ArtiPub自动发布

相关文章
相关标签/搜索