Java Lambda 学习指南(一)

Java8 Lambda表达式(一)Lambda表达式基础知识 1 Lambda表达式简介 1.1 Lambda表达式是什么 JDK1.8是目前最流行的JDK版本,在这一个版本中引入了Lamda表达式。Lambda表达式可以简化代码,让我们的代码更为优雅,而Lambda表达式之所以“优雅”,是因为使用Lambda表达式可以对接口进行简洁的实现,Lamdba表达式本身就是一个匿名函数。 1.2使用Lambda表达式实现Runnable接口... 阅读详情

原文:zh.annas-archive.org/md5/6C9C9AEB5C060CEC246B793E59A880AA

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书涵盖内容

本书深入探讨了 lambda 及其支持功能的特点;例如函数接口和类型推断。

阅读完本书后,您将:

  • 了解现代 Java 中的新功能概述

  • 深入了解 lambda,它们的背景、语法、实现细节以及如何以及何时使用它们

  • 理解函数与类之间的区别,以及这对 lambda 的相关性

  • 理解 lambda 和闭包之间的区别

  • 欣赏推动许多新功能的类型推断的改进

  • 能够使用方法引用并理解作用域和“有效最终”

  • 理解使用 lambda 时产生的字节码的差异

  • 能够推理异常和异常处理最佳实践在使用 lambda 时

您需要为本书做好准备

最新版本的 JDK 和文本编辑器或 IDE。

这本书是为谁准备的

无论您是将传统的 Java 程序迁移到更现代的 Java 风格,还是从头开始构建应用程序,本书都将帮助您开始利用 Java 平台上的函数式编程的力量。

约定

在本书中,您将找到一些区分不同信息类型的文本样式。以下是这些样式的一些示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“这个 LISP 表达式评估为一个函数,当应用时将接受一个参数,将其绑定到arg,然后将1添加到它。”

代码块设置如下:

void anonymousClass() {
    final Server server = new HttpServer();
    waitFor(new Condition() {
        @Override
        public Boolean isSatisfied() {
            return !server.isRunning();
        }
    });
}

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:

void anonymousClass() {
    final Server server = new HttpServer();
    waitFor(new Condition() {
        @Override
        public Boolean isSatisfied() {
            return !server.isRunning();
        }
    });
}

新术语和重要单词以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,会以这样的方式出现在文本中:“为了下载新模块,我们将转到文件 | 设置 | 项目名称 | 项目解释器。”

注意

警告或重要说明会以这样的方式出现在一个框中。

提示

提示和技巧会以这种方式出现。

读者反馈

我们始终欢迎读者的反馈。让我们知道您对本书的看法-您喜欢或不喜欢的地方。读者的反馈对我们很重要,因为它可以帮助我们开发出您真正受益的标题。要向我们发送一般反馈,只需发送电子邮件至 feedback@packtpub.com,并在主题中提及书名。如果您在某个专题上有专业知识,并且有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors。

客户支持

现在您是 Packt 书籍的自豪所有者,我们有很多事情可以帮助您充分利用您的购买。

下载本书的彩色图像

我们还为您提供了一份 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。彩色图像将帮助您更好地理解输出中的变化。您可以从www.packtpub.com/sites/default/files/downloads/LearningJavaLambdas_ColorImages.pdf下载此文件。

勘误

尽管我们已经尽一切努力确保内容的准确性,但错误确实会发生。如果您在我们的书籍中发现错误-也许是文本或代码中的错误-如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以帮助其他读者避免挫折,并帮助我们改进本书的后续版本。如果您发现任何勘误,请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表格链接,并输入您的勘误详情。一旦您的勘误被验证,您的提交将被接受,并且勘误将被上传到我们的网站或添加到该标题的勘误部分下的任何现有勘误列表中。

要查看先前提交的勘误表,请访问www.packtpub.com/books/content/support 并在搜索字段中输入书名。所需信息将出现在勘误表部分下。

盗版

互联网上盗版受版权保护的材料是所有媒体的持续问题。在 Packt,我们非常重视版权和许可的保护。如果您在互联网上发现我们作品的任何非法副本,请立即向我们提供位置地址或网站名称,以便我们采取补救措施。

请通过链接到 suspected pirated material 发送电子邮件至 copyright@packtpub.com 与我们联系。

我们感谢您在保护我们的作者和为您带来有价值内容的能力方面的帮助。

问题

如果您对本书的任何方面有问题,可以通过 questions@packtpub.com 与我们联系,我们将尽力解决问题。

第一章:介绍

欢迎来到学习 Java Lambda。

Java 在其第 8 版发布中经历了巨大的变革。很多变化是由于 Java 变得老态龙钟的观念所驱动的。为了与现代的函数式编程语言竞争,Java 8 引入了像 lambda 这样的函数式编程构造,以更好地支持更加函数式的风格。

本书提供了对 lambda 和其他各种功能的简明解释,以使它们在 Java 中发挥作用。除了提供 lambda 的背景、语法和使用示例外,本书还描述了其他相关功能,如函数接口和类型推断。

通往现代 Java 的道路

Java 8 于 2014 年 3 月 18 日发布,距上一版发布已有两年零七个月。它受到了延迟和技术问题的困扰,但最终发布时,代表了自 Java 5 以来最重大的变革之一。

当然,头条新闻是 lambda 和对支持函数式编程思想的改进。随着Scala等语言占据主导地位,以及现代趋势向函数式编程,Java 必须做些什么来跟上。

尽管 Java 不是也永远不会成为纯函数式编程语言,但 Java 8 的变化使得开发人员可以更容易地使用函数式习语,而不必求助于第三方库。

现代 Java 特性

要让你了解 Java 8 有多么重大的变革,以及为什么它引领了一个新的、现代的 Java,以下是它引入的新特性的大部分完整列表:

  • Lambda 支持。

  • 核心 API 已更新以利用 lambda,包括集合 API 和一个新的函数包,以帮助构建函数构造。

  • 开发了完全新的 API,使用了 lambda,比如流 API,它带来了对数据的函数式处理。例如,流 API 中的map和flatMap等函数使得以声明式的方式处理列表,并摆脱外部迭代转向内部迭代成为可能。这反过来允许库供应商担心细节并根据他们的喜好优化处理。例如,现在 Java 提供了一种并行处理流的方式,而不必让开发人员操心细节。

  • 核心 API 的微小改变;为字符串、集合、比较器、数字和数学引入了新的辅助方法。

  • 一些新增功能正在改变人们编码的方式。例如,Optional类对一些人来说已经很熟悉,它提供了更好的处理空值的方式。

  • 有各种并发库的改进。例如改进的并发哈希映射,可完成的未来,线程安全的累加器,改进的读写锁(称为 StampedLock),一个工作窃取线程池的实现等等。

  • 支持向接口添加静态方法。

  • 默认方法(又称虚拟扩展或防御者方法)。

  • 类型推断得到了改进,引入了新的构造,如函数接口和方法引用,以更好地支持 lambda。

  • 引入了改进的日期和时间 API(类似于流行的Joda-time库)。

  • IO和NIO包得到了欢迎的补充,以使用新的流 API 处理 IO 流。

  • 反射和注解得到了改进。

  • Java 8 配备了全新的 JavaScript 引擎。Nashorn 取代了 Rhino,速度更快,对 ECMA-Script 的支持更好。

  • JVM 的改进;与 JRocket 的集成已完成,创建了一个更快的 JVM。

  • JVM 放弃了永久代的概念,而是使用本机操作系统内存来存储类元数据。这是一个巨大的变革,提供了更好的内存利用率。

  • JRocket 集成还将 Mission control(jmc)作为标准引入了 JDK。它与 JConsole 和 VisualVM 具有类似的功能,但添加了非常廉价的性能分析。

  • 其他杂项改进,如改进 JavaFX,base64 编码支持等等。

第二章:lambda 介绍

在本章中,我们将介绍 lambda 的概念,我们将:

  • 讨论一些关于 lambda 和函数式编程的背景知识

  • 讨论 Java 中函数与类的区别

  • 看一下 Java 中 lambda 的基本语法

在函数式编程中的λs

在我们更深入地看问题之前,让我们先看一些关于 lambda 的一般背景。

如果你以前没有见过,希腊字母λ(lambda)在谈论 lambda 时经常被用作简写。

20 世纪 30 年代和 lambda 演算

在计算机科学中,lambda 演算可以追溯到 lambda 演算。这是 20 世纪 30 年代由Alonzo Church引入的函数的数学表示法。这是一种使用函数探索数学的方法,后来被重新发现为计算机科学中的有用工具。

它正式化了lambda 项的概念和转换这些项的规则。这些规则或函数直接映射到现代计算机科学的思想。lambda 演算中的所有函数都是匿名的,这在计算机科学中再次被字面理解。

这是一个 lambda 演算表达式的例子:

一个 lambda 演算表达式

 λx.x+1

这定义了一个带有单个参数x的匿名函数或lambda。主体跟在点后,将一个加到该参数。

20 世纪 50 年代和 LISP

在 20 世纪 50 年代,John McCarthy在麻省理工学院发明了 LISP。这是一种旨在模拟数学问题的编程语言,受到了 lambda 演算的重大影响。

它使用 lambda 这个词作为运算符来定义一个匿名函数。

这是一个例子:

一个 LISP 表达式

(lambda (arg) (+ arg 1))

这个 LISP 表达式求值为一个函数,当应用时将接受一个参数,将其绑定到arg,然后加1。

这两个表达式产生相同的结果,一个增加数字的函数。你可以看到这两个非常相似。

lambda 演算和 LISP 对函数式编程产生了巨大影响。应用函数和使用函数推理问题的思想直接转移到了编程语言中。因此,在我们的领域中使用了这个术语。演算中的 lambda 与现代编程语言中的 lambda 是相同的,并且以相同的方式使用。

什么是 lambda

简而言之,lambda 只是一个匿名函数。就是这样。没有什么特别的。这只是一种定义函数的简洁方式。当您想要传递可重用功能片段时,匿名函数是有用的。例如,将函数传递到其他函数中。

许多主流语言已经支持 lambda,包括 Scala、C#、Objective-C、Ruby、C++(11)、Python 等。

函数与类

请记住,Java 中的匿名函数与 Java 中的匿名类不同。Java 中的匿名类仍然需要实例化为对象。它可能没有一个适当的名称,但只有当它是一个对象时才有用。

另一方面,函数没有与之关联的实例。函数与它们所作用的数据是分离的,而对象与它所作用的数据是密切相关的。

您可以在现代 Java 中使用 lambda 的任何地方,以前您可能使用单方法接口,因此它可能看起来只是语法糖,但实际上并非如此。让我们看看它们的区别,并将匿名类与 lambda 进行比较;类与函数。

现代 Java 中的 lambda

在 Java 8 之前,匿名类(单方法接口)的典型实现可能看起来像这样。anonymousClass方法调用waitFor方法,传入Condition的一些实现;在这种情况下,它在说,等待某个服务器关闭:

匿名类的典型用法

void anonymousClass() {
    final Server server = new HttpServer();
    waitFor(new Condition() {
        @Override
        public Boolean isSatisfied() {
            return !server.isRunning();
        }
    });
}

等效的 lambda 看起来是这样的:

等效的 lambda 功能

 void closure() { 
     Server server = new HttpServer();
     waitFor(() -> !server.isRunning()); 
 }

在完整性的利益上,一个天真的轮询waitFor方法可能看起来像这样:

class WaitFor {
    static void waitFor(Condition condition) throws   
    InterruptedException {
        while (!condition.isSatisfied())
            Thread.sleep(250);
    }
}

一些理论上的差异

首先,这两种实现实际上都是闭包,后者也是 lambda。我们将在Lambda 与闭包部分更详细地讨论这一区别。这意味着它们都必须在运行时捕获它们的“环境”。在 Java 8 之前,这意味着将闭包需要的东西复制到一个类的实例中(Condition 的匿名实例)。在我们的例子中,server 变量需要被复制到实例中。

因为它是一个副本,必须声明为 final,以确保它在被捕获和使用时不能被更改。这两个时间点可能非常不同,因为闭包通常用于推迟执行直到以后的某个时间点(例如,参见惰性求值)。现代 Java 使用了一个巧妙的技巧,即如果它可以推断出一个变量永远不会被更新,那么它可能就是 final,因此它被视为实际上是 final,你不需要显式地声明它为 final。

另一方面,lambda 不需要复制它的环境或捕获任何术语。这意味着它可以被视为真正的函数,而不是一个类的实例。有什么区别?很多。

函数与类

首先,函数;真正的函数,不需要多次实例化。当谈论分配内存和加载一块机器代码作为函数时,我甚至不确定实例化是否是正确的词。关键是,一旦它可用,它可以被重复使用,它的本质是幂等的,因为它不保留状态。静态类方法是 Java 中最接近函数的东西。

对于 Java 来说,这意味着 lambda 在每次评估时不需要被实例化,这是一个很大的问题。与实例化匿名类不同,内存影响应该是最小的。

在一些概念上的区别方面:

  • 类必须被实例化,而函数不需要。

  • 当类被实例化时,内存被分配给对象。

  • 函数只需要分配一次内存。它们存储在堆的永久区域。

  • 对象作用于它们自己的数据,函数作用于不相关的数据。

  • 在 Java 中,静态类方法大致相当于函数。

一些具体的区别

函数和类之间的一些具体区别包括它们的捕获语义以及它们如何遮蔽变量。

捕获语义

另一个区别在于对 this 的捕获语义。在匿名类中,this 指的是匿名类的实例。例如,Foo$InnerClass而不是Foo。这就是为什么当你从匿名类引用封闭范围时,会出现略显奇怪的语法,比如Foo.this.x。

另一方面,在 lambda 中,this 指的是封闭范围(在我们的例子中直接指的是 Foo)。事实上,lambda 是完全词法作用域,这意味着它们不继承任何名称来自超类型,也不引入新的作用域级别;你可以直接访问封闭范围的字段、方法和局部变量。

例如,这个类表明 lambda 可以直接引用firstName变量。

public class Example {
    private String firstName = "Jack";

    public void example() {
        Function<String, String> addSurname = surname -> {
            // equivalent to this.firstName
            return firstName + " " + surname;  // or even,   
            this.firstName
        };
    }
}

在这里,firstName是this.firstName的简写,因为 this 指的是封闭范围(类Example),它的值将是“Jack”。

匿名类等效需要明确地引用来自封闭范围的firstName。在这种情况下,你不能使用 this,因为这意味着匿名实例,那里没有firstName。因此,以下内容将编译:

public class Example {
    private String firstName = "Charlie";

    public void anotherExample() {
        Function<String, String> addSurname = new Function<String,  
        String>() {
            @Override
            public String apply(String surname) {
                return Example.this.firstName + " " + surname;   
                // OK
            }
        };
    }
}

但这不会。

public class Example {
    private String firstName = "Charlie";

  public void anotherExample() {
    Function<String, String> addSurname = new Function<String,   
    String>() {
      @Override
      public String apply(String surname) {
        return this.firstName + " " + surname;   // compiler error
      }
    };
  }
}

你仍然可以直接访问字段(即简单地调用return firstName + " " + surname),但不能使用 this。这里的重点是演示在 lambda 和匿名实例中使用 this 时捕获语义的差异。

遮蔽变量

引用遮蔽变量变得更加直接,可以通过简化的this语义来推理。例如,

public class ShadowingExample {

    private String firstName = "Charlie";

    public void shadowingExample(String firstName) {
        Function<String, String> addSurname = surname -> {
            return this.firstName + " " + surname;
        };
    }
}

在这里,因为this在 lambda 内部,它指的是外部范围。所以this.firstName将有值"Charlie",而不是同名方法参数的值。捕获语义使其更清晰。如果你使用firstName(并且去掉this),它将指的是参数。

在下一个示例中,使用匿名实例,firstName只是指参数。如果你想引用外部版本,你会使用Example.this.firstName:

public class ShadowingExample {

    private String firstName = "Charlie";

    public void anotherShadowingExample(String firstName) {
        Function<String, String> addSurname = new Function<String,  
        String>() {
            @Override
            public String apply(String surname) {
                return firstName + " " + surname;
            }
        };
    }
}

总结

学术意义上的函数与匿名类(我们在 Java 8 之前经常将其视为函数)是非常不同的东西。了解这些区别是有用的,可以为了除了简洁的语法之外的其他原因来使用 lambda。当然,使用 lambda 还有很多其他优势(至少是 JDK 的大量使用)。

当我们看下新的 lambda 语法时,请记住,尽管 lambda 在 Java 中的使用方式与匿名类非常相似,但它们在技术上是不同的。Java 中的 lambda 不需要在每次评估时实例化,不像匿名类的实例。

这应该提醒你,Java 中的 lambda 不仅仅是语法糖。

λ基本语法

让我们来看一下基本的 lambda 语法。

lambda 基本上是一个匿名的功能块。它很像使用匿名类实例。例如,如果我们想在 Java 中对数组进行排序,我们可以使用Arrays.sort方法,它接受Comparator接口的实例。

它会看起来像这样:

Arrays.sort(numbers, new Comparator<Integer>() {
    @Override
    public int compare(Integer first, Integer second) {
        return first.compareTo(second);
    }
});

这里的Comparator实例是功能的一个抽象部分;它本身没有意义;只有当它被sort方法使用时才有目的。

使用 Java 的新语法,你可以用 lambda 替换它,看起来像这样:

Arrays.sort(numbers, (first, second) -> first.compareTo(second));

这是一种更简洁的实现相同功能的方式。事实上,Java 将其视为Comparator类的实例。如果我们要提取 lambda 的变量(第二个参数),它的类型将是Comparator<Integer>,就像上面的匿名实例一样。

Comparator<Integer> ascending = (first, second) -> first.compareTo(second);
Arrays.sort(numbers, ascending);

因为Comparator上只有一个抽象方法;compareTo,编译器可以推断出当我们有一个这样的匿名块时,我们实际上是指Comparator的一个实例。它可以做到这一点,多亏了我们稍后将讨论的一些其他新特性;函数接口和类型推断的改进。

语法分解

你总是可以从使用单个抽象方法转换为使用 lambda。

假设我们有一个接口Example,有一个方法apply,返回某种类型并接受某个参数:

interface Example {
     R apply(A arg);
}

我们可以用这样的方式实例化一个实例:

new Example() {
    @Override
    public R apply(A args) {
        body
    }
};

要转换为 lambda,我们基本上是去掉多余的部分。我们去掉实例化和注释,去掉方法细节,只留下参数列表和主体。

(args) {
    body
}

然后我们引入新的箭头符号来表示整个内容是 lambda,并且接下来的部分是 lambda 的主体,这就是我们的基本 lambda 语法:

(args) -> {
    body
}

让我们通过以下步骤来看之前的排序示例。我们从匿名实例开始:

Arrays.sort(numbers, new Comparator<Integer>() {
    @Override
    public int compare(Integer first, Integer second) {
        return first.compareTo(second);
    }
});

并去掉实例化和方法签名:

Arrays.sort(numbers, (Integer first, Integer second) {
    return first.compareTo(second);
});

引入 lambda

Arrays.sort(numbers, (Integer first, Integer second) -> {
    return first.compareTo(second);
});

然后我们完成了。不过我们还可以做一些优化。如果编译器足够聪明以推断类型,你可以省略类型。

Arrays.sort(numbers, (first, second) -> {
    return first.compareTo(second);
});

对于简单的表达式,你可以去掉大括号来产生一个 lambda 表达式:

Arrays.sort(numbers, (first, second) -> first.compareTo(second));

在这种情况下,编译器可以推断出足够的信息来知道你的意思。单个语句返回一个与接口一致的值,所以它说,“不需要告诉我你要返回什么,我自己就能看到”。

对于单参数接口方法,甚至可以去掉第一个括号。例如,接受参数x并返回x + 1的 lambda;

(x) -> x + 1

可以不带括号写

x -> x + 1

总结

让我们用一个语法选项的总结来回顾一下。

语法总结:

(int x, int y) -> { return x + y; }
(x, y) -> { return x + y; }
(x, y) -> x + y; x -> x * 2
() -> System.out.println("Hey there!");
System.out::println;

第一个例子 ((int x, int y) -> { return x + y; }) 是创建 lambda 的最冗长的方式。函数的参数及其类型在括号内,然后是新的箭头语法,然后是代码块;要执行的代码块。

您通常可以从参数列表中省略类型,比如 (x, y) -> { return x + y; }。编译器会在这里使用类型推断来尝试猜测类型。它会根据您尝试使用 lambda 的上下文来进行推断。

如果您的代码块返回某些内容或是单行表达式,您可以省略大括号和返回语句,例如 (x, y) -> x + y;。

在只有一个参数的情况下,您可以省略括号 x -> x * 2。

如果根本没有参数,则需要使用“汉堡包”符号。

() -> System.out.println("Hey there!");。

为了完整起见,还有另一种变体;一种称为方法引用的 lambda 的快捷方式。例如,System.out::println;,基本上是对 lambda (value -> System.out.prinltn(value) 的简写。

我们将在稍后更详细地讨论方法引用,所以现在只需知道它们存在,并且可以在任何可以使用 lambda 的地方使用。

第三章:深入了解 Lambda

在本节中,我们将更详细地看一些相关主题,比如:

  • 函数接口

  • 方法和构造函数引用

  • 范围和有效的最终变量

  • 异常透明度

  • Lambda 和闭包之间的区别

  • 正如我们谈到的,lambda 不仅仅是语法糖,我们将看一下 lambda 生成的字节码

函数接口

Java 将 lambda 视为接口类型的实例。它将这个形式化为它所谓的函数接口。函数接口只是一个具有单一方法的接口。Java 将这个方法称为"函数方法",但通常使用"单一抽象方法"或 SAM。

JDK 中所有现有的单一方法接口,如Runnable和Callable,现在都是函数接口,lambda 可以在任何使用单一抽象方法接口的地方使用。事实上,正是函数接口允许了所谓的目标类型;它们提供了足够的信息,以便编译器推断参数和返回类型。

@FunctionalInterface

Oracle 引入了一个新的注解@FunctionalInterface来标记一个接口。这基本上是为了传达意图,但也允许编译器进行一些额外的检查。

例如,这个接口可以编译通过:

public interface FunctionalInterfaceExample {
    // compiles ok
}

但当你通过添加新的注解来指示它应该是一个函数接口时,

@FunctionalInterface // <- error here
    public interface FunctionalInterfaceExample {
      // doesn't compile
}

编译器会报错。它告诉我们"Example 不是一个函数接口",因为"没有找到抽象方法"。通常 IDE 也会提示,IntelliJ会说类似"没有找到目标方法"。它在暗示我们忘了函数方法。"单一抽象方法"需要一个abstract方法。

那么如果我们尝试向接口添加第二个方法会怎样呢?

@FunctionalInterface
public interface FunctionalInterfaceExample {
    void apply();
    void illegal(); // <- error here
}

编译器会再次报错,这次的错误信息是"找到多个非覆盖的抽象方法"。函数接口只能有一个方法。

扩展

那么接口扩展另一个接口的情况呢?

让我们创建一个名为A的新函数接口,另一个名为B的函数接口,它扩展了A。B接口仍然是一个函数接口。它继承了父类的apply方法,正如你所期望的那样:

@FunctionalInterface
interface A {
    abstract void apply();
}

interface B extends A {
}

如果你想要更清晰地表达这一点,你也可以覆盖父类的函数方法:

@FunctionalInterface
interface A {
    abstract void apply();
}

interface B extends A {
    @Override
    abstract void apply();
}

如果我们将它作为 lambda 使用,我们可以验证它作为函数接口是否有效。我们将实现一个方法来展示 lambda 可以分配给类型 A 和类型 B。实现只是打印出"A"或"B"。

@FunctionalInterface
public interface A {
    void apply();
}

public interface B extends A {
    @Override
    void apply();
}

public static void main(String... args) {
   A a = () -> System.out.println("A");
   B b = () -> System.out.println("B");
}

你不能向扩展接口(B)添加新的abstract方法,因为结果类型会有两个abstract方法,IDE 会警告我们,编译器会报错。

@FunctionalInterface
public interface A {
    void apply();
}

public interface B extends A {
    void illegal();     // <- can't do this
}

public static void main(String... args) {
    A a = () -> System.out.println("A");
    B b = () -> System.out.println("B");    // <- error
}

在这两种情况下,你可以覆盖Object中的方法而不会引起问题。你也可以添加默认方法(从 Java 8 开始的新功能)。正如你可能期望的那样,将抽象类标记为函数接口是没有意义的。

其他接口改进

接口通常有一些新的特性,它们包括:

  • 默认方法(虚拟扩展方法)

  • 静态接口方法

  • 以及java.util.function包中一堆新的函数接口;像Function和Predicate这样的东西

摘要

在本节中,我们谈到了任何具有单一方法的接口现在都是"函数接口",而且这个单一方法通常被称为"函数方法"或 SAM(单一抽象方法)。

我们看了一下新的注解,并看了一些现有 JDK 接口如Runnable和Callable是如何使用这个注解进行改造的。

我们还介绍了目标类型的概念,即编译器如何使用函数方法的签名来帮助确定可以在哪里使用 lambda 表达式。我们稍微提了一下这个概念,因为我们将在类型推断部分稍后讨论它。

我们讨论了一些函数接口的示例,编译器和 IDE 在我们犯错时如何帮助我们,并对我们可能遇到的错误有了一些了解。例如,向函数接口添加多个方法。我们还看到了这个规则的例外情况,即当我们重写Object的方法或实现默认方法时。

我们快速查看了接口继承以及它对事物的影响,并提到了我们稍后将要涵盖的其他接口改进。

这里要记住的一个重要观点是,任何地方都可以使用函数接口,现在都可以使用 lambda。Lambda 可以用来代替函数接口的匿名实现。使用 lambda 而不是匿名类可能看起来像语法糖,但它们实际上是有很大不同的。有关更多详细信息,请参阅函数 vs. 类部分。

类型推断改进

现代 Java 中有几个类型推断改进。为了支持 lambda,编译器推断的方式已经得到改进,广泛使用目标类型。这些改进和 Java 7 的推断改进都是在开放JDK 增强提案(JEP)101 下进行管理的。

在我们深入讨论这些之前,让我们回顾一下基础知识。

类型推断是指编程语言自动推断表达式类型的能力。

静态类型语言在编译时知道事物的类型。动态类型语言在运行时知道类型。静态类型语言可以使用类型推断,在源代码中删除类型信息,并使用编译器来找出缺失的部分。

类型推断改进

这意味着静态类型语言(如 Scala)可以使用类型推断来“看起来”像动态语言(如 JavaScript)。至少在源代码级别上是这样。

以下是 Scala 中的一行代码示例:

val name = "Henry"

你不需要明确告诉编译器该值是一个字符串。它会自己推断出来。你可以像这样明确地写出来,

val name : String = "Henry"

但没有必要。

顺便说一句,Scala 还尝试根据其抽象语法树(AST)来确定何时完成语句或表达式。因此,通常情况下,你甚至不需要添加终止分号。

Java 类型推断

类型推断是一个相当广泛的主题,Java 不支持我刚刚谈到的类型推断类型。至少对于像删除变量的类型注释这样的事情。我们必须记住:

String name = "Henry"; // <- we can't drop the String like Scala

因此,Java 在更广义上不支持类型推断。它不能像某些语言那样猜测一切。因此,Java 中的类型推断通常指的是编译器如何为泛型推断类型。Java 7 在引入了菱形操作符(<>)时改进了这一点,但在 Java 可以推断出的内容上仍然有很多限制。

Java 编译器是基于类型擦除构建的;它在编译过程中主动删除类型信息。由于类型擦除,List<String>在编译后变成了List<Object>。

出于历史原因,在 Java 5 中引入泛型时,开发人员无法轻易地逆转使用擦除的决定。Java 需要理解要替换给定泛型类型的类型,但没有信息如何做到这一点,因为所有信息都已被擦除。类型推断是解决方案。

所有泛型值在后台实际上都是Object类型,但通过使用类型推断,编译器可以检查所有源代码用法是否与它认为的泛型一致。在运行时,一切都将作为Object的实例传递,并在后台进行适当的转换。类型推断只是允许编译器提前检查转换是否有效。

因此,类型推断是关于猜测类型的,Java 对类型推断的支持在 Java 8 中将以几种方式得到改进:

  1. lambda 的目标类型推断。

并使用泛化的目标类型推断:

  1. 在方法调用中添加对参数类型推断的支持。

  2. 在链式调用中添加对参数类型推断的支持。

让我们看看当前的问题以及现代 Java 如何解决它们。

用于 lambda 的目标类型推断

现代 Java 对类型推断的一般改进意味着 lambda 可以推断它们的类型参数;所以不再需要使用:

(Integer x, Integer y) -> x + y;

你可以去掉Integer类型注释,使用以下代替:

(x, y) -> x + y;

这是因为函数接口描述了类型,它给编译器提供了所有需要的信息。

例如,如果我们拿一个例子函数接口。

@FunctionalInterface
interface Calculation {
    Integer apply(Integer x, Integer y);
}

当 lambda 被用来代替接口时,编译器首先要做的是确定 lambda 的目标类型。所以如果我们创建一个接受接口和两个整数的方法calculate。

static Integer calculate(Calculation operation, Integer x, Integer y) { 
    return operation.apply(x, y);
}

然后创建两个 lambda;一个加法和减法函数

Calculation addition = (x, y) -> x + y;
Calculation subtraction = (x, y) -> x - y;

并且像这样使用它们:

calculate(addition, 2, 2);
calculate(subtraction, 5, calculate(addition, 3, 2));

编译器理解 lambda 的加法和减法具有 Calculation 的目标类型(它是唯一符合 calculate 方法签名的“形状”)。然后它可以使用方法签名来推断 lambda 参数的类型。接口上只有一个方法,所以没有歧义,参数类型显然是Integer。

我们将看很多目标类型推断的例子,所以现在,只需知道 Java 用来实现很多 lambda 功能的机制依赖于类型推断的改进和目标类型的概念。

方法调用中的类型参数

在 Java 8 之前,有一些情况下编译器无法推断类型。其中之一是在调用带有泛型类型参数的方法时。

例如,Collections类有一个用于生成空列表的泛型方法。它看起来像这样:

public static final <T> List<T> emptyList() { ... }

在 Java 7 中,这个编译:

List<String> names = Collections.emptyList(); // compiles in Java 7

因为 Java 7 编译器可以推断出emptyList方法所需的泛型类型是String类型。然而,它在泛型方法的结果作为参数传递给另一个方法调用时会有困难。

所以如果我们有一个处理列表的方法看起来像这样:

static void processNames(List<String> names) {
    System.out.println("hello " + name);
}

然后用空列表方法调用它。

processNames(Collections.emptyList()); // doesn't compile in Java 7

它不会编译,因为参数的泛型类型已被擦除为Object。它实际上看起来像这样。

processNames(Collections.<Object>emptyList names);

这与processList方法不匹配。

processNames(Collections.<String>emptyList names);

所以在我们使用显式的“类型见证”给它一个额外的提示之前,它不会编译。

processNames(Collections.<String>emptyList());   // compiles in Java 7

现在编译器已经足够了解传递到方法中的泛型类型。

Java 8 中的改进包括更好地支持这一点,因此一般来说,你不再需要类型见证。

我们现在调用processNames的例子编译通过了!

processNames(Collections.emptyList());           // compiles in Java 8

链式方法调用中的类型参数

类型推断的另一个常见问题是方法链。假设我们有一个List类:

static class List<E> {

    static <T> List<T> emptyList() {
        return new List<T>();
    }

    List<E> add(E e) {
        // add element
        return this;
    }
}

并且我们想要链式调用添加一个元素到创建空列表的方法。类型擦除再次出现;类型被擦除,所以下一个方法无法知道它。它不会编译。

List<String> list = List.emptyList().add(":(");

这本来应该在 Java 8 中修复,但不幸的是它被放弃了。所以,至少目前,你仍然需要明确地向编译器提供一个类型;你仍然需要一个类型见证。

List<String> list = List.<String>emptyList().add(":(");

方法引用

我之前提到方法引用有点像是 lambda 的快捷方式。它们是指向方法的一种简洁方便的方式,并允许该方法在任何 lambda 可以使用的地方使用。

当你创建一个 lambda 时,你创建了一个匿名函数并提供方法体。当你将方法引用作为 lambda 使用时,它实际上指向一个已存在的命名方法;它已经有了一个方法体。

你可以把它们想象成将一个常规方法转换为一个函数接口。

基本语法看起来像这样:

Class::method

或者,一个更具体的例子:

String::valueOf

双冒号之前的部分是目标引用,之后是方法名。所以,在这种情况下,我们的目标是String类,并且正在寻找一个叫做valueOf的方法;我们正在引用String上的static方法。

public static String valueOf(Object obj) { ... }

双冒号被称为分隔符。当我们使用它时,我们不是在调用方法,只是引用它。所以记住不要在末尾加括号。

String::valueOf(); // <-- error

你不能直接调用方法引用,它们只能用作 lambda 的替代。所以任何地方可以使用 lambda,你都可以使用方法引用。

例子

这个语句本身不会编译。

public static void main(String... args) {
    String::valueOf;
}

这是因为方法引用不能转换为 lambda,因为编译器无法推断出要创建什么类型的 lambda。

我们碰巧知道这个引用等同于

(x) -> String.valueOf(x)

但是编译器还不知道。它可以知道一些事情。它知道,作为 lambda,返回值应该是String类型,因为String中所有名为valueOf的方法都返回一个字符串。但它不知道要提供什么样的参数。我们需要给它一点帮助,给它一些更多的上下文。

我们将创建一个名为Conversion的函数接口,它接受一个整数并返回一个字符串。这将是我们 lambda 的目标类型。

@FunctionalInterface
interface Conversion {
    String convert(Integer number);
}

接下来,我们需要创建一个场景,在这个场景中我们将使用它作为 lambda。所以我们创建一个小方法,接受一个函数接口并将一个整数应用到它上面。

public static String convert(Integer number, Conversion function) {
    return function.convert(number);
}

现在,问题来了。我们刚刚给编译器足够的信息,以将方法引用转换为等效的 lambda。

当我们调用convert方法时,我们可以传入一个 lambda。

convert(100, (number) -> String.valueOf(number));

我们可以用valueOf方法的引用替换 lambda。编译器现在知道我们需要一个返回字符串并接受一个整数的 lambda。它现在知道valueOf方法“适合”,并且可以替换整数参数。

convert(100, String::valueOf);

给编译器提供所需的信息的另一种方法就是将引用分配给一个类型。

Conversion b = (number) -> String.valueOf(number);

以及作为方法引用

Conversion a = String::valueOf;

“形状”匹配,所以可以分配。

有趣的是,我们可以将相同的 lambda 分配给任何需要相同“形状”的接口。例如,如果我们有另一个具有相同“形状”的函数接口,

在这里,Example返回一个String并接受一个Object,所以它的签名形状与valueOf相同。

interface Example {
    String theNameIsUnimportant(Object object);
}

我们仍然可以将方法引用(或 lambda)分配给它。

Example a = String::valueOf;

方法引用类型

有四种方法引用类型:

  • 构造函数引用

  • 静态方法引用

  • 和两种实例方法引用

最后两个有点混淆。第一个是特定对象的方法引用,第二个是任意对象的方法引用,但是是特定类型的。区别在于你想如何使用这个方法,以及是否提前有实例。

首先,让我们看看构造函数引用。

构造函数引用

基本语法看起来像这样:

String::new

目标类型后跟双冒号,然后是new关键字。它将创建一个 lambda,将调用String类的零参数构造函数。

它等同于这个 lambda

() -> new String()

记住,方法引用永远不带括号;它们不是在调用方法,只是引用一个方法。这个例子是指String类的构造函数,但不实例化一个字符串。

让我们看看我们可能如何实际使用构造函数引用。

如果我们创建一个对象列表,我们可能想要填充该列表,比如十个项目。因此,我们可以创建一个循环,并添加一个新对象十次。

public void usage() {
    List<Object> list = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        list.add(new Object());
  }
}

但是,如果我们想要能够重用该初始化函数,我们可以将代码提取到一个名为initialise的新方法中,然后使用工厂来创建对象。

public void usage() {
    List<Object> list = new ArrayList<>();
    initialise(list, ...);
}

private void initialise(List<Object> list, Factory<Object> factory){
    for (int i = 0; i < 10; i++) {
        list.add(factory.create());
    }
 }

Factory类只是一个带有名为create的方法的功能接口,该方法返回一些对象。然后我们可以将它创建的对象添加到列表中。因为它是一个功能接口,我们可以使用 lambda 来实现初始化列表的工厂:

public void usage() {
    List<Object> list = new ArrayList<>();
    initialise(list, () -> new Object());
}

或者我们可以换成构造函数引用。

public void usage() {
    List<Object> list = new ArrayList<>();
    initialise(list, Object::new);
}

这里还有一些其他事情我们可以做。如果我们给initialise方法添加一些泛型,我们可以在初始化任何类型的列表时重用它。例如,我们可以回过头将列表的类型更改为String,并使用构造函数引用进行初始化。

public void usage() {
    List<String> list = new ArrayList<>();
    initialise(list, String::new);
}

private <T> void initialise(List<T> list, Factory<T> factory) {
    for (int i = 0; i < 10; i++) {
        list.add(factory.create());
    }
}

我们已经看到它如何适用于零参数构造函数,但是当类有多个参数构造函数时呢?

当存在多个构造函数时,您可以使用相同的语法,但编译器会找出哪个构造函数最匹配。它是基于目标类型和推断出的功能接口来完成这一点,它可以用来创建该类型。

让我们以Person类为例,它看起来像这样,您可以看到构造函数接受一堆参数。

class Person {
    public Person(String forename, String surname, LocalDate    
    birthday, Sex gender, String emailAddress, int age) {
      // ...
    }
}

回到我们之前的例子,看看通用的initialise方法,我们可以使用这样的 lambda:

initialise(people, () -> new Person(forename, surname, birthday,
                                    gender, email, age));

但是,要能够使用构造函数引用,我们需要一个带有可变参数的 lambda,看起来像这样:

(a, b, c, d, e, f) -> new Person(a, b, c, d, e, f);

但这并不直接转换为构造函数引用。如果我们尝试使用

Person::new

它不会编译,因为它不知道任何关于参数的信息。如果您尝试编译它,错误会说您创建了一个无效的构造函数引用,不能应用于给定的类型;它找不到参数。

相反,我们必须引入一些间接性,以便为编译器提供足够的信息来找到合适的构造函数。我们可以创建一些可以用作功能接口并且具有正确类型以适应适当构造函数的东西。

让我们创建一个名为PersonFactory的新功能接口。

@FunctionalInterface
interface PersonFactory {
    Person create(String forename, String surname, LocalDate 
    birthday, Sex gender, String emailAddress, int age);
}

在这里,来自PersonFactory的参数与Person上可用的构造函数匹配。神奇的是,这意味着我们可以回过头使用Person的构造函数引用。

public void example() {
    List<Person> list = new ArrayList<>();
    PersonFactory factory = Person::new;
    // ...
}

请注意,我正在使用Person的构造函数引用。这里需要注意的是,构造函数引用可以分配给目标功能接口,即使我们还不知道参数。

方法引用的类型是PersonFactory而不是Person可能看起来有点奇怪。这种额外的目标类型信息帮助编译器知道它必须通过PersonFactory来创建Person。有了这个额外的提示,编译器就能够创建一个基于工厂接口的 lambda,稍后创建一个Person。

写出来的话,编译器会生成这个。

public void example() {
    PersonFactory factory = (a, b, c, d, e, f) -> new Person(a, b,   
    c, d, e, f);
}

之后可以像这样使用:

public void example() {
    PersonFactory factory = (a, b, c, d, e, f) -> new Person(a, b,  
    c, d, e, f);
    Person person = factory.create(forename, surname, birthday,
                                gender, email, age);
}

幸运的是,一旦我们引入了间接性,编译器就可以为我们做到这一点。

它了解要使用的目标类型是PersonFactory,并且了解它的单个abstract方法可以用来代替构造函数。这有点像一个两步过程,首先,找出abstract方法与构造函数具有相同的参数列表,并且返回正确的类型,然后使用双冒号新语法应用它。

为了完成示例,我们需要调整我们的initialise方法以添加类型信息(替换泛型),添加参数来表示人的详细信息,并实际调用工厂。

private void initialise(List<Person> list, PersonFactory factory, 
                       String forename, String surname,                           
                       LocalDate birthday, Sex gender,
                       String emailAddress, int age) {
                         for (int i = 0; i < 10; i++) {
                           list.add(factory.create(forename,  
                           surname, birthday, gender,        
                           emailAddress, age));
                         }
                       }

然后我们可以像这样使用它:

public void example() {
    List<Person> list = new ArrayList<>();
    PersonFactory factory = Person::new;
    initialise(people, factory, a, b, c, d, e, f);
}

或者内联,像这样:

public void example() {
    List<Person> list = new ArrayList<>();
    initialise(people, Person::new, a, b, c, d, e, f);
}

静态方法引用

方法引用可以直接指向静态方法。例如,

String::valueOf

这一次,左侧指的是可以找到静态方法(在这种情况下是valueOf)的类型。它相当于这个 lambda

x -> String.valueOf(x))

更多的例子是我们使用对Comparators类的静态方法的引用对集合进行排序。

Collections.sort(Arrays.asList(5, 12, 4), Comparators::ascending);

// equivalent to
Collections.sort(Arrays.asList(5, 12, 4), (a, b) -> Comparators.ascending(a, b));

在这里,静态方法ascending可能是这样定义的:

public static class Comparators {
    public static Integer ascending(Integer first, Integer second)   
    {
        return first.compareTo(second);
     }
}

特定对象的实例方法引用(在这种情况下是闭包)

这是特定实例的实例方法引用的一个示例:

x::toString

x是我们想要获取的特定实例。它的 lambda 等效如下:

() -> x.toString()

引用特定实例的方法的能力还为我们提供了一种方便的方法,可以在不同的功能接口类型之间进行转换。例如:

Callable<String> c = () -> "Hello";

Callable的功能方法是调用。当调用时,lambda 将返回"Hello"。

如果我们有另一个功能接口Factory,我们可以使用方法引用将Callable转换。

Factory<String> f = c::call;

我们可以重新创建 lambda,但这个技巧是一种有用的方法,可以重复使用预定义的 lambda。将它们分配给变量并重复使用,以避免重复。

以下是使用它的示例:

public void example() {
    String x = "hello";
    function(x::toString);
}

这是一个使用闭包的方法引用的示例。它创建一个 lambda,将在实例x上调用toString方法。

上面的函数的签名和实现如下:

public static String function(Supplier<String> supplier) {
    return supplier.get();
}

Supplier接口是一个看起来像这样的功能接口:

@FunctionalInterface
public interface Supplier<T> {
  T get();
}

当在我们的函数中使用时,它提供一个字符串值(通过调用get),它唯一能做到这一点的方式是如果值在构造时已经被提供。它相当于:

public void example() {
  String x = "";
  function(() -> x.toString());
}

请注意,这里的 lambda 没有参数(它使用“汉堡包”符号)。这表明x的值在 lambda 的局部范围内不可用,因此只能从其范围外部获得。它是一个闭包,因为必须关闭x(它将捕获x)。

如果您有兴趣看到长手,匿名类等效,它将如下所示。再次注意x必须传入:

public void example() {
    String x = "";
    function(new Supplier<String>() {
        @Override
        public String get() {
            return x.toString(); // <- closes over 'x'
        }
    });
}

这三种都是等效的。将其与实例方法引用的 lambda 变体进行比较,其中它的参数不是显式从外部范围传入的。

稍后提供实例的任意对象的实例方法引用(lambda)

最后一种情况是指向由其类型引用的任意对象的方法引用:

Object::toString

因此,在这种情况下,尽管看起来左侧指向一个类(就像static方法引用一样),但实际上指向一个实例。toString方法是Object上的实例方法,而不是static方法。之所以可能不使用常规实例方法语法,是因为您可能尚未有实例可供参考。

因此,在以前,当我们调用x冒号冒号toString时,我们知道x的值。有些情况下,您没有x的值,在这些情况下,您仍然可以传递方法的引用,但稍后使用此语法提供值。

例如,lambda 等效没有x的绑定值。

(x) -> x.toString()

两种类型的实例方法引用之间的区别基本上是学术性的。有时,您需要传递一些内容,其他时候,lambda 的使用将为您提供它。

该示例类似于常规方法引用;它调用字符串的toString方法,只是这一次,字符串是由使用 lambda 的函数提供的,而不是从外部范围传入的。

public void lambdaExample() {
    function("value", String::toString);
}

String部分看起来像是在引用一个类,但实际上是在引用一个实例。我知道这很令人困惑,但为了更清楚地看到事情,我们需要看到使用 lambda 的函数。它看起来像这样。

public static String function(String value, Function<String, String> function) {
    return function.apply(value);
}

因此,字符串值直接传递给函数,它看起来像这样作为完全限定的 lambda。

public void lambdaExample() {
    function("value", x -> x.toString());
}

Java 可以快捷方式查看String::toString;它表示在运行时“提供对象实例”。

如果将其完全扩展为匿名接口,它看起来像这样。x参数可用且未关闭。因此它是一个 lambda 而不是闭包。

public void lambdaExample() {
    function("value", new Function<String, String>() {
      @Override
      // takes the argument as a parameter, doesn't need to close 
      over it
      public String apply(String x) {
        return x.toString();
      }
    });
}

总结

Oracle 描述了四种方法引用(docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html)如下:

种类示例
对静态方法的引用ContainingClass::staticMethodName
对特定对象的实例方法的引用ContainingObject::instanceMethodName
对特定类型的任意对象的实例方法的引用ContainingType::methodName
对构造函数的引用ClassName::new

但是实例方法的描述只是纯粹令人困惑。到底是什么意思呢?特定类型的任意对象的实例方法?难道不是所有对象都是特定类型的吗?为什么重要的是对象是任意的?

我更喜欢将第一个视为特定对象的实例方法,而将第二个视为稍后将提供的任意对象的实例方法。有趣的是,这意味着第一个是闭包,第二个是lambda。一个是绑定的,另一个是未绑定的。方法引用的区别是闭包(闭包)和不闭包(lambda)可能有点学术,但至少这是一个比 Oracle 无用的描述更正式的定义。

种类语法示例
对静态方法的引用Class::staticMethodNameString::valueOf
对特定对象的实例方法的引用object::instanceMethodNamex::toString
参考任意对象后提供的实例方法的引用Class::instanceMethodNameString::toString
对构造函数的引用ClassName::newString::new

或等效的 lambda:

种类语法作为 Lambda
对静态方法的引用Class::staticMethodName(s) -> String.valueOf(s)
对特定对象的实例方法的引用object::instanceMethodName() -> "hello".toString()
对任意对象后提供的实例方法的引用Class::instanceMethodName(s) -> s.toString()
对构造函数的引用ClassName::new() -> new String()

请注意,static方法引用的语法看起来非常类似于对类的实例方法的引用。编译器通过遍历每个适用的静态方法和每个适用的实例方法来确定使用哪一个。如果它找到两者都匹配,结果将是编译器错误。

您可以将整个过程视为从方法引用到 lambda 的转换。编译器提供了一个转换函数,它接受一个方法引用和目标类型,并可以推导出一个 lambda。

作用域

关于 lambda 的好消息是它们不会引入任何新的作用域。在 lambda 中使用变量将引用封闭环境中的变量。

这就是所谓的词法作用域。这意味着 lambda 根本不会引入新的作用域级别;您可以直接访问封闭范围中的字段、方法和变量。这也适用于this和super关键字。因此,我们不必担心解析作用域的疯狂嵌套类语法。

让我们来看一个例子。我们在这里有一个示例类,其中一个成员变量i设置为5的值。

public static class Example {
    int i = 5;

    public Integer example() {
        Supplier<Integer> function = () -> i * 2;
        return function.get();
    }
}

在example方法中,lambda 使用一个名为i的变量,并将其乘以 2。

因为 lambda 是词法作用域的,i简单地指的是封闭类的变量。它在运行时的值将是 5。使用this来强调这一点;在 lambda 中,this与外部相同。

public static class Example {
    int i = 5;
    public Integer example() {
        Supplier<Integer> function = () -> this.i * 2;
        return function.get();
    }
}

在下面的anotherExample方法中,使用了一个方法参数,也叫做i。通常的遮蔽规则在这里生效,i将指代方法参数而不是类成员变量。方法变量遮蔽了类变量。它的值将是传入方法的任何值。

public static class Example {
    int i = 5;

    public Integer anotherExample(int i) {
        Supplier<Integer> function = () -> i * 2;
        return function.get();
    }
}

如果你想引用类变量i而不是参数i,你可以使用这个方法使变量明确。例如,Supplier<Integer> function = () -> i * 2;。

以下示例在yetAnotherExample方法中定义了一个局部作用域变量。记住,lambda 使用其封闭作用域作为自己的作用域,所以在这种情况下,lambda 中的i指的是方法的变量;i将是15而不是5。

public static class Example {
    int i = 5;

    public Integer yetAnotherExample() {
        int i = 15;
        Supplier<Integer> function = () -> i * 2;
        return function.get();
    }
}

如果你想亲自看看,你可以使用以下方法打印出数值:

public static void main(String... args) {
    Example scoping = new Example();
    System.out.println("class scope        = " +    
    scoping.example());
    System.out.println("method param scope = " +  
    scoping.anotherExample(10));
    System.out.println("method scope       = " +   
    scoping.yetAnotherExample());
}

输出将如下所示:

class scope        = 10
method param scope = 20
method scope       = 30

因此,第一个方法打印10;类变量乘以 2 得到 5。第二个方法打印20,因为参数值为 10,乘以 2 得到 20,最后一个方法打印30,因为本地方法变量设置为 15,再次乘以 2 得到 30。

词法作用域意味着推迟到封闭环境。每个例子都有一个不同的封闭环境或作用域。你看到一个变量定义为类成员、方法参数和方法内部的局部变量。在所有情况下,lambda 的行为都是一致的,并引用了这些封闭作用域中的变量。

如果你已经熟悉基本的 Java 作用域,Lambda 作用域应该是直观的,这里真的没有什么新东西。

实际上 final

在 Java 7 中,传递给匿名类实例的任何变量都需要被设为 final。

这是因为编译器实际上将它所需的所有上下文或环境复制到匿名类的实例中。如果这些值在其下发生变化,可能会发生意外的副作用。因此 Java 要求变量是 final,以确保它不会改变,内部类可以安全地对其进行操作。安全地,我是指没有线程之间的竞争条件或可见性问题。

让我们来看一个例子。首先,我们将使用 Java 7 创建一个名为filter的方法,该方法接受一个人员列表和一个谓词。我们将创建一个临时列表来包含我们找到的任何匹配项,然后枚举每个元素,测试谓词对每个人是否成立。如果测试是积极的,我们将把他们添加到临时列表中,然后返回所有匹配项。

// java 7
private List<Person> filter(List<Person> people, Predicate<Person> predicate) {
    ArrayList<Person> matches = new ArrayList<>();
    for (Person person : people)
        if (predicate.test(person))
            matches.add(person);
    return matches;
}

然后我们将创建一个方法,使用这个方法来找到列表中所有符合退休条件的人。我们设置一个退休年龄变量,然后调用filter方法,传入一个任意的人员列表和一个Predicate接口的新匿名实例。

我们将实现这个方法,如果一个人的年龄大于或等于退休年龄变量,就返回 true。

 public void findRetirees() {
     int retirementAge = 55;
     List<Person> retirees = filter(allPeople, new 
     Predicate<Person>   
     () {
         @Override
         public boolean test(Person person) {
             return person.getAge() >= retirementAge; // <-- 
             compilation error
         }
     });
 }

如果你尝试编译这个,当访问变量时会得到编译器失败。这是因为变量不是 final。我们需要添加final使其编译。

final int retirementAge = 55;

注意

像这样将环境传递给匿名内部类是闭包的一个例子。环境是闭包"封闭"的内容;它必须捕获它需要执行任务的变量。Java 编译器使用复制技巧来实现这一点,而不是尝试管理对同一变量的多次更改。在闭包的上下文中,这被称为变量捕获。

Java 8 引入了"实际上 final"的概念,这意味着如果编译器可以确定特定变量永远不会改变,它可以在任何需要 final 变量的地方使用。它将其解释为"实际上"final。

在我们的例子中,如果我们切换到 Java 8 并删除final关键字。事情仍然可以编译。不需要使变量为 final。Java 知道变量不会改变,因此它实际上是 final。

int retirementAge = 55;

当然,如果你把它设为final,它仍然可以编译。

但是如果我们在初始化后尝试修改变量呢?

int retirementAge = 55;
// ...
retirementAge = 65;

编译器发现了变化,不能再将变量视为有效的最终值。我们得到了原始的编译错误,要求我们将其声明为最终值。相反,如果在变量声明中添加final关键字不会导致编译错误,那么该变量就是有效的最终值。

我一直在用匿名类示例来演示这一点,因为有效最终值的概念并不是特定于 lambda 的。当然,它也适用于 lambda。您可以将上面的匿名类转换为 lambda,而不会发生任何变化。仍然不需要将变量声明为最终值。

规避最终值

您仍然可以通过传递最终对象或数组来绕过安全网,然后在 lambda 中更改它们的内部。

例如,我们拿着我们的人员名单,假设我们想要计算他们所有的年龄总和。我们可以创建一个循环和求和的方法,就像这样:

private static int sumAllAges(List<Person> people) {
    int sum = 0;
    for (Person person : people) {
        sum += person.getAge();
    }
    return sum;
}

其中,当列表被枚举时,保持总和计数。作为一种替代,我们可以尝试抽象出循环行为,并传入一个要应用于每个元素的函数。就像这样:

public final static Integer forEach(List<Person> people, Function<Integer, Integer> function) {
  Integer result = null;
  for (Person t : people) {
    result = function.apply(t.getAge());
  }
  return result;
}

为了实现求和行为,我们只需要创建一个可以求和的函数。您可以使用匿名类来做到这一点:

private static void badExample() {
    Function<Integer, Integer> sum = new Function<Integer, Integer>   
   () {
        private Integer sum = 0;

        @Override
        public Integer apply(Integer amount) {
            sum += amount;
            return sum;
        }
   };
}

函数的方法接受一个整数并返回一个整数。在实现中,总和变量是一个类成员变量,并且每次应用函数时都会发生变化。这种变化通常在函数式编程中是不好的形式。

尽管如此,我们可以像这样将其传递给我们的forEach方法:

forEach(allPeople, sum);

我们将得到所有人年龄的总和。这是因为我们使用了相同的函数实例,所以sum变量在每次迭代期间被重用和改变。

坏消息是我们不能直接将这个转换成 lambda;在 lambda 中没有成员变量的等价物,所以除了在 lambda 之外没有地方可以放置sum变量。

double sum = 0;
forEach(allPeople, x -> {
    return sum += x;
});

但这突显了变量并不是有效的最终值(它在 lambda 的函数体中被改变),所以它必须被声明为最终值。

但如果我们将它声明为最终值

final double sum = 0;
forEach(allPeople, x -> {
    return sum += x;
});

我们不能再在函数体内修改它了!这是一个鸡生蛋蛋生鸡的情况。

绕过最终值

int[] sum = {0};
forEach(allPeople, x -> sum[0] += x);

数组引用在这里确实是最终的,但我们可以修改数组内容而不重新分配引用。然而,这通常是不好的形式,因为它会引发我们之前讨论的所有安全问题。我提到这个只是为了举例说明,但不建议您经常这样做。通常最好不要创建具有副作用的函数,如果您使用更多的函数式方法,完全可以避免这些问题。这种求和的更符合惯例的方法是使用所谓的fold或在 Java 术语中称为reduce。

异常处理

在 lambda 中没有新的异常处理语法。lambda 中抛出的异常会传播到调用者,就像您对常规方法调用的预期一样。调用 lambda 或处理它们的异常没有任何特殊之处。

然而,有一些微妙之处需要您注意。首先,作为 lambda 的调用者,您可能不知道可能抛出什么异常,如果有的话,其次,作为 lambda 的作者,您可能不知道您的 lambda 将在什么上下文中运行。

当您创建一个 lambda 时,通常会将如何执行该 lambda 的责任交给您传递给它的方法。就你所知,您的 lambda 可能会并行运行,或者在将来的某个时刻运行,因此您抛出的任何异常可能不会得到您的预期处理。您不能依赖异常处理来控制程序的流程。

为了证明这一点,让我们编写一些代码来依次调用两个东西。我们将使用Runnable作为一个方便的 lambda 类型。

public static void runInSequence(Runnable first, Runnable second) {
    first.run();
    second.run();
}

如果第一次调用 run 抛出异常,方法将终止,第二个方法将不会被调用。调用者需要处理异常。如果我们使用这种方法在两个银行账户之间转账,我们可能会编写两个 lambda。一个用于借方操作,一个用于贷方。

public void transfer(BankAccount a, BankAccount b, Integer amount) {
    Runnable debit = () -> a.debit(amount);
    Runnable credit = () -> b.credit(amount);
 }

然后我们可以这样调用我们的runInSequence方法:

public void transfer(BankAccount a, BankAccount b, Integer amount) {
    Runnable debit = () -> a.debit(amount);
    Runnable credit = () -> b.credit(amount);
    runInSequence(debit, credit);
 }

任何异常都可以通过使用try/catch来捕获和处理,就像这样:

public void transfer(BankAccount a, BankAccount b, Integer amount) {
    Runnable debit = () -> a.debit(amount);
    Runnable credit = () -> b.credit(amount);
    try {
        runInSequence(debit, credit);
    } catch (Exception e) {
        // check account balances and rollback
    }
  }

这就是问题所在。作为 lambda 的作者,我可能根本不知道runInSequence是如何实现的。它很可能是异步运行的,就像这样:

public static void runInSequence(Runnable first, Runnable second) {
    new Thread(() -> {
        first.run();
        second.run();
    }).start();
}

在这种情况下,第一个调用中的任何异常都将终止线程,异常将消失到默认的异常处理程序,我们原始的客户端代码将没有机会处理异常。

使用回调

顺便说一句,解决在不同线程上引发异常的特定问题的一种方法是使用回调函数。首先,您可以防御runInSequence方法中的异常:

public static void runInSequence(Runnable first, Runnable second) {
    new Thread(() -> {
        try {
            first.run();
            second.run();
        } catch (Exception e) {
            // ...
        }
    }).start();
}

然后引入一个异常处理程序,可以在发生异常时调用:

public static void runInSequence(Runnable first, Runnable second,
        Consumer<Throwable> exceptionHandler) {
    new Thread(() -> {
        try {
            first.run();
            second.run();
        } catch (Exception e) {
            exceptionHandler.accept(e);
        }
    }).start();
}

Consumer 是一个函数接口(Java 8 中的新功能),在这种情况下,它将异常作为参数传递给它的accept方法。

当我们将其连接到客户端时,我们可以传递一个回调 lambda 来处理任何异常。

public void nonBlockingTransfer(BankAccount a, BankAccount b, Integer amount) {
    Runnable debit = () -> a.debit(amount);
    Runnable credit = () -> b.credit(amount);
    runInSequence(debit, credit, (exception) -> {
      /* check account balances and rollback */
    });
}

这是延迟执行的一个很好的例子,因此它有自己的缺点。异常处理程序方法可能(或可能不会)在以后的某个时间点执行。nonBlockingTransfer方法将已经完成,银行账户本身可能在它触发时处于其他状态。您不能指望在方便的时候调用异常处理程序;我们已经打开了一个并发问题的大潘头。

处理编写 lambda 时的异常

让我们从 lambda 作者的角度来看待处理异常。之后,我们将看看在调用 lambda 时如何处理异常。

让我们看看如果我们想使用 lambda 来实现transfer方法,但这次想要重用一个提供runInSequence方法的现有库。

在开始之前,让我们看一下BankAccount类。您会注意到这次,debit和credit方法都会抛出一个已检查的异常;InsufficientFundsException。

class BankAccount {
    public void debit(int amount) throws InsufficientFundsException    
    {
        // ...
     }

     public void credit(int amount) throws 
     InsufficientFundsException   
     {
         // ...
      }
}

class InsufficientFundsException extends Exception { }

让我们重新创建transfer方法。我们将尝试创建借方和贷方 lambda,并将其传递给runInSequence方法。请记住,runInSequence方法是由某个库的作者编写的,我们无法看到或修改它的实现。

public void transfer(BankAccount a, BankAccount b, Integer amount) {
    Runnable debit = () -> a.debit(amount);   <- compiler error
    Runnable credit = () -> b.credit(amount); <- compiler error
    runInSequence(debit, credit);
 }

借方和贷方都会抛出一个已检查的异常,所以这次你会看到一个编译错误。如果我们将其添加到方法签名中也没有任何区别;异常会发生在 lambda 内部。记住我说过 lambda 中的异常会传播给调用者吗?在我们的情况下,这将是runInSequence方法,而不是我们定义 lambda 的地方。它们之间没有相互通信,可能会引发异常。

// still doesn't compile
public void transfer(BankAccount a, BankAccount b, Integer amount)
       throws InsufficientFundsException {
    Runnable debit = () -> a.debit(amount);
    Runnable credit = () -> b.credit(amount);
    runInSequence(debit, credit);
}

因此,如果我们无法强制要求已检查的异常在 lambda 和调用者之间是透明的,一种选择是将已检查的异常包装为运行时异常,就像这样:

public void transfer(BankAccount a, BankAccount b, Integer amount) {
    Runnable debit = () -> {
        try {
            a.debit(amount);
        } catch (InsufficientFundsException e) {
            throw new RuntimeException(e);
        }
   };
   Runnable credit = () -> {
       try {
           b.credit(amount);
       } catch (InsufficientFundsException e) {
           throw new RuntimeException(e);
       }
   };
   runInSequence(debit, credit);
 }

这样可以解决编译错误,但这还不是全部。这非常冗长,我们仍然需要捕获和处理现在是运行时异常的调用runInSequence。

public void transfer(BankAccount a, BankAccount b, Integer amount){
    Runnable debit = () -> { ... };
    };
    Runnable credit = () -> { ... };
    try {
        runInSequence(debit, credit);
    } catch (RuntimeException e) {
        // check balances and rollback
    }
}

尽管还有一两个小问题;我们抛出并捕获了一个RuntimeException,这可能有点松散。我们真的不知道在runInSequence方法中可能会抛出什么其他异常。也许更明确一些会更好。让我们创建RuntimeException的一个新子类型,然后使用它。

class InsufficientFundsRuntimeException extends RuntimeException {
    public   
    InsufficientFundsRuntimeException(InsufficientFundsException   
    cause) {
        super(cause);
    }
}

在我们修改原始 lambda 以抛出新异常之后,我们可以限制 catch 处理只处理我们知道的异常;即InsufficientFundsRuntimeException。

现在我们可以实现某种余额检查和回滚功能,并确信我们理解了可能导致它的所有情况。

public void transfer(BankAccount a, BankAccount b, Integer amount) {
    Runnable debit = () -> {
        try {
            a.debit(amount);
        } catch (InsufficientFundsException e) {
            throw new InsufficientFundsRuntimeException(e);
        }
   };
   Runnable credit = () -> {
       try {
           b.credit(amount);
       } catch (InsufficientFundsException e) {
           throw new InsufficientFundsRuntimeException(e);
       }
   };
   try {
       runInSequence(debit, credit);
   } catch (InsufficientFundsRuntimeException e) {
       // check balances and rollback
   }
 }

所有这些麻烦之处在于,代码中的异常处理样板比实际业务逻辑更多。Lambda 应该使事情变得不那么冗长,但这里充满了噪音。如果我们将受检异常的包装泛化为运行时等价物,我们可以做得更好。我们可以创建一个捕获异常类型的泛型签名的函数接口。

让我们称它为Callable,它的单个方法是call。不要将其与 JDK 中同名的类混淆;我们正在创建一个新类来说明如何处理异常。

@FunctionalInterface
interface Callable<E extends Exception> {
    void call() throws E;
}

我们将更改 transfer 的旧实现,并创建 lambda 以匹配新函数接口的“形状”。我暂时省略了类型。

public void transfer(BankAccount a, BankAccount b, Integer amount) {
    ??? debit = () -> a.debit(amount);
    ??? credit = () -> b.credit(amount);
 }

从类型推断部分可以看出,Java 可以将其视为Callable类型,因为它没有参数,Callable也没有参数,它具有相同的返回类型(无)并且抛出与接口相同类型的异常。我们只需要给编译器一个提示,这样我们就可以将其分配给Callable的实例。

public void transfer(BankAccount a, BankAccount b, Integer amount) {
    Callable<InsufficientFundsException> debit = () ->   
    a.debit(amount);
    Callable<InsufficientFundsException> credit = () -> 
    b.credit(amount);
 }

像这样创建 lambda 不会导致编译错误,因为函数接口声明可能会抛出异常。它不需要在我们创建 lambda 的时候警告我们,因为函数方法的签名会在我们尝试调用它时引发编译器错误,如果需要的话。就像普通方法一样。

如果我们尝试将它们传递给runInSequence方法,我们将会得到一个编译错误。

public void transfer(BankAccount a, BankAccount b, Integer amount) {
    Callable<InsufficientFundsException> debit = () -> 
    a.debit(amount);
    Callable<InsufficientFundsException> credit = () -> 
    b.credit(amount);
    runInSequence(debit, credit); <- doesn't compile
 }

lambda 的类型是错误的。我们仍然需要一个Runnable类型的 lambda。我们将不得不编写一个方法,可以将Callable转换为Runnable。与此同时,我们将受检异常包装为运行时异常。就像这样:

public static Runnable unchecked(Callable<InsufficientFundsException> function) {
    return () -> {
        try {
            function.call();
        } catch (InsufficientFundsException e) {
            throw new InsufficientFundsRuntimeException(e);
        }
    };
}

现在要做的就是将其连接到我们的 lambda 中:

public void transfer(BankAccount a, BankAccount b, Integer amount) {
    Runnable debit = unchecked(() -> a.debit(amount));
    Runnable credit = unchecked(() -> b.credit(amount));
    runInSequence(debit, credit);
 }

一旦我们把异常处理放回去,我们就回到了更简洁的方法体,并且以与以前相同的方式处理了潜在的异常。

public void transfer(BankAccount a, BankAccount b, Integer amount) {
    Runnable debit = unchecked(() -> a.debit(amount));
    Runnable credit = unchecked(() -> b.credit(amount));
    try {
         runInSequence(debit, credit);
    } catch (InsufficientFundsRuntimeException e) {
        // check balances and rollback
    }
 }

这个缺点是这不是一个完全通用的解决方案;我们仍然需要为不同的函数创建 unchecked 方法的变体。我们只是把冗长的语法隐藏起来了。冗长仍然存在,只是被移动了。是的,我们已经从中获得了一些重复利用,但是如果异常处理是透明的,或者我们没有检查异常,我们就不需要掩盖问题了。

值得指出的是,如果我们在 Java 7 中使用匿名类而不是 lambda,我们可能最终会做类似的事情。在 Java 8 之前仍然可以做很多这样的事情,你最终会创建辅助方法来推迟冗长。

当然,lambda 为小型匿名功能提供了更简洁的表示,但由于 Java 的受检异常模型,处理 lambda 中的异常通常会导致我们之前遇到的所有冗长问题。

作为调用者(在调用 lambda 时处理异常)

我们已经从编写 lambda 的角度看到了一些事情,现在让我们看看在调用 lambda 时的情况。

现在假设我们正在编写一个提供runInSequence方法的库。这次我们有更多的控制权,不再局限于使用Runnable作为 lambda 类型。因为我们不想强迫客户在他们的 lambda 中处理异常(或将它们包装为运行时异常),我们将提供一个函数接口,声明可能会抛出受检异常。

我们将称它为FinancialTransfer,有一个transfer方法:

@FunctionalInterface
interface FinancialTransfer {
    void transfer() throws InsufficientFundsException;
}

我们在说,每当发生银行交易时,都有可能出现资金不足的情况。然后当我们实现我们的runInSequence方法时,我们接受这种类型的 lambda。

public static void runInSequence(FinancialTransfer first,
        FinancialTransfer second) throws InsufficientFundsException      
{
    first.transfer();
    second.transfer();
 }

这意味着当客户端使用该方法时,他们不必强制在他们的 lambda 内部处理异常。例如,编写这样的方法。

// example client usage
public void transfer(BankAccount a, BankAccount b, Integer amount) {
    FinancialTransfer debit = () -> a.debit(amount);
    FinancialTransfer credit = () -> b.credit(amount);
 }

这次在创建 lambda 时不会有编译器错误。无需将BankAccount方法的异常包装为运行时异常;函数接口已经声明了异常。但是,runInSequence现在会抛出一个已检查的异常,因此明确表示客户端必须处理可能性,您将看到一个编译器错误。

public void transfer(BankAccount a, BankAccount b, Integer amount) {
    FinancialTransfer debit = () -> a.debit(amount);
    FinancialTransfer credit = () -> b.credit(amount);
    runInSequence(debit, credit);  <- compiler error
 }

因此,我们需要用try/catch来包装调用,以使编译器满意:

public void transfer(BankAccount a, BankAccount b, Integer amount) {
    FinancialTransfer debit = () -> a.debit(amount);
    FinancialTransfer credit = () -> b.credit(amount);
    try {
        runInSequence(debit, credit);  <- compiler error
    } catch (InsufficientFundsException e) {
        // whatever
    }
 }

最终的结果类似于我们之前看到的,但不需要未经检查的方法。作为库开发人员,我们使客户更容易与我们的代码集成。

但是,如果我们尝试一些更奇特的东西呢?让我们再次将runInSequence方法异步化。无需从方法签名中抛出异常,因为如果从不同的线程中抛出异常,它不会传播到调用者。因此,这个版本的runInSequence方法不包括 throws 子句,transfer方法也不再被强制处理异常。然而,对.transfer的调用仍然会抛出异常。

public static void runInSequence(Runnable first, Runnable second) {
    new Thread(() -> {
        first.transfer();   <- compiler error
        second.transfer();  <- compiler error
    }).start();
}

public void transfer(BankAccount a, BankAccount b, Integer amount) {
    FinancialTransfer debit = () -> a.debit(amount);
    FinancialTransfer credit = () -> b.credit(amount);
    runInSequence(debit, credit);  <- compiler error
 }

由于runInSequence方法中仍然存在编译器错误,我们需要另一种处理异常的方法。一种技术是传入一个在发生异常时将被调用的函数。我们可以使用这个 lambda 将异步运行的代码与调用者连接起来。

首先,我们将重新添加catch块,并传入一个函数接口作为异常处理程序。我将在这里使用Consumer接口,它是 Java 8 中的新功能,并且是java.util.function包的一部分。然后在 catch 块中调用接口方法,传入原因。

public void runInSequence(FinancialTransfer first,     
       FinancialTransfer second,
       Consumer<InsufficientFundsException> exceptionHandler) {
    new Thread(() -> {
        try {
            first.transfer();
            second.transfer();
        } catch (InsufficientFundsException e) {
            exceptionHandler.accept(e);
        }
    }).start();
}

要调用它,我们需要更新transfer方法,以传入一个回调的 lambda。下面的参数 exception 将是传递给runInSequence中的accept方法的任何内容。它将是InsufficientFundsException的一个实例,客户端可以自行处理。

public void transfer(BankAccount a, BankAccount b, Integer amount) {
    FinancialTransfer debit = () -> a.debit(amount);
    FinancialTransfer credit = () -> b.credit(amount);
    Consumer<InsufficientFundsException> handler = (exception) -> {
        /* check account balances and rollback */
   };
   runInSequenceNonBlocking(debit, credit, handler);
}

我们已经为我们的库提供了一种替代的异常处理机制,而不是强制它们捕获异常。

我们已经将异常处理内部化到库代码中。这是延迟执行的一个很好的例子;如果有异常,客户端不一定知道何时会调用他的异常处理程序。例如,由于我们在另一个线程中运行,银行账户本身可能在执行时已经被更改。再次强调,使用异常来控制程序流是一种有缺陷的方法。您不能指望在方便时调用异常处理程序。

Lambda vs 闭包

术语闭包和lambda经常可以互换使用,但它们实际上是不同的。在本节中,我们将看一下它们之间的区别,以便您清楚地知道哪个是哪个。

下面是显示每个 Java 主要版本的发布日期的表格。Java 5.0 于 2004 年推出,包括了第一个重大的语言更改,包括泛型支持等内容:

https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/lrn-java-lmd/img/timeline.jpg

大约在 2008 年至 2010 年期间,有很多工作正在进行,以引入闭包到 Java 中。原本计划在 Java 7 中引入,但最终没有及时实现。相反,它演变为了 Java 8 中的 lambda 支持。不幸的是,在那个时候,人们经常互换使用术语“闭包”和“lambda”,因此对于 Java 社区来说有点混淆。事实上,OpenJDK 网站上仍然有一个closures的项目页面,还有一个lambdas的项目页面。

从 OpenJDK 项目的角度来看,他们真的应该从一开始就一直使用“lambda”。事实上,OpenJDK 搞错了,他们忽略了 Java 自 1.1 版本以来就已经支持闭包。

我在这里有点追究,因为尽管闭包和 lambda 之间存在技术上的差异,但两个项目的目标都是实现相同的事情,即使它们在术语上使用不一致。

那么 lambda 和闭包之间的区别是什么?基本上,闭包是lambda 的一种类型,但 lambda 不一定是闭包。

基本区别

就像 lambda 一样,闭包实际上是一个匿名的功能块,但有一些重要的区别。闭包依赖于外部值(不仅仅是它的参数),而 lambda 只依赖于它的参数。闭包被认为是“关闭”它所需的环境。

例如,以下内容:

(server) -> server.isRunning();

是一个 lambda,但这

() -> server.isRunning();

这是一个闭包。

它们都返回一个布尔值,指示某个服务器是否正在运行,但一个使用它的参数,另一个必须从其他地方获取变量。它们都是 lambda;从一般意义上讲,它们都是匿名的功能块,在 Java 语言意义上,它们都使用新的 lambda 语法。

第一个例子是指传递给 lambda 的服务器变量作为参数,而第二个例子(闭包)从其他地方获取服务器变量;也就是环境。为了获取变量的实例,lambda 必须“关闭”环境或捕获 server 的值。我们在之前谈到“有效最终”时已经看到了这一点。

让我们扩展这个例子,以便更清楚地看到事情。首先,我们将在一个静态类中创建一个方法来执行一个简单的轮询和等待。它将在每次轮询时检查一个函数接口,以查看是否满足某个条件。

class WaitFor {
    static <T> void waitFor(T input, Predicate<T> predicate)
            throws InterruptedException {
        while (!predicate.test(input))
            Thread.sleep(250);
    }
}

我们使用Predicate(另一个新的java.util接口)作为我们的函数接口,并测试它,如果条件不满足,就暂停一小段时间。我们可以用一个简单的 lambda 调用这个方法,检查某个 HTTP 服务器是否正在运行。

void lambda() throws InterruptedException {
    waitFor(new HttpServer(), (server) -> !server.isRunning());
}

服务器参数由我们的waitFor方法提供,并且将是我们刚刚定义的HttpServer的实例。它是一个 lambda,因为编译器不需要捕获服务器变量,因为我们在运行时手动提供它。

注意

顺便说一句,我们可能可以使用方法引用… waitFor(new HttpServer(), HttpServer::isRunning);

现在,如果我们将其重新实现为一个闭包,它将看起来像这样。首先,我们必须添加另一个waitFor方法。

static void waitFor(Condition condition) throws InterruptedException {
    while (!condition.isSatisfied())
        Thread.sleep(250);
}

这次,使用一个更简单的签名。我们传递一个不需要参数的函数接口。Condition接口有一个简单的isSatisfied方法,没有参数,这意味着我们必须提供实现可能需要的任何值。它已经暗示了它的用法可能会导致闭包。

使用它,我们会写成这样:

void closure() throws InterruptedException {
    Server server = new HttpServer();
    waitFor(() -> !server.isRunning());
}

这里服务器实例没有作为参数传递给 lambda,而是从封闭的作用域中访问。我们已经定义了变量,lambda 直接使用它。这个变量必须被编译器捕获或复制。lambda“关闭”了服务器变量。

这个“关闭”表达式来自这样一个想法,即具有开放绑定(或自由变量)的 lambda 表达式已经被词法环境或作用域关闭(或绑定)。结果是一个封闭的表达式。没有未绑定的变量。更准确地说,闭包关闭的是值而不是变量。

我们已经看到闭包被用来提供一个匿名的功能块,以及等效的 lambda 之间的区别,但是我们仍然可以做出更有用的区分。

其他区别

匿名函数是一个没有名称的函数文字,而闭包是函数的一个实例。根据定义,lambda 没有实例变量;它不是一个实例。它的变量是作为参数提供的。然而,闭包有实例变量,这些变量在实例创建时提供。

有了这个理解,lambda 通常比闭包更有效,因为它只需要评估一次。一旦有了函数,就可以重复使用它。由于闭包封闭了其本地环境之外的东西,因此每次调用它都必须进行评估。每次使用时都必须新建一个实例。

我们在函数与类部分看到的所有问题在这里也是相关的。使用闭包而不是 lambda 可能会考虑到内存问题。

总结

我们在这里谈论了很多,让我们简要总结一下区别。

Lambda 只是匿名函数,类似于 Java 中的静态方法。就像静态方法一样,它们不能引用其范围之外的变量,除了它们的参数。一种特殊类型的 lambda,称为闭包,可以捕获其范围之外的变量(或者封闭它们),以便可以使用外部变量或它们的参数。因此,简单的规则是,如果 lambda 使用了其范围之外的变量,它也是一个闭包。

闭包可以被视为函数的实例。这对于 Java 开发人员来说有点奇怪的概念。

一个很好的例子是传统的匿名类,如果没有新的 lambda 语法,我们将传递它们。这些可以“封闭”变量,因此它们本身也是闭包。因此,自 Java 1.1 以来,我们一直在 Java 中支持闭包。

看看这个例子。服务器变量必须由编译器封闭,以便在Condition接口的匿名实例中使用。这既是匿名类实例,也是闭包。

@since Java 1.1!
void anonymousClassClosure() {
    Server server = new HttpServer();
    waitFor(new Condition() {
        @Override
        public Boolean isSatisfied() {
            return !server.isRunning();
        }
    });
}

Lambda 并不总是闭包,但闭包总是 lambda。

在本节中,我们将探讨当您将匿名类编译为 lambda 时,编译器输出的差异。首先,我们会提醒自己关于 java 字节码以及如何阅读它。然后,我们将研究匿名类和 lambda 在捕获变量和不捕获变量时的情况。我们将比较 Java 8 之前的闭包与 lambda,并探讨 lambda 并不仅仅是语法糖,而是与传统方法产生非常不同的字节码。

字节码回顾

首先,让我们回顾一下我们对字节码的了解。

从源代码到可执行的机器代码。Java 编译器生成字节码。这要么由 JVM 解释,要么由即时编译器重新编译。

当它被解释时,字节码会即时转换为机器码并执行。这每次遇到字节码时都会发生,但是 JVM。

当它被即时编译时,JVM 会直接将其编译成机器码,然后继续执行。

两者都发生在运行时,但即时编译提供了许多优化。

因此,Java 字节码是源代码和机器代码之间的中间表示。

注意

作为一个快速的旁注:Java 的 JIT 编译器多年来一直享有很好的声誉。但回到我们的介绍,是约翰·麦卡锡在 1960 年首次写了关于 JIT 编译的。因此,有趣的是,不仅 lambda 支持受到 LISP 的影响。 (Aycock 2003, 2. JIT Compilation Techniques, 2.1 Genesis, p. 98)。

字节码是 JVM 的指令集。顾名思义,字节码由单字节指令(称为操作码)以及与参数相关的字节组成。因此,可能有 256 个操作码可用,尽管实际使用的只有大约 200 个。

JVM 使用基于栈的计算模型,如果我们想要增加一个数字,必须使用栈来实现。所有指令或操作码都针对栈进行操作。

所以例如,5 + 1 变成了 5 1 +,其中 5 被推入栈中,

https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/lrn-java-lmd/img/Push5.jpg

1 被推入栈,然后…

https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/lrn-java-lmd/img/Push1.jpg

应用了 + 运算符。加号会弹出栈顶的两个帧,将数字相加,然后将结果推回栈上。结果如下。

https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/lrn-java-lmd/img/iadd6.jpg

每个操作码都像这样针对栈进行操作,所以我们可以将我们的示例转换为一系列 Java 字节码:

https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/lrn-java-lmd/img/Push5.jpg

push 5 操作码变成了 iconst_5。

https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/lrn-java-lmd/img/iconst1.jpg

push 1 操作码变成了 iconst_1:

https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/lrn-java-lmd/img/iconst_1_5.jpg

add 变成了 iadd。

https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/lrn-java-lmd/img/iadd6.jpg

iconst_x 操作码和 iadd 操作码是操作码的示例。操作码通常具有前缀和/或后缀来指示它们作用的类型,在这些示例中,i 指的是整数,x 是特定于操作码的后缀。

我们可以将操作码分为以下几类:

组示例
栈操作aload_n, istore, swap, dup2
控制流指令goto, ifeq, iflt
对象交互new, invokespecial, areturn
算术、逻辑和类型转换iadd, fcmpl, i2b

与栈操作有关的指令,如 aload 和 istore。

使用像 goto 和 if equal 这样的操作码来控制程序流程。

创建对象和访问方法使用像 new 和 invokespecial 这样的代码。当我们查看用于调用 lambda 表达式的不同操作码时,我们将特别关注这个组。

最后一组是关于算术、逻辑和类型转换的,包括像 iadd、float compare long (fcmpl) 和 integer to byte (i2b) 这样的代码。

描述符

操作码通常会使用参数,它们在字节码中看起来有点晦涩,因为它们通常是通过查找表引用的。在内部,Java 使用所谓的 描述符 来描述这些参数。

它们使用特定的语法描述类型和签名,你将在整个字节码中看到这种语法。你经常会在编译器或调试输出中看到相同的语法,所以在这里回顾一下是有用的。

这是一个方法签名描述符的示例。

Example$1."<init>":(Lcom/foo/Example;Lcom/foo/Server;)V

它描述了一个名为 $1 的类的构造函数,我们碰巧知道这是 JVM 对另一个类内第一个匿名类实例的命名。在这种情况下是 Example。所以我们有一个匿名类的构造函数,它接受两个参数,一个是外部类 com.foo.Example 的实例,另一个是 com.foo.Server 的实例。

作为构造函数,该方法不返回任何东西。V 符号表示 void。

看一下下面的描述符语法的分解。如果在描述符中看到大写的 Z,它指的是布尔值,大写的 B 指的是字节,依此类推。

https://github.com/OpenDocCN/freelearn-java-zh/raw/master/docs/lrn-java-lmd/img/descriptors.jpg

还有一些需要提到的:

  • 类使用大写的 L 描述,后面跟着完全限定的类名,然后是一个分号。类名使用斜杠而不是点来分隔。

  • 数组使用一个开放的方括号后跟列表中的类型来描述。没有闭合方括号。

转换方法签名

让我们将以下方法签名转换为方法描述符:

long f (int n, String s, int[] array);

该方法返回一个 long,所以我们用括号描述它是一个方法,返回一个大写的 J。

()J

第一个参数是 int 类型,所以我们使用大写的 I。

(I)J

下一个参数是一个对象,所以我们使用 L 来描述它是一个对象,完全限定名称并用分号关闭。

(ILString;)J

最后一个参数是整数数组,所以我们插入数组语法,然后是 int 类型:

(ILString;[I)J

然后我们完成了。一个 JVM 方法描述符。

代码示例

让我们看一下一些示例生成的字节码。

我们将根据我们在“lambda vs 闭包”部分中看到的示例,查看四个不同功能块的字节码。

我们将探讨:

  1. 一个简单的匿名类。

  2. 一个匿名类关闭某些变量(旧样式闭包)。

  3. 没有参数的 lambda。

  4. 带参数的 lambda。

  5. 闭包覆盖某些变量的 lambda(新样式闭包)。

示例字节码是使用javap命令行工具生成的。本节仅显示了部分字节码列表,完整的源代码和字节码列表请参见附录 A。此外,完全限定的类名已经被缩短以更好地适应页面。

示例 1

第一个示例是一个简单的匿名类实例,传递给我们的waitFor方法。

public class Example1 {
    // anonymous class
    void example() throws InterruptedException {
        waitFor(new Condition() {
            @Override
            public Boolean isSatisfied() {
                return true;
            }
        });
    }
}

如果我们看一下下面的字节码,要注意的是匿名类的一个实例在第 6 行被新建。#2是一个查找,其结果显示在注释中。因此,它使用new操作码与常量池中的#2进行操作,这恰好是匿名类Example$1。

void example() throws java.lang.InterruptedException;
    descriptor: ()V 
    flags:
    Code:
      stack=3, locals=1, args_size=1
         0: new #2 // class Example1$1
         3: dup
         4: aload_0
         5: invokespecial #3 // Method Example1$1."":(LExample1;)V
         8: invokestatic #4 // Method WaitFor.waitFor:  
            (LCondition;)V
        11: return
      LineNumberTable:
        line 10: 0
        line 16: 11
      LocalVariableTable:
        Start Length Slot Name Signature
            0     12    0 this LExample1;
    Exceptions: 
      throws java.lang.InterruptedException

创建后,使用invokespecial在第 9 行调用构造函数。这个操作码用于调用构造方法、私有方法和超类的可访问方法。您可能会注意到方法描述符包括对Example1的引用。所有匿名类实例都有对父类的这个隐式引用。

下一步使用invokestatic调用我们的waitFor方法,传递匿名类在第 10 行。invokestatic操作码用于调用静态方法,非常快速,因为它可以直接拨号方法,而不是像在对象层次结构中那样找出要调用的方法。

示例 2

Example 2类是另一个匿名类,但这次它关闭了server变量。这是一种旧式的闭包:

public class Example2 {
    // anonymous class (closure)
    void example() throws InterruptedException {
        Server server = new HttpServer();
        waitFor(new Condition() {
            @Override
            public Boolean isSatisfied() {
                return !server.isRunning();
            }
        });
    }
}

字节码与之前类似,只是Server类的一个实例被新建(在第 3 行),并且在第 5 行调用了它的构造函数。匿名类$1的实例仍然是用invokespecial构造的(在第 11 行),但这次它除了调用类的实例外,还将Server的实例作为参数。

为了关闭server变量,它直接传递给匿名类:

void example() throws java.lang.InterruptedException;
    Code:
       0: new #2 // class Server$HttpServer
       3: dup
       4: invokespecial #3 // Method Server$HttpServer."":()V
       7: astore_1
       8: new #4 // class Example2$1
      11: dup
      12: aload_0
      13: aload_1
      14: invokespecial #5 // Method Example2$1."":   
          (LExample2;LServer;)V
      17: invokestatic #6 // Method WaitFor.waitFor:(LCondition;)V
      20: return

示例 3

Example 3类使用了一个 Java lambda 与我们的waitFor方法。这个 lambda 除了返回 true 之外什么也不做。它相当于示例 1。

public class Example3 {
    // simple lambda
    void example() throws InterruptedException {
        waitFor(() -> true);
    }
}

这次字节码非常简单。它使用invokedynamic操作码在第 3 行创建 lambda,然后将其传递给下一行的invokestatic操作码。

 void example() throws java.lang.InterruptedException;
     Code:
        0: invokedynamic #2, 0 // InvokeDynamic #0:isSatisfied:   
           ()LCondition;
        5: invokestatic #3 // Method WaitFor.waitFor:(LCondition;)V
        8: return

invokedynamic调用的描述符针对Condition接口的isSatisfied方法(第 3 行)。

我们在这里看不到invokedynamic的机制。invokedynamic操作码是 Java 7 的新操作码,旨在为 JVM 上的动态语言提供更好的支持。它通过直到运行时才将类型链接到方法来实现这一点。其他“invoke”操作码都在编译时解析类型。

对于 lambda 来说,这意味着占位符方法调用可以像我们刚刚看到的那样放入字节码中,并且实现可以在 JVM 上在运行时完成。

如果我们查看包含常量池的更详细的字节码,我们可以解除查找。例如,如果我们查找编号 2,我们可以看到它引用了#0和#26。

Constant pool:
   #1 = Methodref #6.#21 // Object."":()V
   #2 = InvokeDynamic #0:#26 // #0:isSatisfied:()LCondition;
   ...
BootstrapMethods:
    0: #23 invokestatic LambdaMetafactory.metafactory:              
            (LMethodHandles$Lookup;LString;
            LMethodType;LMethodType;
            LMethodHandle;LMethodType;)LCallSite;
      Method arguments:
        #24 ()LBoolean;
        #25 invokestatic Example3.lambda$example$25:()LBoolean;
        #24 ()LBoolean;

常量0在一个特殊的查找表中用于引导方法(第 6 行)。它引用了对 JDK LambdaMetafactory的静态方法调用,以创建 lambda。这就是繁重的工作所在。所有目标类型推断以适应类型和任何部分参数评估都在这里进行。

实际的 lambda 显示为一个名为lambda$example$25的方法句柄(第 12 行),没有参数,返回一个布尔值。它使用invokestatic调用,这表明它被视为一个真正的函数;它没有与之关联的对象。与之前的匿名示例不同,它也没有对包含类的隐式引用。

它被传递到LambdaMetafactory,我们知道它是一个方法句柄,通过在常量池中查找它。lambda 的编号由编译器分配,并且对于每个需要的 lambda,它只是从零递增。

Constant pool:
    // invokestatic Example3.lambda$example$25:()LBoolean;
    #25 = MethodHandle #6:#35

示例 4

Example 4类是另一个 lambda,但这次它以Server的实例作为参数。它在功能上等同于示例 2,但它不会闭包变量;它不是一个闭包。

public class Example4 {
    // lambda with arguments
    void example() throws InterruptedException {
        waitFor(new HttpServer(), (server) -> server.isRunning());
    }
}

就像示例 2 一样,字节码必须创建服务器的实例,但这次,invokedynamic操作码引用了Predicate类型的test方法。如果我们按照引用(#4)到引导方法表,我们会看到实际的 lambda 需要一个HttpServer类型的参数,并返回一个Z,这是一个原始布尔值。

void example() throws java.lang.InterruptedException;
    descriptor: ()V
    flags:
    Code:
      stack=2, locals=1, args_size=1
         0: new #2 // class Server$HttpServer
         3: dup
         4: invokespecial #3 // Method Server$HttpServer."":()V
         7: invokedynamic #4, 0 // InvokeDynamic #0:test:  
            ()LPredicate;
        12: invokestatic #5 // Method WaitFor.waitFor:     
            (LObject;LPredicate;)V
        15: return
      LineNumberTable:
        line 13: 0
        line 15: 15
      LocalVariableTable:
        Start Length Slot Name Signature
            0     16    0 this LExample4;
    Exceptions:
      throws java.lang.InterruptedException

因此,对 lambda 的调用仍然是静态方法调用,但这次在调用时将变量作为参数传递。

示例 4(使用方法引用)

有趣的是,如果我们使用方法引用,功能完全相同,但得到的字节码不同。

public class Example4_method_reference {
    // lambda with method reference
    void example() throws InterruptedException {
        waitFor(new HttpServer(), HttpServer::isRunning);
    }
}

通过对LambdaMetafactory的调用,当最终执行发生时,method_reference会导致调用invokevirtual而不是invokestatic。invokevirtual操作码用于调用公共、受保护和包保护方法,因此需要一个实例。实例由metafactory方法提供,根本不需要 lambda(或静态函数);在这个字节码中没有lambda$。

void example() throws java.lang.InterruptedException;
    descriptor: ()V
    flags:
    Code:
      stack=2, locals=1, args_size=1
         0: new #2 // class Server$HttpServer
         3: dup
         4: invokespecial #3 // Method Server$HttpServer."":()V
         7: invokedynamic #4, 0 // InvokeDynamic #0:test:   
            ()LPredicate;
        12: invokestatic #5 // Method WaitFor.waitFor:
            (LObject;LPredicate;)V
        15: return
      LineNumberTable:
        line 11: 0
        line 12: 15
      LocalVariableTable:
        Start Length Slot Name Signature
            0     16    0 this LExample4_method_reference;
    Exceptions:
      throws java.lang.InterruptedException

示例 5

最后,示例 5 使用 lambda,但是闭包了服务器实例。它等同于示例 2,是一种新的样式闭包。

public class Example5 {
    // closure
    void example() throws InterruptedException {
        Server server = new HttpServer();
        waitFor(() -> !server.isRunning());
    }
}

它以与其他 lambda 相同的方式介绍了基础知识,但是如果在引导方法表中查找metafactory方法,您会注意到这次 lambda 的方法句柄具有Server类型的参数。它使用invokestatic(第 9 行)调用,并且变量在调用时直接传递到 lambda 中。

BootstrapMethods:
    0: #34 invokestatic LambdaMetafactory.metafactory:
            (LMethodHandles$Lookup;
             LString;LMethodType;
             LMethodType;
             LMethodHandle;LMethodType;)LCallSite;
      Method arguments:
        #35 ()LBoolean; // <-- SAM method to be implemented by the        
            lambda
        #36 invokestatic Example5.lambda$example$35:
          (LServer;)LBoolean;
        #35 ()LBoolean; // <-- type to be enforced at invocation   
        time

因此,就像示例 2 中的匿名类一样,编译器添加了一个参数来捕获该术语,但这次是方法参数而不是构造函数参数。

总结

我们看到使用匿名类将创建一个新实例,并使用invokespecial调用它的构造函数。

我们看到闭包变量的匿名类在其构造函数上有一个额外的参数,用于捕获该变量。

我们看到 Java lambda 使用invokedynamic指令来推迟类型的绑定,并且特殊的lambda$方法句柄用于实际表示 lambda。在这种情况下,该方法句柄没有参数,并且使用invokestatic调用它,使其成为真正的函数。

lambda 是由LambdaMetafactory类创建的,它本身是invokedynamic调用的目标。

当 lambda 有参数时,我们看到LambdaMetafactory描述要传递到 lambda 中的参数。invokestatic操作码用于执行 lambda,就像以前一样。但我们还看了一个方法引用,代替 lambda。在这种情况下,没有创建lambda$方法句柄,而是使用invokevirtual直接调用方法。

最后,我们看了一个闭包变量的 lambda。这个创建了一个lambda$方法句柄上的参数,并且再次使用invokestatic调用。

lambda-1.1.16-javadoc.jar 立即下载

相关推荐

【一】PCIe基础知识

Linux PCIe基础知识

他山之石 可以攻玉 4万+

Java完全参考手册 第11版

Java完全参考手册 第11版 【下载地址】Java完全参考手册第11版 《Java完全参考手册》第11版是Java编程领域的权威指南,全面覆盖Java SE 11标准,适合所有层次的开发者。本书由著名作者Herbert Schildt撰写,内容涵盖从基础语法到高级特性的方方面面,包括数据类型、控制语句、类与对象、多线程...

gitblog_06788的博客 520

可信数据空间:可信空间、区域功能节点、业务功能节点、连接器的关联关系

可信数据空间通过分层架构实现安全数据共享:可信空间作为顶层制定全局规则;区域功能节点协调辖区内功能节点协作;功能节点提供具体数据服务;连接器作为唯一通信通道确保安全交互。系统采用"规则自上而下约束,数据自下而上流动"机制,通过分层治理和标准化连接,在保证数据主权的同时实现跨域协作,具有策略分级执行、去中心化治理等特性,支持灵活扩展业务场景。

友莘居士的博客 1686

死磕Lambda表达式(一):初识Lambda

Lambda表达式是表示可传递匿名函数的一种简洁方式,Lambda表达式没有名称,但是有参数列表、函数主体、返回类型,还可能有一个可以抛出的异常列表。

万猫学社 2万+

Android12 车载开发TaskView实现分屏

这里只给出TaskView的代码,相关联的源码,请看源码,如果想要使用TaskView需要自己去编译frameWork的源码,打包一个aar,或者jar出来使用,否则无法直接使用。1.如果分屏的应用如启动两个应用按照权重占比,互相不叠加在一起,点触摸击事件都可响应,例如左边启动QQ了,右边启动的是微信,二者窗口都可操作。TaskView是谷歌整合安卓多任务多窗口下交互重新重构的独立出来的module,本身继承自ServiceView,TaskView的相关概念性的东西参考。//创建的taskview。

android_bird_bird的博客 9550

迅投miniQMT量化交易实战指南:从入门到自动化交易系统构建

迅投miniQMT量化交易实战指南摘要 本文全面介绍迅投miniQMT量化交易系统的开发流程,重点包括: 环境配置:详细说明miniQMT极简模式的安装步骤,包括xtquant库获取、Python版本要求及验证方法 行情数据模块:解析xtdata模块的核心功能,包括: 获取板块分类与股票列表 查询合约基础信息 下载历史K线数据(支持tick、分钟、日线等周期) 交易执行模块:介绍xttrader模块的交易API,涵盖: 账户连接与初始化 订单管理(下单、撤单、查询) 持仓与资产查询 实战开发:提供可直接运行

huanghm88的专栏 667

Lambda完整学习指南

Lambda表达式是Java SE 8中一个重要的新特性。lambda表达式允许你通过表达式来代替功能接口。lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码块)。Lambda表达式还增强了集合库。Java Lambda表达式的一个重要用法是简化某些匿名内部类(Anonymous Classes)的写法。实际上Lambda表达式并不仅仅是匿名内部类的语法糖,JVM内部是通过invokedynamic指令来实现Lambda表达式的。

z_ssyy的博客 3472

esp8266步进电机驱动控制

blinker控制步进电机起保停,正反转(Arduino uno+esp8266+TB6600驱动器) Arduino uno的板子的程序 esp8266板子接入到Arduino uno的控制引脚上的5,6,7 esp8266程序 //****库***** #define BLINKER_WIFI //官方wifi协议库 #define BLINKER_PRINT Serial //串口协议库 #include <Blinker.h&gt

perseverance51博客 2855

从入门到入土:Lambda完整学习指南,包教包会!

什么是Lambda表达式 Lambda表达式是Java SE 8中一个重要的新特性。lambda表达式允许你通过表达式来代替功能接口。lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码块)。Lambda表达式还增强了集合库。 Java Lambda表达式的一个重要用法是简化某些匿名内部类(Anonymous Classes)...

Java笔记虾 6050

高云FPGA系列教程(2):FPGA点灯工程创建、程序下载和固化

相比于国外FPGA厂商EDA开发环境,几个G到几十个G的大小,高云云源软件安装包也很小巧,界面也很简洁,综合速度非常快,是我没想到的,好像在使用单片机一样,点一下就编译完成了,以本文简单的LED示例工程为例,比特流文件几乎在几秒钟内生成;无论是下载比特流文件到RAM还是Flash中,速度都很快,而且只有一种fs程序文件格式,不用再进行bit/mcs转换;云源软件自带的编辑器也很好用,和Notepad++界面差不多,而且快捷键很方便,具有基本的自动补全功能。

whik1194的博客 5461

Codex 100个真实案例 - 用AI做在线考试系统(自动组卷+防作弊)

本案例使用 Codex 从零搭建一个功能完整的在线考试系统。涵盖题库管理(单选/多选/判断/填空)、智能随机组卷、考试倒计时、自动判分、成绩统计分析、以及多维度防作弊机制(切屏检测、禁止复制粘贴、IP限制等)。前端使用 Vue 3 + Element Plus 构建考试界面,后端使用 Node.js + Express + MySQL 提供 API 服务。全程用自然语言驱动 Codex 完成开发,最终交付一个可直接部署的在线考试平台。

yueyeguzhuo的专栏 81

【Java 8系列】Lambda 表达式,一看就废

热门系列: 【Java编程系列】二进制如何表示小数?0.3+0.6为什么不等于0.9?纳尼!!! 【Java 8系列】Java开发者的判空利器 -- Optional 程序人生,精彩抢先看 目录 1.前言 1.1 为什么 Java 需要 Lambda 表达式? 2.正文 2.1Lambda的编写方式 2.2Lambda 表达式的结构 2.3函数式接口 2.4方法引用 3.总结 1.前言 Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的...

Yangy的博客 4451

YOLOv11改进策略【模型轻量化】| 替换骨干网络 CVPR-2024 RepViT 轻量级的Vision Transformers架构

本文记录的是基于RepVit的YOLOv11轻量化改进方法研究。RepVit的网络结构借鉴ViT的设计理念,通过分离的token mixe和减少推理时的计算和内存成本,同时减少扩展比率并增加宽度,降低延迟,并通过来弥补参数大幅减少的问题,提高了准确性。本文在替换骨干网络中配置了原论文中的和五种模型,以满足不同的需求。

Limiiiing的博客 2975

Java·Lambda

本次文章主要为大家讲解了什么是数据结构中的Lambda,Lambda表达式是Java SE 8中一个重要的新特性。lambda表达式允许你通过表达式来代替功能接口。 lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码块)。

xinhang10的博客 6145

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

Java8一Lambda与函数式接口

关于Lambda表示在工作学习中会经常用到,但并没有全面的去了解。在这里做一个较为详细的记录供以后学习查阅。主要参考Java 8 Lambda 表达式 引言 Java8之前,我们在使用Runnale创建线程的时候,经常需要将Runable实例传入new Thread中。一般采用匿名内部类将函数作为参数的形式传入 new Thread(new Runnable() { @O...

J.Jian博客 8831

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

JAVA高级(一)——lambda

JAVA高级(一)——lambdalambda基础1、是什么是函数是接口?2、lambda的特点3、在哪里以及如何使用Lambdalambda实现:环绕执行模式1、行为参数化2、使用函数时接口传递行为3、执行一个行为并转为lambda使用函数式接口1、常用的函数型接口2、重构使用lambda的问题2.1 重构lambda找不到情况2.2 从lambda表达式到方法引用的转换3、使用lambda重构设计模式3.1 重构策略模式3.2 模板方法 lambda基础 1、是什么是函数是接口? 函数式接口(Func

XiaoCoder的博客 3815

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

Java Lambda 学习指南(二)

原文:zh.annas-archive.org/md5/6C9C9AEB5C060CEC246B793E59A880AA。

龙哥盟 455

一:java-lambda 之什么是 lambda

什么是 lambda 表达式: “Lambda 表达式”(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包(注意和数学传统意义上的不同)。 lambda表达式为java添加了缺失的函数式编程特性,使我们能将函数当做一等公民看待 在将函数作为一等公民的语言中(JavaScript, Python 等),lambda表达式

charles_Lii的博客 498

Java8 Lambda表达式

Java8 Lambda表达式是简洁的匿名函数写法,用于实现函数式接口。基本语法为(参数) -> 表达式,支持类型推断和简化写法。核心概念包括函数式接口(如Predicate、Consumer、Function、Supplier等)、方法引用(四种类型:静态/实例/构造方法引用)和Stream API(用于集合处理)。方法引用简化了Lambda表达式,Stream API提供了函数式集合操作方式,包括中间操作和终端操作。这些特性使Java代码更简洁、可读性更强。

qq_44474810的博客 1036

深入理解 Java Lambda

Java Lambda Java Lambda Lambda Lambda 函数式接口 Lambda为什么可以做到类型推导?可以做到什么程度? 方法应用 Lambda和匿名类是什么关系?仅仅是语法糖? Java编译器如何处理Lambda? Lambda lambda名称的由来 许多年前,在计算机出来之前,有位名叫 Alonzo Church 的逻辑学家,他想证明 什么样...

何欣澄的博客 1891

Java中的Lambda

大家应该都知道面向对象的特性(抽象、封装、继承、多态)。其实在 出现之前,我们关注的往往是某一类对象应该具有什么样的属性,当然这也是面向对象的核心–对数据进行抽象。但是出现以后,这一点开始出现变化,在某种场景下,更加关注某一类共有的行为(这与之前的接口有些类似),这也就是提出函数式编程的目的。面向对象编程到面向行为编程的变化我们知道,对于一个变量,我们可以赋给其一个**“值”**。如果你想把**“一块代码”**赋给一个变量,应该怎么做呢?比如,我想把右边那块代码,赋给一个叫做的变量:在之前,这个是做不到的

qq_25775675的博客 4679

【Java基础:系统性学习指南】Lambda与Stream API的现代化编程

摘要:本文聚焦于Java中Lambda表达式与Stream API的现代化编程特性。深入剖析Lambda的本质,通过字节码对比,阐述从匿名内部类到invokedynamic指令的转变。同时,探讨Stream API在使用时的性能陷阱,尤其是并行流的错误使用场景,并给出数据竞争案例。此外,详细介绍Optional类的正确用法,提倡使用orElseGet()进行延迟计算以替代orElse()。通过丰富的实操流程和完整代码示例,助力读者全面掌握这些现代化编程技巧。

专注于人工智能、软件开发、工控自动化、工厂数字化及智能化等领域,希望和大家共同进步! 540

JAVA的Lambda

Lambda 表达式是简化函数式编程的重要工具,能够提高代码的可读性和灵活性。常见的函数式接口包括FunctionConsumerSupplierPredicate和BiFunction,这些接口可以帮助我们在不同场景下使用 Lambda 表达式。Lambda 表达式的基本模板易于理解,能够帮助我们在 Java 中实现更为灵活的函数式编程。

xjsj62728的博客 1208

Java 8 Lambda函数编程入门(一)

什么是函数式编程每个人对函数式编程的理解不尽相同。但其核心是:在思考问题时,使用不可变值和函 数,函数对一个值进行处理,映射成另一个值。

WSYW126的博客 1938

Java Lambda

或参数列表:Lambda表达式的参数列表可以包含零个、一个或多个参数,参数类型可以省略,因为编译器能够根据上下文推断出参数的类型,这称为类型推断。箭头操作符:箭头操作符->将参数列表与Lambda主体隔开。Lambda主体:Lambda主体可以是一个表达式或一个代码块。如果是一个表达式,那么表达式的值会自动作为返回值;如果是一个代码块,那么必须使用return语句来返回值。在Java中,任何接口如果只有一个抽象方法,那么它就是一个函数式接口。这个概念在包中得到了广泛的应用。

曾续缘的博客 889
上一篇: PyTorch 生成式人工智能学习指南(五)
下一篇: Java Lambda 学习指南(二)
绝不原创的飞龙
绝不原创的飞龙 优质创作者: 人工智能、数据科学与机器学习技术领域 优质创作者: 人工智能、数据科学与机器学习技术领域
博客等级 码龄15年 4万+粉丝 2万+原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值