写点什么

Java SE 8: Lambda 表达式

2013 年 8 月 15 日

Java SE 8 在 6 月 13 的版本中已经完全了全部的功能。在这些新的功能中, lambda 表达式是推动该版本发布的最重要新特性。因为 Java 第一次尝试引入函数式编程的相关内容。社区对于 lambda 表达式也期待已久。Lambda 表达式的相关内容在 JSR 335 中定义,本文的内容基于最新的规范和 JDK 8 Build b94 。 开发环境使用的是 Eclipse

Lambda 表达式

要理解 lambda 表达式,首先要了解的是函数式接口(functional interface)。简单来说,函数式接口是只包含一个抽象方法的接口。比如 Java 标准库中的 java.lang.Runnable java.util.Comparator 都是典型的函数式接口。对于函数式接口,除了可以使用 Java 中标准的方法来创建实现对象之外,还可以使用 lambda 表达式来创建实现对象。这可以在很大程度上简化代码的实现。在使用 lambda 表达式时,只需要提供形式参数和方法体。由于函数式接口只有一个抽象方法,所以通过 lambda 表达式声明的方法体就肯定是这个唯一的抽象方法的实现,而且形式参数的类型可以根据方法的类型声明进行自动推断。

以 Runnable 接口为例来进行说明,传统的创建一个线程并运行的方式如下所示:

复制代码
public void runThread() {
new Thread(new Runnable() {
public void run() {
System.out.println("Run!");
}
}).start();
}

在上面的代码中,首先需要创建一个匿名内部类实现 Runnable 接口,还需要实现接口中的 run 方法。如果使用 lambda 表达式来完成同样的功能,得到的代码非常简洁,如下面所示:

复制代码
public void runThreadUseLambda() {
new Thread(() -> {
System.out.println("Run!");
}).start();
}
{1}

相对于传统的方式,lambda 表达式在两个方面进行了简化:首先是 Runnable 接口的声明,这可以通过对上下文环境进行推断来得出;其次是对 run 方法的实现,因为函数式接口中只包含一个需要实现的方法。

Lambda 表达式的声明方式比较简单,由形式参数和方法体两部分组成,中间通过“->”分隔。形式参数不需要包含类型声明,可以进行自动推断。当然在某些情况下,形式参数的类型声明是不可少的。方法体则可以是简单的表达式或代码块。

比如把一个整数列表按照降序排列可以用下面的代码来简洁实现:

复制代码
Collections.sort(list, (x, y) -> y - x);

Lambda 表达式“(x, y) -> y - x“实现了 java.util.Comparator 接口。

在 Java SE 8 之前的标准库中包含的函数式接口并不多。Java SE 8 增加了 java.util.function 包,里面都是可以在开发中使用的函数式接口。开发人员也可以创建新的函数式接口。最好在接口上使用注解 @FunctionalInterface 进行声明,以免团队的其他人员错误地往接口中添加新的方法。

下面的代码使用函数式接口 java.util.function.Function 实现的对列表进行 map 操作的方法。从代码中可以看到,如果尽可能的使用函数式接口,则代码使用起来会非常简洁。

复制代码
public class CollectionUtils {
public static <t r=""></t> List<r></r> map(List<t></t> input, Function<t></t> processor) {
ArrayList<r></r> result = new ArrayList<r></r>();
for (T obj : input) {
result.add(processor.apply(obj));
}
return result;
}
public static void main(String[] args) {
List<string></string> input = Arrays.asList(new String[] {"apple", "orange", "pear"});
List<integer></integer> lengths = CollectionUtils.map(input, (String v) -> v.length());
List<string></string> uppercases = CollectionUtils.map(input, (String v) -> v.toUpperCase());
}
}

方法和构造方法引用

方法引用可以在不调用某个方法的情况下引用一个方法。构造方法引用可以在不创建对象的情况下引用一个构造方法。方法引用是另外一种实现函数式接口的方法。在某些情况下,方法引用可以进一步简化代码。比如下面的代码中,第一个 forEach 方法调用使用的是 lambda 表达式,第二个使用的是方法引用。两者作用相同,不过使用方法引用的做法更加简洁。

复制代码
List<string></string> input = Arrays.asList(new String[] {"apple", "orange", "pear"});
input.forEach((v) -> System.out.println(v));
input.forEach(System.out::println);

构造方法可以通过名称“new”来进行引用,如下面的代码所示:

复制代码
List<long></long> dateValues = Arrays.asList(new Long[] {0L, 1000L});
List<date></date> dates = CollectionUtils.map(dateValues, Date::new);

接口的默认方法

Java 开发中所推荐的实践是面向接口而不是实现来编程。接口作为不同组件之间的契约,使得接口的实现可以不断地演化。不过接口本身的演化则比较困难。当接口发生变化时,该接口的所有实现类都需要做出相应的修改。如果在新版本中对接口进行了修改,会导致早期版本的代码无法运行。Java 对于接口更新的限制过于严格。在代码演化的过程中,一般所遵循的原则是不删除或修改已有的功能,而是添加新的功能作为替代。已有代码可以继续使用原有的功能,而新的代码则可以使用新的功能。但是这种更新方式对于接口是不适用的,因为往一个接口中添加新的方法也会导致已有代码无法运行。

接口的默认方法的主要目标之一是解决接口的演化问题。当往一个接口中添加新的方法时,可以提供该方法的默认实现。对于已有的接口使用者来说,代码可以继续运行。新的代码则可以使用该方法,也可以覆写默认的实现。

考虑下面的一个简单的进行货币转换的接口。该接口的实现方式可能是调用第三方提供的服务来完成实际的转换操作。

复制代码
public interface CurrencyConverter {
BigDecimal convert(Currency from, Currency to, BigDecimal amount);
}

该接口在开发出来之后,在应用中得到了使用。在后续的版本更新中,第三方服务提供了新的批量处理的功能,允许在一次请求中同时转换多个数值。最直接的做法是在原有的接口中添加一个新的方法来支持批量处理,不过这样会造成已有的代码无法运行。而默认方法则可以很好的解决这个问题。使用默认方法的新接口如下所示。

复制代码
public interface CurrencyConverter {
BigDecimal convert(Currency from, Currency to, BigDecimal amount);
default List<bigdecimal></bigdecimal> convert(Currency from, Currency to, List<bigdecimal></bigdecimal> amounts) {
List<bigdecimal></bigdecimal> result = new ArrayList<bigdecimal></bigdecimal>();
for (BigDecimal amount : amounts) {
result.add(convert(from, to, amount));
}
return result;
}
}

新添加的方法使用 default 关键词来修饰,并可以有自己的方法体。

默认方法的另外一个作用是实现行为的多继承。Java 语言只允许类之间的单继承关系,但是一个类可以实现多个接口。在默认方法引入之后,接口中不仅可以包含变量和方法声明,还可以包含方法体,也就是行为。通过实现多个接口,一个 Java 类实际上可以获得来自不同接口的行为。这种功能类似于 JavaScript 等其他语言中可见的“混入类”( mixin )。实际上,Java 中一直存在“常量接口( Constant Interface )”的用法。常量接口中只包含常量的声明。通过实现这样的接口,就可以直接引用这些常量。通过默认方法,可以创建出类似的帮助接口,即接口中包含的都是通过默认方法实现的帮助方法。比如创建一个 StringUtils 接口包含各种与字符串操作相关的默认方法。通过继承该接口就可以直接使用这些方法。

Java SE 8 标准库已经使用默认方法来对集合类中的接口进行更新。比如 java.util.Collection 接口中新增的默认方法 removeIf 可以删除集合中满足某些条件的元素。还有 java.lang.Iterable 接口中新增的默认方法 forEach 可以遍历集合中的元素,并执行一些操作。这些新增的默认方法大多使用了 java.util.function 包中的函数式接口,因此可以使用 lambda 表达式来非常简洁的进行操作。

Lambda 表达式是 Java SE 8 在提高开发人员生产效率上的一个重大改进。通过语法上的改进,可以减少开发人员需要编写和维护的代码数量。

2013 年 8 月 15 日 05:1028582

评论

发布
暂无评论
发现更多内容

开发人员应当避免的代价高昂的职业错误

小隐乐乐

职业规划 职业素养 架构师

Uniapp使用GoEasy实现websocket实时通讯

GoEasy消息推送

uni-app websocket 即时通讯

最右JS2Flutter框架——开篇(一)

刘剑

flutter 前端 探索与实践

打造Redis分布式环境下的银弹?我觉得Redisson比Redlock更胜一筹

码农月半

Java redis redis高可用 Redis项目

为你的 SpringBoot 服务生成或推送各平台的部署包

华宇法律科技

Docker k8s springboot

公司制的黄昏:区块链重构商业世界

CECBC区块链专委会

区块链思维 裂变 契约 激励

架构师训练营 - 第 5 周命题作业

红了哟

架构师训练营 - 第五周命题作业

牛牛

极客大学架构师训练营 命题作业 一致性Hash算法

第五周总结

武鹏

架构师训练营 - 第五周 - 作业

韩挺

产业区块链发展迎来爆发期

CECBC区块链专委会

产业区块链 系统稳定性 应用安全性 信任的机器

区块链技术打通医疗应用场景

CECBC区块链专委会

行业资讯 生产 区块链技术 生活服务

Week 05- 作业一:一致性 hash 算法

dean

极客大学架构师训练营

架构师训练营学习总结——缓存与消息队列【第五周】

王海

极客大学架构师训练营

第五周作业-一致性hash算法实现

吴建中

极客大学架构师训练营

程序员是这样解读《隐秘的角落》

陈东泽 EuryChen

学习 程序员 隐秘的角落

用一致性Hash算法的实现负载均衡(Kotlin)

Acker飏

极客大学架构师训练营 一致性Hash算法

Week5 一致性hash算法

TiK

你都如何回忆我,带着笑或是很沉默

小天同学

回忆 高考 青春

阿里内推面试,挂在了一道简单的问题上…

小新

Java 阿里巴巴 程序员 架构 面试

架构师训练营 - 第 5 周学习总结

红了哟

这份架构PDF如何得到百度、洋码头、饿了么CTO等大咖联袂推荐?

小新

Java 架构 面试 队列

码农必备SQL高性能优化指南!35+条优化建议立马get

码哥小胖

MySQL SQL语法 sql查询 sql

架构师训练营 - 第五周 - 学习总结

韩挺

搞懂Spring事务失效的8大原因,轻轻松松面试过关

码哥小胖

Java spring Spring Boot

老龄化时代的人机共生:京东数科以AI机器人推动产业增长

脑极体

week5

Geek_2e7dd7

week5 学习总结

Geek_2e7dd7

Week5 学习总结

wyzwlj

极客大学架构师训练营

week5-总结 技术选型

a晖

首次揭秘!​春晚活动下快手实时链路保障实践

Apache Flink

Apache flink 架构 实时计算

NLP领域的2020年大事记及2021展望

NLP领域的2020年大事记及2021展望

Java SE 8: Lambda表达式-InfoQ