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.classpublic 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.classpublic 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转换成容器或MapStream<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主动公布