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

Posted 20K+

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了闷棍暴打面试官 JDK源码系列 打破 lambda 问到底 !相关的知识,希望对你有一定的参考价值。

给 20K+ 加星标, 助力 20K+


1


家喻户晓的 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中。
    更多请参考 https://www.runoob.com/java/java8-new-features.html

我们在学习基于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 风格操作.

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

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



2


真正的 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);
}

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

上一小节总结了 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));
}

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

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

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

https://zhuanlan.zhihu.com/p/147719155

3


写一个 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()));

}
}

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

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

CGlib 性能怎么样?

Byte Buddy vs cglib vs javassist vs JDK proxy

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

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

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

文章来源 https://www.jrebel.com/blog/java-code-generation-libraries-comparison

CGlib 更多扩展用法参考 https://www.runoob.com/w3cnote/cglibcode-generation-library-intro.html

反编译上述 lambda

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

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

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

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改进而演进的能力

结论引用: https://blog.csdn.net/weixin_40834464/article/details/107874710

更多反编译细节: https://colobu.com/2014/11/06/secrets-of-java-8-lambda/

小结

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





< END >
作者:萧子山


   扫码回复 "加群"  和  20K+ 程序员一起认识世界





深入浅出分享 Java 干货 , 找回对代码的 Passion , 助力月入 20K+ , 点击在看即可帮助更多人.

                                                                                  


以上是关于闷棍暴打面试官 JDK源码系列 打破 lambda 问到底 !的主要内容,如果未能解决你的问题,请参考以下文章

《完爆面试官》系列之Spring源码篇(上)

面试官 | Oracle JDK 和 OpenJDK 有什么区别?

BAT大厂面试官必问的HashMap相关面试题及部分源码分析

面试题系列论JDK源码一道经典面试题

面试官系统精讲Java源码及大厂真题系列之Java线程安全的解决办法

面试官:你分析过线程池源码吗?