深入理解 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 条评论
登录 后参与评论

相关文章

来自专栏web前端教室

javascript 红皮高程(10)

继续string类型的相关知识哈,不细看不知道啊,这JS的知识点真是太细碎了。因为许多知识点都互相交织着,但某些属性却并不是所有的对象都有。例如: 转换字符...

1877
来自专栏逸鹏说道

我为NET狂面试题-基础篇-答案

面向过程: 答案:图片只贴核心代码,完整代码请打开解决项目查看 (答案不唯一,官方答案只供参考,若有错误欢迎提出~) 99乘法表 https://githu...

32913
来自专栏HappenLee的技术杂谈

C++雾中风景11:厘清C++之中的类型转换

开门见山,先聊聊笔者对类型转换的看法吧。从设计上看,一门面向对象的语言是不一样提供类型转换的,这种方式破坏了类型系统。C++为了兼容C也不得不吞下这个苦果,在实...

1013
来自专栏lulianqi

UNICODE与ASCII

     ASCII 是用来表示英文字符的一种编码规范。每个ASCII字符占用1 个字节,因此,ASCII 编码可以表示的最大字符数是255(00H—FFH)。...

923
来自专栏C/C++基础

C++中函数重载、隐藏、覆盖和重写的区别

C++规定在同一作用域中,同名函数的形式参数(指参数的个数、类型或者顺序)不同时,构成函数重载。

772
来自专栏工科狗和生物喵

【计算机本科补全计划】C++ Primer:指针和const限定符

正文之前 今天下午看了一下午的计算机组成与设计,结果好死不死的看到了设计部分--处理器的设计。天哪,我现在还只是一个准备给人装一台电脑做实验田的家伙,连用都不咋...

2704
来自专栏PHP在线

PHP实现四种基本排序算法

许多人都说算法是程序的核心,算法的好坏决定了程序的质量。作为一个初级phper,虽然很少接触到算法方面的东西。但是对于基本的排序算法还是应该掌握的,它是程序开发...

35112
来自专栏华章科技

从Zero到Hero,一文掌握Python关键代码

首先,什么是 Python?根据 Python 创建者 Guido van Rossum 所言,Python 是一种高级编程语言,其设计的核心理念是代码的易读性...

773
来自专栏java一日一条

Java有值类型吗?

有人看了我之前的文章『Swift 语言的设计错误』,问我:“你说 Java 只有引用类型(reference type),但是根据 Java 的官方文档,Jav...

732
来自专栏Play & Scala 技术分享

为Play初学者准备的Scala基础知识

3196

扫码关注云+社区