关于java:Java-Lambda表达式知多少

42次阅读

共计 6670 个字符,预计需要花费 17 分钟才能阅读完成。

1. 匿名外部类实现

匿名外部类依然是一个类,只是不须要程序员显示指定类名,编译器会主动为该类取名。因而如果有如下模式的代码,编译之后将会产生两个 class 文件:

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 文件别离是主类和匿名外部类产生的:

进一步剖析主类 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 文件:

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

编译之后的后果:

通过 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 之所以费这么大功夫引入函数式编程,起因有二:

  1. 代码简洁 函数式编程写出的代码简洁且用意明确,应用 stream 接口让你从此辞别 for 循环。
  2. 多核敌对 ,Java 函数式编程使得编写并行程序从未如此简略,你须要的全副就是调用一下parallel() 办法。

图中 4 种 stream 接口继承自 BaseStream,其中IntStream, LongStream, DoubleStream 对应三种根本类型(int, long, double,留神不是包装类型),Stream对应所有残余类型的 stream 视图。为不同数据类型设置不同 stream 接口,能够 1. 进步性能,2. 减少特定接口函数。

尽管大部分状况下 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()

函数原型为 <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() 自有其存在的理由。

需要:求出一组单词的长度之和。这是个“求和”操作,操作对象输出类型是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 类库,因为要实现的性能自身很简单。

收集器(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 主动公布

正文完
 0