家喻户晓的 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 标注?
- 主要作用就是使lambda变得好维护, 如果不符合lambda 接口风格, 编译时就会报错 ( 因为lambda 只需要一个接口 ).
- 如果没有 @FunctionalInterface 注解, 其实也可以使用 lambda, 但是写完代码后, 80%的时间都是给别人看的, 如果其他维护者不指定该接口是lambda风格, 在上面增增减减. 就不易长期维护
- 该注解就类似于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要如此实现?
匿名内部类有一定的缺陷:
- 编译器为每个匿名内部类生成一个新的类文件,生成许多类文件是不可取的,因为每个类文件在使用之前都需要加载和验证,这会影响应用程序的启动性能,加载可能是一个昂贵的操作,包括磁盘I/O和解压缩JAR文件本身。
- 如果lambdas被转换为匿名内部类,那么每个lambda都有一个新的类文件。由于每个匿名内部类都将被加载,它将占用JVM的元空间,如果JVM将每个此类匿名内部类中的代码编译为机器码,那么它将存储在代码缓存中。
- 此外,这些匿名内部类将被实例化为单独的对象。因此,匿名内部类会增加应用程序的内存消耗。
- 最重要的是,从一开始就选择使用匿名内部类来实现lambdas,这将限制未来lambda实现更改的范围,以及它们根据未来JVM改进而演进的能力
小结
实践证明, 计算 lambda 循环的耗时,需要排除第一次init调用后, 后续平均速度并不慢, 而且还能有效减少代码行数, 何乐而不为呢? 试试把java8项目改成 lambda风格吧!