深入学习 Java 中的 Lambda

前言

我花了相当多的阅读和编码时间才最终理解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会导致代码大大缩减并更易读,尤其在与流有关时。在其他情况下,如果采取更保守的做法和最佳实践,另外一种方法可能会是更好的替代。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 如何更快乐的使用Java 8中的Lambda特性

    前言 Java 8 的 Lambda 特性较之于先前的泛型加入更能鼓舞人心的,我对 Lambda 的理解是它得以让 Java 以函数式思维的方式来写代码.而写出的代码是否是函数式,并不单纯在包含了多少 Lambda 表达式,而在思维,要神似. 实际中看过一些代码,为了 Lambda 表达式而 Lambda(函数式),有一种少年不识愁滋味,为赋新词强说愁的味道.从而致使原本一个简单的方调用硬生生的要显式的用类如 apply(), accept(obj) 等形式.不仅造成代码可读性差,且可测试性也变

  • Java8使用lambda实现Java的尾递归

    前言 本篇介绍的不是什么新知识,而是对前面讲解的一些知识的综合运用.众所周知,递归是解决复杂问题的一个很有效的方式,也是函数式语言的核心,在一些函数式语言中,是没有迭代与while这种概念的,因为此类的循环通通可以用递归来实现,这类语言的编译器都对递归的尾递归形式进行了优化,而Java的编译器并没有这样的优化,本篇就要完成这样一个对于尾递归的优化. 什么是尾递归 本篇将使用递归中最简单的阶乘计算来作为例子 递归实现 /** * 阶乘计算 -- 递归解决 * * @param number 当前阶

  • Java8简单了解Lambda表达式与函数式接口

    Java8被称作Java史上变化最大的一个版本.其中包含很多重要的新特性,最核心的就是增加了Lambda表达式和StreamAPI.这两者也可以结合在一起使用.首先来看下什么是Lambda表达式. 使用Lambda表达式不仅让代码变的简单.而且可读.最重要的是代码量也随之减少很多.然而,在某种程度上,这些功能在Scala等这些JVM语言里已经被广泛使用. 并不奇怪,Scala社区是难以置信的,因为许多Java 8里的内容看起来就像是从Scala里搬过来的.在某种程度上,Java 8的语法要比Sc

  • java中Lambda常用场景代码实例

    本文实例为大家分享了java中Lambda常用场景的具体代码,供大家参考,具体内容如下 public class test18 { /** * lambda表达式的常用场景 */ @Test public void test() { List<String> list_one = new ArrayList<>(); list_one.add("NIKE"); list_one.add("Addidas"); /** * 用在匿名内部类里简写

  • Java8与Scala中的Lambda表达式深入讲解

    前言 最近几年Lambda表达式风靡于编程界.很多现代编程语言都把它作为函数式编程的基本组成部分.基于JVM的编程语言如Scala.Groovy及Clojure把它作为关键部分集成在语言中.而如今,(最终)Java 8也加入了这个有趣的行列. Java8 终于要支持Lambda表达式!自2009年以来Lambda表达式已经在Lambda项目中被支持.在那时候,Lambda表达式仍被称为Java闭包.在我们进入一些代码示例以前,先来解释下为什么Lambda表达式在Java程序员中广受欢迎. 1.为

  • java使用lambda表达式对List集合进行操作技巧(JDK1.8)

    具体代码如下所示: import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; import java.util.stream.Collectors; public class LambdaStudy { public static void main(String[] args) { //初始化list集合 List<String> list = new ArrayList&l

  • 详解Java中的Lambda表达式

    简介 Lambda表达式是Java SE 8中一个重要的新特性.lambda表达式允许你通过表达式来代替功能接口. lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码块). Lambda表达式还增强了集合库. Java SE 8添加了2个对集合数据进行批量操作的包: java.util.function 包以及java.util.stream 包. 流(stream)就如同迭代器(iterator),但附加了许多额外的功能.

  • Java8中Lambda表达式使用和Stream API详解

    前言 Java8 的新特性:Lambda表达式.强大的 Stream API.全新时间日期 API.ConcurrentHashMap.MetaSpace.总得来说,Java8 的新特性使 Java 的运行速度更快.代码更少.便于并行.最大化减少空指针异常. 0x00. 前置数据 private List<People> peoples = null; @BeforeEach void before () { peoples = new ArrayList<>(); peoples

  • 深入学习 Java 中的 Lambda

    前言 我花了相当多的阅读和编码时间才最终理解Java Lambdas如何在概念上正常工作的.我阅读的大多数教程和介绍都遵循自顶向下的方法,从用例开始,最后以概念性问题结束.在这篇文章中,我想提供一个自下而上的解释,从其他已建立的Java概念中推导出Lambdas的概念. 首先介绍下方法的类型化,这是支持方法作为一流公民的先决条件.基于此,Lambdas的概念是被以匿名类用法的进化和特例提出的.所有这一切都通过实现和使用高阶函数映射来说明. 这篇文章的主要受众是那些已掌握函数式编程基础的人,以及那

  • 关于Java 中的 Lambda 表达式

    这篇文章我们将讨论关于Java 中的 Lambda 表达式,Lambda 表达式是 Java 涉足函数式编程的过程.它接受参数并将其应用于表达式或代码块.以下是语法的基本示例: (parameter1, parameter2) => expression 或者 (parameter1, parameter2) => {code block} Lambda 表达式非常有限,如果它不是 void,则必须立即返回一个值.他们不能使用诸如 if 或 for 之类的关键字来保持简单性.如果需要更多行代码

  • Java中使用Lambda表达式和函数编程示例

    目录 1.简单介绍 2.Lambdas和Scopes 3.Lambdas与局部变量 4.Lambda体与局部变量 5.Lambdas和'This'和'Super'关键字 6.Lambdas和Exceptions 7.预定义的功能接口 1.简单介绍 第一个示例演示变量声明上下文中的lambda.它将lambda()->{System.out.println("running"):}分配给可运行接口类型的变量r. 第二个示例类似,但演示了赋值上下文中的lambda(到先前声明的变量r

  • 学习Java中的List集合

    目录 1.概述 2.List的使用 2.1List的常用方法 3.List的实现类 3.1ArrayList 3.2Vector 3.3LinkedList 3.4ArrayList与Vector的区别 1.概述 List是一个有序集合(也被称为序列).此接口的用户在列表中的每个元素都被插入的地方有精确的控制.用户可以通过它们的整数索引(在列表中的位置)访问元素,并在列表中搜索元素. 说是List集合,其实只是习惯说法,因为它是Collection接口的一个子接口(Collection有很多的子

  • Java 中的 Lambda List 转 Map 的多种方法详解

    目录 故事背景 公共代码 方式一(partitioningBy 分两组) 方式二(groupingBy 分多组) 方式三(toMap 自定义<Key, Value>) 故事背景 我们平时在项目中经常会遇到 List 转 Map 的情况,但是传统的方式又显得太臃肿,于是就想到 Lambda 神器,今天我们就来看看都有哪几种转换方式(List -> Map) 公共代码 // Person 实体类 @Data class Person { private String uuid; privat

  • 快速入门Java中的Lambda表达式

    Lambda简介 Lambda表达式是Java SE 8中一个重要的新特性.lambda表达式允许你通过表达式来代替功能接口. lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码块). Lambda表达式还增强了集合库. Java SE 8添加了2个对集合数据进行批量操作的包: java.util.function 包以及 java.util.stream 包. 流(stream)就如同迭代器(iterator),但附加了许多

  • 深入理解Java中的Lambda表达式

    Java 8 开始出现,带来一个全新特性:使用 Lambda 表达式 (JSR-335) 进行函数式编程.今天我们要讨论的是 Lambda 的其中一部分:虚拟扩展方法,也叫做公共辩护(defender)方法.该特性可以让你在接口定义中提供方法的默认实现.例如你可以为已有的接口(如 List 和 Map)声明一个方法定义,这样其他开发者就无需重新实现这些方法,有点像抽象类,但实际却是接口.当然,Java 8 理论上还是兼容已有的库. 虚拟扩展方法为 Java 带来了多重继承的特性,尽管该团队声称与

  • 深入学习Java中的SPI机制

    概述 SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现. Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦. SPI整体机制图如下 当服务

  • 学习Java中的日期和时间处理及Java日历小程序的编写

    Java 在 java.util 包中提供了 Date 类,这个类封装了当前的日期和时间. Date 类支持两种构造函数.第一个构造函数初始化对象的当前日期和时间. Date( ) 下面的构造函数接收一个参数等于自1970年1月1日午夜起已经过的毫秒数 Date(long millisec) 一旦有一个可用的日期对象,可以调用以下任何一种支持的方法使用时间: SN 方法和描述 1 boolean after(Date date) 如果调用Date对象包含或晚于指定的日期则返回true,否则,返回

随机推荐