函数式编程的原理

  • 所有的事情都是必须要做的,当你在某个地方少做了,那么肯定有别的地方多做了。
  • 上面这句话可以表达出我们简单的使用函数式编程,而底层必然是JDK为我们做了某些适配。看一个简单的例子 ```java public class Test { public static void main(String[] args) {

    }

    public static void print(List list) {

    1. list.stream().filter(test -> test >= 1).count();

    } }

  1. - javap -verbose Test.class
  2. ```java
  3. // print 方法
  4. public static void print(java.util.List<java.lang.Integer>);
  5. descriptor: (Ljava/util/List;)V
  6. flags: ACC_PUBLIC, ACC_STATIC
  7. Code:
  8. stack=2, locals=1, args_size=1
  9. 0: aload_0
  10. 1: invokeinterface #2, 1 // InterfaceMethod java/util/List.stream:()Ljava/util/stream/Stream;
  11. 6: invokedynamic #3, 0 // InvokeDynamic #0:test:()Ljava/util/function/Predicate;
  12. 11: invokeinterface #4, 2 // InterfaceMethod java/util/stream/Stream.filter:(Ljava/util/function/Predicate;)Ljava/util/stream/Stream;
  13. 16: invokeinterface #5, 1 // InterfaceMethod java/util/stream/Stream.count:()J
  14. 21: pop2
  15. 22: return
  16. LineNumberTable:
  17. line 15: 0
  18. line 16: 22
  19. LocalVariableTable:
  20. Start Length Slot Name Signature
  21. 0 23 0 list Ljava/util/List;
  22. LocalVariableTypeTable:
  23. Start Length Slot Name Signature
  24. 0 23 0 list Ljava/util/List<Ljava/lang/Integer;>;
  25. Signature: #27 // (Ljava/util/List<Ljava/lang/Integer;>;)V
  26. }
  27. SourceFile: "Test.java"
  28. // 内部类
  29. InnerClasses:
  30. public static final #72= #71 of #75; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
  31. // 引导方法保存由invokedynamic使用的引导方法
  32. BootstrapMethods:
  33. 0: #39 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  34. Method arguments:
  35. #40 (Ljava/lang/Object;)Z
  36. #41 invokestatic com/ldl/function/source/Test.lambda$print$0:(Ljava/lang/Integer;)Z
  37. #42 (Ljava/lang/Integer;)Z
  • Lambda的本质:内部类,结果类似如下 java -Djdk.internal.lambda.dumpProxyClasses

    1. static final class Main$$Lambda$1 implements Predicate<Integer> {
    2. private Main$$Lambda$1() {
    3. }
    4. @Override
    5. public boolean test(Integer x) {
    6. return x > 1;
    7. }
    8. }
  • LambdaMetafactory.metafactory(...) 方法,此方法就是在内存中动态生成字节码文件, ```java

public class LambdaMetafactory { public static CallSite metafactory(MethodHandles.Lookup caller, String invokedName, MethodType invokedType, MethodType samMethodType, MethodHandle implMethod, MethodType instantiatedMethodType) throws LambdaConversionException { AbstractValidatingLambdaMetafactory mf; mf = new InnerClassLambdaMetafactory(caller, invokedType, invokedName, samMethodType, implMethod, instantiatedMethodType, false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY); mf.validateMetafactoryArgs(); return mf.buildCallSite(); } }

  1. ```java
  2. final class InnerClassLambdaMetafactory extends AbstractValidatingLambdaMetafactory {
  3. // 计数器
  4. private static final AtomicInteger counter = new AtomicInteger(0);
  5. // 生成innerClass
  6. public InnerClassLambdaMetafactory(MethodHandles.Lookup caller,
  7. MethodType invokedType,
  8. String samMethodName,
  9. MethodType samMethodType,
  10. MethodHandle implMethod,
  11. MethodType instantiatedMethodType,
  12. boolean isSerializable,
  13. Class<?>[] markerInterfaces,
  14. MethodType[] additionalBridges)
  15. throws LambdaConversionException {
  16. super(caller, invokedType, samMethodName, samMethodType,
  17. implMethod, instantiatedMethodType,
  18. isSerializable, markerInterfaces, additionalBridges);
  19. implMethodClassName = implDefiningClass.getName().replace('.', '/');
  20. implMethodName = implInfo.getName();
  21. implMethodDesc = implMethodType.toMethodDescriptorString();
  22. implMethodReturnClass = (implKind == MethodHandleInfo.REF_newInvokeSpecial)
  23. ? implDefiningClass
  24. : implMethodType.returnType();
  25. constructorType = invokedType.changeReturnType(Void.TYPE);
  26. // 生成 Lambda 表达式的类名
  27. // 这里我们发现,每次生成Class名字的时候 都是自增的,保证其是唯一的
  28. lambdaClassName = targetClass.getName().replace('.', '/') + "$$Lambda$" + counter.incrementAndGet();
  29. cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
  30. int parameterCount = invokedType.parameterCount();
  31. if (parameterCount > 0) {
  32. argNames = new String[parameterCount];
  33. argDescs = new String[parameterCount];
  34. for (int i = 0; i < parameterCount; i++) {
  35. argNames[i] = "arg$" + (i + 1);
  36. argDescs[i] = BytecodeDescriptor.unparse(invokedType.parameterType(i));
  37. }
  38. } else {
  39. argNames = argDescs = EMPTY_STRING_ARRAY;
  40. }
  41. }
  42. }
  • 事实上,生成在内存中的内部类,被JVM管理,其会被缓存起来,但是缓存到哪儿了还没有验证,如何验证其只生成了一次?
  • 编写一个Spring Boot Web应用,然后编写测试方法,然后在LambdaMetafactory#metafactoryInnerClassLambdaMetafactory#InnerClassLambdaMetafactory打断点,其中首次请求如下方法的时候,InnerClassLambdaMetafactory#InnerClassLambdaMetafactory的lambdaClassName会生产值,但是后续再次调用的时候,则没有经过这个值。所以判定是JVM将其保存在内存中了。

    1. @RestController
    2. @RequestMapping("/test")
    3. public class TestFacade {
    4. @Resource
    5. private OrderService orderService;
    6. @GetMapping("/testLambda")
    7. public void testLambda() {
    8. List<Integer> list = Arrays.asList(1, 23, 4, 5);
    9. list.stream().filter(x -> (x + 1) > 20).count();
    10. }
    11. }

    影响性能的因素

  • 影响并行流性能的主要因素有5个,依次分析如下

  • 数据大小
  • 输入数据的大小会影响并行化处理对性能的提升。将问题分解之后并行化处理,再将结果合并会带来额外的开销。因此只有数据足够大、每个数据处理管道花费的时间足够多时,并行化处理才有意义。
  • 源数据结构
  • 每个管道的操作都基于一些初始数据源,通常是集合。将不同的数据源分割相对容易,这里的开销影响了在管道中并行处理数据时到底能带来多少性能上的提升。
  • 装箱
  • 处理基本类型比处理装箱类型要快。
  • 核的数量
  • 极端情况下,只有一个核,因此完全没必要并行化。显然,拥有的核越多,获得潜在性能提升的幅度就越大。在实践中,核的数量不单指你的机器上有多少核,更是指运行时你的机器能使用多少核。这也就是说同时运行的其他进程,或者线程关联性(强制线程在某些核或CPU上运行)会影响性能。
  • 单元处理开销
  • 比如数据大小,这是一场并行执行花费时间和分解合并操作开销之间的战争。花在流中每个元素身上的时间越长,并行操作带来的性能提升越明显。
  • 根据性能的好坏,将核心类库提供的通用数据结构分成以下3组
  • 性能好
  • ArrayList、数组或IntStream.range,这些数据结构支持随机读取,也就是说它们能轻而易举地被任意分解。
  • 性能一般
  • HashSet、TreeSet,这些数据结构不易公平地被分解,但是大多数时候分解是可能的。
  • 性能差
  • 有些数据结构难于分解,比如,可能要花O(N)的时间复杂度来分解问题。其中包括LinkedList,对半分解太难了。还有Streams.iterate和BufferedReader.lines,它们长度未知,因此很难预测该在哪里分解。
  • 内存影响性能的因素
  • 上面我们做了测试,JVM将Lambda和内部类会有个映射,所以只有第一次执行的时候,才会生成内存中的字节码文件,这是其慢的主要原因。并且在调用方法的时候,会创建对象,这是慢的究极原因,我认为其每次去处理Lambda都要去新建对象,这样的话总体性能就比较低 (这是我个人理解,可能不对)。

    推荐文章

  • Java8探讨与分析匿名内部类、Lambda表达式、方法引用的底层实现:https://www.cnblogs.com/chenjingquan/p/10574320.html

  • Lambda表达式的实现方式:https://blog.csdn.net/zxhoo/article/details/38495085