深入理解 Java 中的 Lambda

作者:李三石 来源:my.oschina.net/leili

我花了相当多的阅读和编码时间才最终理解Java Lambdas如何在概念上正常工作的。我阅读的大多数教程和介绍都遵循自顶向下的方法,从用例开始,最后以概念性问题结束。在这篇文章中,我想提供一个自下而上的解释,从其他已建立的Java概念中推导出Lambdas的概念。

首先介绍下方法的类型化,这是支持方法作为一流公民的先决条件。基于此,Lambdas的概念是被以匿名类用法的进化和特例提出的。所有这一切都通过实现和使用高阶函数映射来说明。

这篇文章的主要受众是那些已掌握函数式编程基础的人,以及那些想从概念上理解Lambdas如何嵌入Java语言的人。

方法类型

从Java 8起方法就是一等公民了。按照标准的定义,编程语言中的一等公民是一个具有下列功能的实体,

  • 可以作为参数进行传递,
  • 可以作为方法的返回值
  • 可以赋值给一个变量.

在Java中,每一个参数、返回值或变量都是有类型的,因此每个一等公民都必须是有类型的。Java中的一种类型可以是以下内容之一:

  • 一种内建类型 (比如 int 或者 double)
  • 一个类 (比如ArrayList)
  • 一个接口 (比如 Iterable)

方法是通过接口进行定义类型的。它们不隐式的实现特定接口,但是在必要的时候,如果一个方法符合一个接口,那么在编译期间,Java编译器会对其进行隐式的检查。举个例子说明:

class LambdaMap {
    static void oneStringArgumentMethod(String arg) {
        System.out.println(arg);
    }
}

关于oneStringArgumentMethod函数的类型,与之相关的有:它的的函数是静态的,返回类型是void,它接受一个String类型的参数。一个静态函数符合包含一个apply函数的接口,apply函数的签名相应地符合这个静态函数的签名。oneStringArgumentMethod函数对应的接口因此必须符合下列标准。

  • 它必须包含一个名为apply的函数。
  • 函数返回类型必须是void。
  • 函数必须接受一个String类型可以转换到的对象的参数。

在符合这个标准的接口之中,下面的这个是最明确的:

interface OneStringArgumentInterface {
    void apply(String arg);
}

利用这个接口,函数可以分配给一个变量:

OneStringArgumentInterface meth = LambdaMap::oneStringArgumentMethod;

用这种方法使用接口作为类型,函数可以借此被分配给变量,传递参数并且从函数返回:

static OneStringArgumentInterface getWriter() {
    return LambdaMap::oneStringArgumentMethod;
}

static void write(OneStringArgumentInterface writer, String msg) {
    writer.apply(msg);
}

最终函数是一等公民。

泛型函数类型

就像使用集合一样,泛型为函数类型增加了大量的功能和灵活性。实现功能上的算法而不考虑类型相关信息,泛型函数类型使其变为可能。在对map函数的实现中,会在下面用到这种功能。

在这提供的OneStringArgumentInterface一个泛型版本:

interface OneArgumentInterface<T> {
    void apply(T arg);
}

OneStringArgumentInterface函数可以被分配给它:

OneArgumentInterface<String> meth = LambdaMap::oneStringArgumentMethod;

通过使用泛型函数类型,它现在可以以一种通用的方法实现算法,就像它在集合中使用的一样:

static <T> void applyArgument(OneArgumentInterface<T> meth, T arg) {
    meth.apply(arg);
}

上面的函数并没有什么用,然而它至少可以提出一个想法:对函数作为第一个类成员的支持怎样可以形成非常简洁且灵活的代码:

applyArgument(Lambda::oneStringArgumentMethod, "X ");

实现map

在诸多高阶函数中,map是最经典的. map的第一个参数是函数,该函数可以接收一个参数并返回一个值;第二个参数是值列表. map使用传入的函数处理值列表的每一项,然后返回一个新的值列表。下面Python的代码片段,可以很好的说明map的用法:

>>> map(math.sqrt, [1, 4, 9, 16])
[1.0, 2.0, 3.0, 4.0]

在本节的后续内容中,将给出该函数的Java实现。Java 8已经通过Stream提供了该函数。因为主要出于教学目的,所以,本节中给出的实现特意保持简单,仅限于List对象使用。

与Python不同,在Java中必须首先考虑map第一个参数的类型:一个可以接收一个参数并返回一个值的方法。参数的类型和返回值的类型可以不同。下面接口符合这个预期,显然,I表示参数(入参),O表示返回值(出参):

interface MapFunction<I, O> {
    O apply(I in);
}

泛型map方法的实现,变得惊人的简单明了:

static <I, O> List<O> map(MapFunction<I, O> func, List<I> input) {
    List<O> out = new ArrayList<>();

    for (I in : input) {
        out.add(func.apply(in));
    }

    return out;
}
  1. 创建新的返回值列表out(用于保存O类型的对象).
  2. 通过遍历input,func处理列表的每一项,并将返回值添加到out中。
  3. 返回out.

下面是实际使用map方法的实例:

MapFunction<Integer, Double> func = Math::sqrt;

List<Double> output = map(func, Arrays.asList(1., 4., 9., 16.));
System.out.println(output);

在Python one-liner的推动下,可以用更简洁的方法表达:

System.out.println(map(Math::sqrt, Arrays.asList(1., 4., 9., 16.)));

Java毕竟不是Python…

Lambdas来了!

读者可能会注意到,还没有提到Lambdas。这是由于采用了“自下而上”的方式描述,现在基础已基本建立,Lambdas将在后续的章节中介绍。

下面的用例作为基础:一个double类型的list,表示半径,然后得到一个列表,表示圆面积。map方法就是为此任务预先准备的。计算圆面积的公式是众所周知的:

A = r2π

应用这个公式的方法很容易实现:

static Double circleArea(Double radius) {
    return Math.pow(radius, 2) * Math.PI;
}

这个方法现在可以用作map方法的第一个参数:

System.out.println(
        map(LambdaMap::circleArea,
            Arrays.asList(1., 4., 9., 16.)));

如果circleArea方法只需要这一次, 没有道理把类接口被他弄得乱七八糟,也没有道理将实现和真正使用它的地方分离。最佳实践是使用用匿名内部类。可以看到,实例化一个实现MapFunction接口的匿名内部类可以很好的完成这个任务:

System.out.println(
        map(new MapFunction<Double, Double>() {
                public Double apply(Double radius) {
                    return Math.sqrt(radius) * Math.PI;
                }
            },
            Arrays.asList(1., 2., 3., 4.)));

这看起来很漂亮,但是很多人会认为函数式的解决方案更清晰,更具可读性:

List<Double> out = new ArrayList<>();
for (Double radius : Arrays.asList(1., 2., 3., 4.)) {
    out.add(Math.sqrt(radius) * Math.PI);
}
System.out.println(out);

到目前为止,最后是使用Lambda表达式。 读者应该注意Lambda如何取代上面提到的匿名类:

System.out.println(
        map(radius -> { return Math.sqrt(radius) * Math.PI; },
            Arrays.asList(1., 2., 3., 4.)));

这看起来简洁明了 - 请注意 Lambda 表达式如何缺省任何明确的类型信息。 没有显式模板实例化,没有方法签名。

Lambda表达式由两部分组成,这两部分被->分隔。第一部分是参数列表,第二部分是实际实现。

Lambda表达式和匿名内部类作用完全相同,然而它摒弃了许多编译器可以自动推断的样板代码。让我们再次比较这两种方式,然后分析编译器为开发人员节省了哪些工作。

MapFunction<Double, Double> functionLambda =
        radius -> Math.sqrt(radius) * Math.PI;

MapFunction<Double, Double> functionClass =
        new MapFunction<Double, Double>() {
            public Double apply(Double radius) {
                return Math.sqrt(radius) * Math.PI;
            }
        };
  • 对于Lambda实现来说,只有一个表达式,返回语句和花括号可以省略。这使得代码更简短。
  • Lambda表达式的返回值类型是从Lambda实现推断出来的。
  • 对于参数类型,我不完全确定,但我认为必须从Lambda表达式所处的上下文中推断出参数类型。
  • 最后编译器必须检查返回值类型是否与Lambda的上下文匹配,以及参数类型是否与Lambda实现匹配。

这一切都可以在编译期间完成,根本没有运行时开销。

结语

总而言之,Java中的Lambdas的概念是整洁的。我支持编写更简洁、更清晰的代码,并让程序员免于编写可由编译器自动推断的架手架代码。它是语法糖,如上所述,它只不过是使用匿名类也能实现的功能。然而,我会说它是非常甜的语法糖。

另一方面,Lambdas还支持更加混淆以及难以调试的代码。Python社区很早就意识到了这一点 - 虽然Python也有Lambda,但它若被广泛使用则通常被认为是不好的风格(当嵌套函数可以被使用时,它并不难于规避)。对于Java来说,我会给出类似的建议。毫无疑问,在某些情况下,使用Lambdas会导致代码大大缩减并更易读,尤其在与流有关时。在其他情况下,如果采取更保守的做法和最佳实践,另外一种方法可能会是更好的替代。

另外,最近新建了一个微信交流学习群,想要进入的小伙伴可以在后台加一下。

原文发布于微信公众号 - java技术学习之道(javajsxxzd)

原文发表时间:2018-06-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏恰同学骚年

《代码的未来》读书笔记:也谈闭包

  原文中使用了C语言的函数对象,这里我们主要从.NET平台来说。在.NET中,委托这个概念对C++程序员来说并不陌生,因为它和C++中的函数指针非常类似,很多...

732
来自专栏小小挖掘机

关于Python语言规范你需要知道的一些小tips

之前写代码感觉一直缺乏一定的规范,所以整理了一些Python的语言规范方面的东东,这个来自google发布的开源项目风格指南-Python语言规范。 1、对你的...

2816
来自专栏Phoenix的Android之旅

重构 - 完全不用 if-else 可能吗?

上次那篇重构-为什么 if-else 不是好代码 说到代码中的 if-else会随着代码量的增加,在迭代的过程中变的越来越难以维护, 然后用工厂模式的思路可以把...

702
来自专栏Android群英传

Kotlin Primer·第四章·Kotlin 的类特性(下)

672
来自专栏微信公众号:Java团长

Java集合框架综述

最近打算复习复习JDK中的集合框架,并尝试分析其源码,这么做一方面是这些类非常实用,掌握其实现能更好的优化我们的程序;另一方面是学习借鉴JDK是如何实现了这么一...

763
来自专栏静晴轩

Lua代码片段收集

Lua实现闭包 --[[@Func :实现闭包 @Desc : 当一个函数内部嵌套另一个函数定义时,内部的函数体可以访问外部的函数的局部变量,这种特征...

2775
来自专栏小白的技术客栈

Python面向对象编程-完整版

面向对象是一种编程范式。范式是指一组方法论。编程范式是一组如何组织代码的方法论。编程范式指的是软件工程中的一种方法学。

953
来自专栏向治洪

Kotlin 是如何避免空指针问题的

在谈Kotlin的优势的时候,大家都会想到空指针安全这一点,那么Kotlin又是如何避免这些问题的呢?下面从Kotlin的一些语法规则上给出介绍。 可空类型 默...

2057
来自专栏一个会写诗的程序员的博客

《Kotlin 极简教程 》第4章 基本数据类型与类型系统

到目前为止,我们已经了解了Kotlin的基本符号以及基础语法。我们可以看出,使用Kotlin写的代码更简洁、可读性更好、更富有生产力。

772
来自专栏java一日一条

Java集合框架综述

近被陆陆续续问了几遍HashMap的实现,回答的不好,打算复习复习JDK中的集合框架,并尝试分析其源码,这么做一方面是这些类非常实用,掌握其实现能更好的优化我们...

481

扫码关注云+社区