为了账号安全,请及时绑定邮箱和手机立即绑定

​闷棍暴打面试官 JDK源码系列 (一) 打破 lambda 问到底 !

标签:
Java 算法

家喻户晓的 lambda

Java 8 (又称为 jdk 1.8) 是 Java 语言开发的一个主要版本。 Oracle 公司于 2014 年 3 月 18 日发布 Java 8 ,它支持函数式编程,新的 JavaScript 引擎,新的日期 API,新的Stream API 等。

  • Lambda 表达式 − Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中)。
  • Stream API −新添加的Stream API(java.util.stream) 把真正的Lambda函数式编程风格引入到Java中。

我们在学习基于lambda 开发的众多 api 的时候一定要弄清楚的一个问题. lambda 语法与基于lambda 语法的api类之间到底有什么关系!

首先看看 lambda 风格代码

   @FunctionalInterface
    interface MathOperation {
        String sayMessage(Integer b);
    }

    // lambda 原生函数风格
    public String lambdaApi(Integer data){
        String salutation = "Hello lambda";
        MathOperation addition = (Integer b) -> salutation  +  b.toString();
        return addition.sayMessage(data);
    }

它有以下特点.

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

基于lambda 风格的api代码

    // lambda 流api 1.1
    public List lambdaStreamApi(List<MbRelatedWeightResource> data){
        return data.stream().sorted(comparing(MbRelatedWeightResource::getMlRecommended)
            .thenComparing(MbRelatedWeightResource::getThroughWeight)
            .thenComparing(MbRelatedWeightResource::getHeat).reversed()).collect(Collectors.toList());
    }

    // lambda 并行流api 1.2
    public List lambdaParallelStreamApi(List<MbRelatedWeightResource> data){
        return data.parallelStream().sorted(comparing(MbRelatedWeightResource::getMlRecommended)
            .thenComparing(MbRelatedWeightResource::getThroughWeight)
            .thenComparing(MbRelatedWeightResource::getHeat).reversed()).collect(Collectors.toList());
    }

    // lambda 缩减api 1.3
    public Integer lambdaCurtailApi(List<MbRelatedWeightResource> data){
        return data.stream()
            .map(MbRelatedWeightResource::getThroughWeight)
            .reduce(BigDecimal.ZERO.intValue(), (a,b) -> a+b);
    }

    // lambda 映射api 1.4
    public Map lambdaMaplApi(List<MbRelatedWeightResource> data){
        // 把 MbRelatedWeightResource 转换为Map[ThroughWeight]=Heat:
        return data.stream().map(kv -> {
            Map<Integer,Integer> map = new HashMap<>();
            map.put(kv.getThroughWeight(),kv.getHeat());
            return map;
        }).reduce(new HashMap<>(),(m1,m2)->{
            m1.putAll(m2);
            return m2;
        });
    }

    // lambda Spliterator api 1.5
    public void lambdaSpliteratorApi(List<MbRelatedWeightResource> data){
        System.out.println(data.stream().spliterator().getExactSizeIfKnown());
        data.stream().spliterator().trySplit().forEachRemaining(System.out::println);
    }

   // 非流api 的 lambda Runnable api 2.1
    public static Thread getThread() throws InterruptedException {
        return  new Thread(() -> System.out.println("In Java8, Lambda expression"));
    }

    // 非流api 的 lambda PathMatcher api 2.2
    public static PathMatcher getPathMatcher(){
        return Objects::nonNull;
    }

    // 非流api 的 lambda PathMatcher api 2.2
    public static AfterTestExecutionCallback getAfterTestExecutionCallback() {
        return (ExtensionContext context) ->{
            if(Objects.nonNull(context)){
                throw new NullPointerException();
            }
        };
    }

从书写代码的角度来看待 lambda

  • 原生lambda: 使用前要有一个相关的接口, 然后你要提前用lambda风格做该接口的入参出参, 最后调用即可, 书写上类似于匿名类.
  • @FunctionInterface : 大部分匿名内部类在jdk1.8之前大都只有一个接口, 是让开发者用来写匿名类的. 1.8 之后可以用 lambda 风格来写了, 而且两种风格可以互相转化.
  • :: 静态方法引用 (Double Colon运算符): 静态方法引用允许我们使用符合该类方法的入参与泛型, 作为简单lambda表达式的替代品, 不限于该方法是否被 static。
  • 流式api: 通过阅读 lambda 流式api 源码发现, 每个操作都会返回下一个操作的超集, 然后调用对应api进入下一个环节, 如果遇到需要操作元素时 @Function 子集api 进行 lambda 风格操作.

源码入口: github.com/XiaoZiShan/studey-advance/blob/2b644b68c06f952bdafffe04e24db743989d8d4e/src/test/java/advance/basearithmetic/lambda/LambdaDemoSolutionTest.java
在这里插入图片描述

真正的 lambda

为什么所有的 lambda 接口都被 @FunctionInterface 标注?

  1. 主要作用就是使lambda变得好维护, 如果不符合lambda 接口风格, 编译时就会报错 ( 因为lambda 只需要一个接口 ).
  2. 如果没有 @FunctionalInterface 注解, 其实也可以使用 lambda, 但是写完代码后, 80%的时间都是给别人看的, 如果其他维护者不指定该接口是lambda风格, 在上面增增减减. 就不易长期维护
  3. 该注解就类似于lambda类型检测
 /*
        匿名函数式接口风格
        1. 接口类只能定义一个接口
        2. jdk 1.8 后接口默认使用 public abstract 修饰
        3. 可以写多个 default 方法, 以及 Override Object 类的抽象接口
        4. 如果 FunctionalInterface 的抽象方法具有相同的签名,则它们可以由其他功能接口扩展。
        5. 用lambda表达式代替了内部类,但这两个概念在一个重要方面有所不同:作用域。
     */
    @FunctionalInterface
    public interface Demo1 {
        
        String method(String string);
        
        default String def1() {
            return "?:";
        }
        
        default String def2() {
            return "?:";
        }
        
        @Override
        boolean equals(Object obj);
    }
    
    public String add1(String string, Demo1 demo1) {
        return demo1.method(string);
    }

上述讲的代码, 比较冗余, 在 java.util.function 包中就提供了很多现成的 api···

    public String add2(String string, Function<String, String> fn) {
        return fn.apply(string);
    }

在这里插入图片描述
在这里插入图片描述

上一小节总结了 lambda 与 内部类语法上是可以互相转换的. 我们来试一下

    /*
     * 不使用 lambda 调用 stream api
     */
    public List<MbRelatedWeightResource> noLambdaUseStreamApi() {
        List<MbRelatedWeightResource> compareToList = new ArrayList<>();
        compareToList.add(new MbRelatedWeightResource(100, 51, 21));
        compareToList.add(new MbRelatedWeightResource(100, 51, 20));
        compareToList.add(new MbRelatedWeightResource(101, 1, 1));
        compareToList.add(new MbRelatedWeightResource(100, 50, 20));
        // 调用比较器进行排序
    
        return compareToList.stream().sorted(new Comparator<MbRelatedWeightResource>() {
            @Override
            public int compare(MbRelatedWeightResource o1, MbRelatedWeightResource o2) {
                return o1.getHeat() - o2.getHeat();
            }
        }).filter(new Predicate<MbRelatedWeightResource>() {
            @Override
            public boolean test(MbRelatedWeightResource mbRelatedWeightResource) {
                return mbRelatedWeightResource.getHeat() != 1;
            }
        }).collect(Collectors.toList());
    }

在使用 lambda 表达式时 方法抛出异常, 我们应该如何处理? 这种情况能不能继续写 lambda 了?

/*
     * 自定义 Lambda 包装器处理异常
     */
    protected static <T, E extends Exception> Consumer<T> handlingConsumerWrapper(ThrowingConsumer<T> throwingConsumer,
            Class<E> exceptionClass) {
        
        return i -> {
            try {
                throwingConsumer.accept(i);
            } catch (Exception ex) {
                try {
                    E exCast = exceptionClass.cast(ex);
                    System.err.println(
                            "捕获指定异常 : " + exCast.getMessage());
                } catch (ClassCastException ccEx) {
                    throw new RuntimeException(ex);
                }
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        };
    }
	
	 @Test
    @Order(5)
    @DisplayName("自定义 Lambda 包装器处理异常")
    void HandlingConsumerWrapper(){
        List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
        integers.forEach(
                handlingConsumerWrapper(
                        i -> System.out.println(50 / i),
                        ArithmeticException.class));
    }

源码入口: github.com/XiaoZiShan/studey-advance/blob/2b644b68c06f952bdafffe04e24db743989d8d4e/src/test/java/advance/basearithmetic/lambda/LambdaEasySolutionTest.java

在这里插入图片描述

更多lambda语法请参考
github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-lambdas

写一个 lambda 支持的 Class

使用 lambda 书写构造者类

import javax.naming.OperationNotSupportedException;
import java.util.Optional;
import java.util.function.Function;

/**
 * Created by cglib lazy Stream 入参!
 */
@FunctionalInterface
public interface CglibLambdaArgsBuilder<T,L,R> {
    
    R build(T t,L a) throws OperationNotSupportedException;
    
    default <V> CglibLambdaArgsBuilder<T, L,V> andThen( Function<? super R, ? extends V> after) {
        return (T t, L a) -> Optional.of(after).get().apply(build(t, a));
    }
}

根据参数 设置cglib懒加载, 返回 lambda api 对象

import lombok.AllArgsConstructor;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.LazyLoader;

import javax.naming.OperationNotSupportedException;
import java.util.Collection;
import java.util.Optional;
import java.util.stream.Stream;

/**
 * Created by cglib lambda 操作 Lazy Bean 工具类!
 */

public class CglibLambdaLazyBeanStream <T> {
   
    // cglib 要求data 入参必须有空参构造 @NoArgsConstructor
    private T data;
    
    private Boolean areLazy;
    
    public CglibLambdaLazyBeanStream() throws OperationNotSupportedException {
        throw new OperationNotSupportedException();
    }
    
    /**
     * 构造函数
     * @param data 需转换数据
     * @param b 是否开启懒加载
     */
    public CglibLambdaLazyBeanStream(T data,Boolean b) {
        this.data = data;
        this.areLazy = b;
    }
    
    // 处理 VO
    public Optional toVO() {
        if (Boolean.TRUE.equals(areLazy)){
            LazyLoader lazy = new ConcreteClassLazyLoader(this.data.getClass(),data);
            return Optional.ofNullable(Enhancer.create(this.data.getClass(),lazy));
        }else {
            return Optional.ofNullable(data);
        }
    }
    
    // 处理 Collection
    public Stream<T> toCollection() throws OperationNotSupportedException {
        if (Boolean.FALSE.equals(data instanceof Collection)){
            throw new OperationNotSupportedException();
        }
        
        if (Boolean.TRUE.equals(areLazy)){
            LazyLoader lazy = new ConcreteClassLazyLoader(this.data.getClass(),data);
            return Stream.of((T) Enhancer.create(this.data.getClass(),lazy));
        }else {
            return  Stream.of(data);
        }
    }
    
    // 更多功能自行扩展
}


@AllArgsConstructor
class ConcreteClassLazyLoader<E,T> implements LazyLoader {
    
    private Class<E> exceptionClass;
    
    private T data;
    
    @Override
    public E loadObject() throws Exception {
        System.out.println("LazyLoader loadObject() ...");
        E exCast = exceptionClass.cast(data);
        return exCast;
    }
}

单元测试


import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import studey.advance.basearithmetic.lambda.CglibLambdaArgsBuilder;
import studey.advance.basearithmetic.lambda.CglibLambdaLazyBeanStream;
import studey.advance.datastructure.pojo.MbRelatedWeightResource;

import javax.naming.OperationNotSupportedException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @see studey.advance.basearithmetic.lambda.CglibLambdaLazyBeanStream 测试用例
 */
@DisplayName("使用 lambda 改造 cglib lazy bean 赋值")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class CglibLambdaLazyBeanStreamTest  {
    
    @Test
    @Order(1)
    @DisplayName("bean lazy Maping to VO")
    void toVO() throws Throwable {
        CglibLambdaArgsBuilder<MbRelatedWeightResource, Boolean, CglibLambdaLazyBeanStream> builder = CglibLambdaLazyBeanStream::new;
        CglibLambdaLazyBeanStream clStream = builder.build(new MbRelatedWeightResource(100, 51, 20),Boolean.TRUE);
        MbRelatedWeightResource mrwr1 = (MbRelatedWeightResource)clStream.toVO().get();
        // System.out.println(mrwr1);
        MbRelatedWeightResource mrwr = (MbRelatedWeightResource)clStream.toVO().orElseThrow(RuntimeException::new);
        System.out.println(mrwr.getHeat());
        System.out.println(mrwr.getMlRecommended());
        System.out.println(mrwr.getThroughWeight());
    }
    
    @Test
    @Order(2)
    @DisplayName("bean lazy Maping to Collection")
    void toCollection() throws OperationNotSupportedException {
        List<MbRelatedWeightResource> comparatorList = new ArrayList<>();
        comparatorList.add(new MbRelatedWeightResource(100, 51, 20));
        comparatorList.add(new MbRelatedWeightResource(101, 1, 1));
        comparatorList.add(new MbRelatedWeightResource(100, 50, 20));
        CglibLambdaArgsBuilder<List<MbRelatedWeightResource>, Boolean, CglibLambdaLazyBeanStream> builder = CglibLambdaLazyBeanStream::new;
        CglibLambdaLazyBeanStream<MbRelatedWeightResource> clStream = builder.build(comparatorList, Boolean.TRUE);
        List<MbRelatedWeightResource> list1 = clStream.toCollection().collect(Collectors.toList());
        // System.out.println(list1);
        Stream<MbRelatedWeightResource> stream = clStream.toCollection();
        System.out.println(stream.collect(Collectors.toList()));
        
    }
}

源码入口: github.com/XiaoZiShan/studey-advance/blob/2b644b68c06f952bdafffe04e24db743989d8d4e/src/test/java/advance/basearithmetic/lambda/CglibLambdaLazyBeanStreamTest.java

在这里插入图片描述

CGlib 性能怎么样?

Byte Buddy vs cglib vs javassist vs JDK proxy

在这里插入图片描述

第一行显示库用18种不同方法(作为无操作存根)实现接口所需的时间。基于这些运行时类,第二行显示在生成的类的实例上调用存根所需的时间。

在此度量中,Byte Buddy和cglib表现最佳,因为这两个库都允许您将固定的返回值硬编码到生成的类中,而javassist和JDK代理仅允许注册适当的回调。

反编译上述 lambda

反编译前先问个问题, lambda forech 和 原生for循环, 到底那个快?
网上很多博客都说 lambda forech 很慢, 原生 for循环比较快, 那么真相到底是什么呢?

在这里插入图片描述

奇怪! 为什么第二次耗时比第一次少这么多? 是偶然现象吗?
其实如果仔细看上述单元测试例子, 普遍存在这种情况, 既然要问到底, 就反编译下源码吧!

java -verbose:class -verbose:jni -verbose:gc -XX:+PrintCompilation studey.advance.basearithmetic.lambda**

解释一下命令的意思

输出jvm载入类的相关信息
-verbose:class

输出native方法调用的相关情况
-verbose:jni

输出每次GC的相关情况
-verbose:gc

当一个方法被编译时打印相关信息
-XX:+PrintCompilation

先看看 CglibLambda 相关汇编代码,

[0.028s][info][class,load] java.lang.invoke.LambdaMetafactory source: shared objects file
[0.028s][info][class,load] java.lang.invoke.MethodHandles$Lookup source: shared objects file
[0.028s][info][class,load] java.lang.invoke.MethodType$ConcurrentWeakInternSet source: shared objects file
[0.028s][info][class,load] java.lang.invoke.MethodType$ConcurrentWeakInternSet$WeakEntry source: shared objects file
[0.028s][info][class,load] java.lang.Void source: shared objects file
[0.028s][info][class,load] java.lang.invoke.MethodTypeForm source: shared objects file
[0.028s][info][class,load] java.lang.invoke.MethodHandles source: shared objects file
[0.028s][info][class,load] java.lang.invoke.MemberName$Factory source: shared objects file

省略无用代码, 关注 java.lang.invoke 包...

[0.029s][info][class,load] java.lang.invoke.MethodHandleImpl source: shared objects file
[0.029s][info][class,load] java.lang.invoke.Invokers source: shared objects file
[Dynamic-linking native method java.lang.String.intern ... JNI]
[0.029s][info][class,load] java.lang.invoke.LambdaForm$Kind source: shared objects file
[0.029s][info][class,load] java.lang.NoSuchMethodException source: shared objects file
     28   29     n 0       java.lang.invoke.MethodHandle::linkToStatic(LLLLLLL)L (native)   (static)
[0.029s][info][class,load] java.lang.invoke.LambdaForm$BasicType source: shared objects file
[0.029s][info][class,load] java.lang.invoke.LambdaForm$Name source: shared objects file

我们可以结合JIT编译时间,结合JVM载入类的日志发现两个结论:

凡是使用了Lambda,JVM会额外加载 LambdaMetafactory类,且耗时较长
在第二次调用Lambda方法时,JVM就不再需要额外加载 LambdaMetafactory类,因此执行较快
完美印证了之前提出的问题:为什么第一次 foreach 慢,以后都很快,但这就是真相吗?我们继续往下看

匿名内部类在编译阶段会多出一个类,而Lambda不会,它仅会多生成一个函数
该函数会在运行阶段,会通过LambdaMetafactory工厂来生成一个class,进行后续的调用

为什么Lamdba要如此实现?

匿名内部类有一定的缺陷:

  1. 编译器为每个匿名内部类生成一个新的类文件,生成许多类文件是不可取的,因为每个类文件在使用之前都需要加载和验证,这会影响应用程序的启动性能,加载可能是一个昂贵的操作,包括磁盘I/O和解压缩JAR文件本身。
  2. 如果lambdas被转换为匿名内部类,那么每个lambda都有一个新的类文件。由于每个匿名内部类都将被加载,它将占用JVM的元空间,如果JVM将每个此类匿名内部类中的代码编译为机器码,那么它将存储在代码缓存中。
  3. 此外,这些匿名内部类将被实例化为单独的对象。因此,匿名内部类会增加应用程序的内存消耗。
  4. 最重要的是,从一开始就选择使用匿名内部类来实现lambdas,这将限制未来lambda实现更改的范围,以及它们根据未来JVM改进而演进的能力

小结

实践证明, 计算 lambda 循环的耗时,需要排除第一次init调用后, 后续平均速度并不慢, 而且还能有效减少代码行数, 何乐而不为呢? 试试把java8项目改成 lambda风格吧!

点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消