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

时间:2022-02-12 06:06:05

这周开学,上了两天感觉课好多,学校现在还停水,宿舍网络也还没通,简直爆炸,感觉能静下心看书的时间越来越少了...寒假还有些看过书之后的存货,现在写一点发出来。加上假期两个月左右都过去了书才看了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.

初探Lambda表达式/Java多核编程【4】Lambda变量捕获的更多相关文章

  1. 初探Lambda表达式/Java多核编程【1】从集合到流

    从集合到流 接上一小节初探Lambda表达式/Java多核编程[0]从外部迭代到内部迭代,本小节将着手使用"流"这一概念进行"迭代"操作. 首先何为" ...

  2. 初探Lambda表达式/Java多核编程【2】并行与组合行为

    今天又翻了一下书的目录,第一章在这之后就结束了.也就是说,这本书所涉及到的新的知识已经全部点到了. 书的其余部分就是对这几个概念做一些基础知识的补充以及更深层次的实践. 最后两个小节的内容较少,所以合 ...

  3. 初探Lambda表达式/Java多核编程【3】Lambda语法与作用域

    接上一篇:初探Lambda表达式/Java多核编程[2]并行与组合行为 本节是第二章开篇,前一章已经浅显地将所有新概念点到,书中剩下的部分将对这些概念做一个基础知识的补充与深入探讨实践. 本章将介绍L ...

  4. 初探Lambda表达式/Java多核编程【0】从外部迭代到内部迭代

    开篇 放假前从学校图书馆中借来一本书,Oracle官方的<精通Lambda表达式:Java多核编程>. 假期已过大半才想起来还没翻上几页,在此先推荐给大家. 此书内容及其简洁干练,如果你对 ...

  5. Java 函数式编程(Lambda表达式)与Stream API

    1 函数式编程 函数式编程(Functional Programming)是编程范式的一种.最常见的编程范式是命令式编程(Impera Programming),比如面向过程.面向对象编程都属于命令式 ...

  6. Java 函数式编程和Lambda表达式

    1.Java 8最重要的新特性 Lambda表达式.接口改进(默认方法)和批数据处理. 2.函数式编程 本质上来说,编程关注两个维度:数据和数据上的操作. 面向对象的编程泛型强调让操作围绕数据,这样可 ...

  7. Lambda&amp&semi;Java多核编程-5-函数式接口与function包

    从前面的总结中我们知道Lambda的使用场景是实现一个函数式接口,那么本篇就将阐述一下何为函数式接口以及Java的function包中提供的几种函数原型. 函数式接口 早期也叫作SAM(Single ...

  8. Lambda&amp&semi;Java多核编程-6-方法与构造器引用

    在Lambda&Java多核编程-2-并行与组合行为一文中,我们对Stream<Contact>里的每一位联系人调用call()方法,并根据能否打通的返回结果过滤掉已经失效的项. ...

  9. Java函数式编程和lambda表达式

    为什么要使用函数式编程 函数式编程更多时候是一种编程的思维方式,是种方法论.函数式与命令式编程的区别主要在于:函数式编程是告诉代码你要做什么,而命令式编程则是告诉代码要怎么做.说白了,函数式编程是基于 ...

随机推荐

  1. mysql交互式连接&amp&semi;非交互式连接

    交互式操作:通俗的说,就是你在你的本机上打开mysql的客户端,就是那个黑窗口,在黑窗口下进行各种sql操作,当然走的肯定是tcp协议. 非交互式操作:就是你在你的项目中进行程序调用.比如一边是tom ...

  2. 【开源】MQTT推送服务器——zer0MqttServer(Java编写)

    目录 说明 功能 如何使用 参考帮助 说明 重要的放前面:V1.0版本是一个非常基础的版本,除了完整的MQTT协议实现外,其他功能什么都没做. MQTT 协议是 IBM 开发的即时通讯协议,相对于 I ...

  3. 转载:Android调用相册、拍照实现缩放、切割图片

    好几天没有写博客了,感觉都有点懈怠了.笔者参加了大学生第二届软件设计大赛,这几天 一直在弄大赛的事情,没有花些时间来整理博客.好在经过一些时日比赛的东西也弄得差不多了, 接下来就是将这段时间学习里面有 ...

  4. uva 10820

    /* 交表 _________________________________________________________________________________ #include &lt ...

  5. Cocos2d-JS目录说明

    frameworks---- 引擎所在,包含两个文件夹cocos2d-html5 和js-bindings.前者是html5引擎,后者是-x的引擎,外部接口是js写,但基础模块却是cpp来实现. sa ...

  6. influxdb和boltDB简介——底层本质类似LMDB,MVCC&plus;B&plus;树

    influxdb influxdb是最新的一个时间序列数据库,最新一两年才产生,但已经拥有极高的人气.influxdb 是用Go写的,0.9版本的influxdb对于之前会有很大的改变,后端存储有Le ...

  7. GitHub使用教程及常见错误解决

    1.下载Git并安装 Git for Windows Git-1.8.4-preview20130916.exe 按照默认步骤完成安装 2.设置SSH建立计算机与Github的链接 2.1 点击 开始 ...

  8. Delphi实现HTMLWebBrowser实现HTML界面

    HTML的界面有以下特点:图文混排,格式灵活,可以包含Flash.声音和视频等,实现图文声像的多媒体界面,而且易于建立和维护.另外,HTML的显示环境一般机器上都具备,通常不需要安装额外的软件.当然, ...

  9. Ubuntu离线安装软件包

    一.应用场景 a.当我们需要在多台电脑安装同一个软件,并且这个软件很大,下载需要很长时间时 b.需要安装软件的ubuntu不能上网 二.离线安装包的制作 2.1.通过如下指令下载XXXX软件所需要的d ...

  10. Linux 高性能服务器编程——多进程编程

    问题聚焦:     进程是Linux操作系统环境的基础.     本篇讨论以下几个内容,同时也是面试经常被问到的一些问题:     1 复制进程映像的fork系统调用和替换进程映像的exec系列系统调 ...