初探Lambda表达式/Java多核编程【4】Lambda变量捕获

时间:2023-01-01 17:45:20

这周开学,上了两天感觉课好多,学校现在还停水,宿舍网络也还没通,简直爆炸,感觉能静下心看书的时间越来越少了...寒假还有些看过书之后的存货,现在写一点发出来。加上假期两个月左右都过去了书才看了1/7都不到...还得去续借一下,看来买书多看书少的毛病也得改一改,先致力于把剁手买的书啃完。

另外再次推荐下我现在看的这本书(详见第0篇),越看越费劲...干货非常多而且特别干,总之相比于其他书可以说是一页顶三页了,每一段都值得仔细琢磨,发现看不懂的还得调转方向先去填坑。

接上一篇:初探Lambda表达式/Java多核编程【3】Lambda语法与作用域

变量捕获

当使用匿名内部类并去实现其中的接口时,更多时候我们不会去访问定义在外部的变量,反而更加倾向于将其写成类似于静态方法的一种“函数”。

就如同前文中所举的键提取器键比较器之类的例子,作为单纯的行为(如Math类中的那些静态方法),不需要引入或操作任何外部量就能够达到目的。

同时在上一篇文章中我们也对Lambda之于外部变量的访问与继承有了粗浅的了解,书中这一小节的内容将使我们用更专业的术语来表述这一问题。

DoubleUnaryOperator doubleUnaryOperator = x -> Math.abs(x);
Stream.of(-0.1, 0.2, -0.3, 0.4, -0.5)
        .map(e -> doubleUnaryOperator.applyAsDouble(e))
        .forEach(e -> System.out.println(e));

此段代码中出现的所有Lambda都有一个特性,即只通过参数与返回值与外部交互:

  • x -> Math.abs(x) 接收x,返回其绝对值
  • e -> doubleUnaryOperator.applyAsDouble(e) 接收e,返回运算结果
  • e -> System.out.println(e) 接收e,无返回值

我们将这种类型的Lambda称为无捕获Lambda无状态Lambda,究其缘由应该是此种Lambda与外部或整个系统状态无关,不对外部量直接进行捕获,仅通过参数获得输入。

相反地,捕获Lambda则会访问外部量。

“捕获”指保留住Lambda对其外部环境的引用。

同样在前一篇文章中,我们介绍了Lambda与匿名内部类在访问外部变量时,都不允许有修改变量的倾向,即若:

final double a  = 3.141592;
double b = 3.141592;

DoubleUnaryOperator anotherDoubleUnaryOperator = x -> {
    a = 2; // ERROR
    b = 3; // ERROR
    return 0.0;
    };

则:

  • 无法改变final量的值
  • 不允许在Lambda表达式中修改使用的(外部)变量

相应的报错信息:

  • Cannot assign a value to final variable
  • Variable used in lambda expression should be final or effectively final

由是观之,我们将Lambda的这种变量捕获行为称之为值捕获更加确切。

在实际操作中,如果我们在Lambda体内或匿名内部类中要对外部的某些量进行操作,那么这种限制也是很容易被规避,因为即使数组是final的(即该数组不能再指向其他数组对象),里面的值依旧可变。

所以我们只要将那个需要被操作的外部变量包装进一个数组即可:

final double[] a = {3.141592};
final double[] b = {3.141592};

DoubleUnaryOperator anotherDoubleUnaryOperator = x -> {
    a[0] = 2; // COOL
    b[0] = 3; // ALSO COOL
    return 0.0;
};

也算是一个小技巧,在安卓开发中特别常见。

至于为何库的设计者如此竭力防止调用者修改外部变量,书中给出的解释是保证程序的正确性以及性能,很容易想到,如果我们将Lambda传递给另一个线程,此时如果Lambda在某一时刻修改了外部变量的值,便很容易引起多线程相关的bug。同时,我们若要解决线程安全问题,就需要给相关的外部变量上锁或使用volatile关键字,导致了计算任务分发至不同线程后的效率问题,又违背了Lambda的初衷。

关于线程安全,书中还出现了以下两个关键词:

  • 可见性问题
  • 竞态条件

看来还需要提升知识水平才能把多线程、高并发的坑给填了,现在还不大能看懂(╯-_-)╯╧╧。

抛开线程不谈,Lambda的生命周期可能比使用Lambda的方法调用的周期还要长,因此如果Lambda捕获的外部变量是可变的,还会引起与局部变量内存泄漏相关的问题。

对于常见的需要修改外部变量的场景:

final int[] sum = {0};
Stream.of(0, 1, 2, 3, 4, 5)
        .forEach(e -> sum[0] += e);

Stream给出了完美的解决方案:

sum[0] = Stream.of(0, 1, 2, 3, 4, 5)
        .mapToInt(e -> Integer.valueOf(e))
        .sum();

也可以是:

sum[0] = Stream.of(0, 1, 2, 3, 4, 5)
        .parallel()
        .mapToInt(e -> Integer.valueOf(e))
        .sum();

总之,面向并行的方式能够给我们带来更优越的性能。

小结

Lambda能够修改字段,不受final的限制:

private static class AClass {
    private String aString = "Animal Farm";

    void aMethod() {
        new Thread(() -> aString = aString.concat(" is good.")).run();
        System.out.println(aString);
    }
}

对字段aString的引用实际上由this.aString解引用而来,其中this充当了不可变局部变量的角色,进而使我们能够修改aString的值,与将外部变量包装进数组有异曲同工之妙。

本章代码:

import java.util.function.DoubleUnaryOperator;
import java.util.stream.Stream;

public class Bar {
    public static void main(String[] args) {
        DoubleUnaryOperator doubleUnaryOperator = x -> Math.abs(x);
        Stream.of(-0.1, 0.2, -0.3, 0.4, -0.5)
                .map(e -> doubleUnaryOperator.applyAsDouble(e))
                .forEach(e -> System.out.println(e));

//        final double a  = 3.141592;
//        double b = 3.141592;
//
//        DoubleUnaryOperator anotherDoubleUnaryOperator = x -> {
//            a = 2;
//            b = 3;
//            return 0.0;
//        };

        final double[] a = {3.141592};
        final double[] b = {3.141592};

        DoubleUnaryOperator anotherDoubleUnaryOperator = x -> {
            a[0] = 2;
            b[0] = 3;
            return 0.0;
        };

        final int[] sum = {0};
        Stream.of(0, 1, 2, 3, 4, 5)
                .forEach(e -> sum[0] += e);
        System.out.println(sum[0]);

        sum[0] = Stream.of(0, 1, 2, 3, 4, 5)
                .parallel()
                .mapToInt(e -> Integer.valueOf(e))
                .sum();
        System.out.println(sum[0]);

        new AClass().aMethod();
    }

    private static class AClass {
        private String aString = "Animal Farm";

        void aMethod() {
            new Thread(() -> aString = aString.concat(" is good.")).run();
            System.out.println(aString);
        }
    }
}

以及运行结果:

0.1
0.2
0.3
0.4
0.5
15
15
Animal Farm is good.