Java Lambda 表达式(一):入门
为什么要使用 Lambda 表达式
在 Java 8 以前,假设我们要实现一个不论是多么简单的接口(例如只包含一个方法的接口),最简单的做法也是要使用匿名类,但匿名类的语法可能看起来不怎么实用且不怎么清楚。
譬如,对于 GUI 应用程序中的事件处理程序,我们在 Java 8 以前的常规做法是向方法传递匿名类。比如,以下的 HelloWorld
GUI 应用程序源码:
package com.wuxianjiezh.test;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class HelloWorld extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World!");
Button btn = new Button();
btn.setText("Say 'Hello World'");
// 在 Java 8 以前,我们最简洁的写法可能就是匿名类
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Hello World!");
}
});
StackPane root = new StackPane();
root.getChildren().add(btn);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
}
}
虽然上面代码的匿名类通常比局部类更简洁,但对于只有一个方法的类,即使是匿名类也似乎有点过分和繁琐。
稍安毋躁,我们先在 Java 8 中查看一下 EventHandler
接口的源码:
package javafx.event;
import java.util.EventListener;
@FunctionalInterface
public interface EventHandler<T extends Event> extends EventListener {
void handle(T event);
}
从上面的源码中,我们发现 EventHandler
接口只有一个抽象方法 void handle(T event);
。细心的童鞋可能还发现了一个新奇的东西——@FunctionalInterface
注解,嗯!这个注解是什么意思呢?可不可以不写呢?不要急,后面我们会再讲到的。
对于 EventHandler
这样一个只有一个抽象方法的接口,我们称之为单一抽象方法接口(SAM,Single Abstract Method Interface)。而对于这样的接口,我们就可以使用 Java 8 引入的 Lambda 表达式来简化代码。
对于上述代码,我们可以使用 Lambda 表达代替匿名类表达式:
package com.wuxianjiezh.test;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class HelloWorld extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Hello World!");
Button btn = new Button();
btn.setText("Say 'Hello World'");
// 从 Java 8 开始,我们可以使用 Lambda 表达式
btn.setOnAction(event -> System.out.println("Hello World!"));
StackPane root = new StackPane();
root.getChildren().add(btn);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
}
}
使用 Lambda 表达式的前置条件
Lambda 表达式并不是可以随意使用的,它有且仅有一个前置条件:Lambda 表达式只能用于实现函数接口(functional interface,函数式接口,功能接口)。
函数接口和我们上面提到的单一抽象方法接口是完全相同的,只是叫法不同而已。
函数接口有以下几个特点:
- 仅包含一个抽象方法的任何接口
- 但函数接口可以包含一个或多个默认方法或静态方法
@FunctionalInterface
是 Java 8 中新加入的注解,它用于指示接口是函数接口,如果不是,则会在编译期间就产生错误。这个注解不是必须的,可写可不写
Lambda 表达式的理想用例
现在,我们来模拟一个业务操作。假设某书单的图书由以下 Book
类表示:
package com.wuxianjiezh.test;
import java.util.ArrayList;
import java.util.List;
/**
* 图书实体类。
*
* @author 吴仙杰
*/
public class Book {
/**
* 书名。
*/
private String name;
/**
* 作者。
*/
private String author;
/**
* 出版社。
*/
private String publisher;
/**
* 定价。
*/
private float cny;
public Book(String name, String author, String publisher, float cny) {
this.name = name;
this.author = author;
this.publisher = publisher;
this.cny = cny;
}
/**
* 打印图书信息。
*/
public void printBook() {
System.out.println("书名:" + this.name +
"\t出版社:" + this.publisher +
"\t定价:" + this.cny + "元");
}
/**
* 创建书单。
*
* @return 书籍列表
*/
public static List<Book> createBookList() {
return new ArrayList<Book>() {{
add(new Book("Python编程 : 从入门到实践", "[美] 埃里克·马瑟斯", "人民邮电出版社", 89f));
add(new Book("代码大全(第2版)", "[美] 史蒂夫·迈克康奈尔", "电子工业出版社", 128f));
add(new Book("Java编程思想 (第4版)", "[美] Bruce Eckel", "机械工业出版社", 108f));
add(new Book("编程珠玑 : 第2版", "[美] Jon Bentley", "人民邮电出版社", 39f));
add(new Book("JavaScript高级程序设计(第3版)", "[美] 尼古拉斯·泽卡斯", "人民邮电出版社", 99f));
add(new Book("重构(第2版)全彩精装版 : 改善既有代码的设计", "Martin Fowler", "人民邮电出版社", 168f));
}};
}
public String getName() {
return name;
}
public String getAuthor() {
return author;
}
public String getPublisher() {
return publisher;
}
public float getCny() {
return cny;
}
}
其中 Book#createBookList
代表我们所创建的书单。
现在我们要编写一个测试类 BookListTest
,要从书单中筛选出由“人民邮电出版社”出版,并且价格在 100 元以内(包含 100 元)的图书。
在局部类中指定搜索条件代码
package com.wuxianjiezh.test;
import java.util.List;
import java.util.Objects;
/**
* 书单测试类。
*
* <p>假设我们的目的是为了筛选出书单中由“人民邮电出版社”出版,
* 并且价格在 100 元以内(包含 100 元)的图书。
*
* @author 吴仙杰
*/
public class BookListTest {
private static final String PUBLISHER = "人民邮电出版社";
private static final float MAX_AMOUNT = 100f;
// 用于指定搜索条件的接口
interface CheckBook {
boolean test(Book book);
}
/**
* 打印出符合条件的图书信息。
*
* @param bookList 书单
* @param filter 过滤器函数
*/
public static void printBooks(List<Book> bookList, BookListTest.CheckBook filter) {
for (Book book : bookList) {
if (filter.test(book)) {
book.printBook();
}
}
}
public static void main(String[] args) {
// 创建书单
List<Book> bookList = Book.createBookList();
// 在局部类中指定搜索条件代码
class CheckMyBook implements CheckBook {
@Override
public boolean test(Book book) {
return Objects.equals(book.getPublisher(), PUBLISHER)
&& book.getCny() <= MAX_AMOUNT;
}
}
// 打印出符合条件的图书信息
printBooks(bookList, new CheckMyBook());
}
}
在匿名类中指定搜索条件代码
package com.wuxianjiezh.test;
import java.util.List;
import java.util.Objects;
/**
* 书单测试类。
*
* <p>假设我们的目的是为了筛选出书单中由“人民邮电出版社”出版,
* 并且价格在 100 元以内(包含 100 元)的图书。
*
* @author 吴仙杰
*/
public class BookListTest {
private static final String PUBLISHER = "人民邮电出版社";
private static final float MAX_AMOUNT = 100f;
// 用于指定搜索条件的接口
interface CheckBook {
boolean test(Book book);
}
/**
* 打印出符合条件的图书信息。
*
* @param bookList 书单
* @param filter 过滤器函数
*/
public static void printBooks(List<Book> bookList, BookListTest.CheckBook filter) {
for (Book book : bookList) {
if (filter.test(book)) {
book.printBook();
}
}
}
public static void main(String[] args) {
// 创建书单
List<Book> bookList = Book.createBookList();
// 在匿名类中指定搜索条件代码
// 打印出符合条件的图书信息
printBooks(bookList, new CheckBook() {
@Override
public boolean test(Book book) {
return Objects.equals(book.getPublisher(), PUBLISHER)
&& book.getCny() <= MAX_AMOUNT;
}
});
}
}
使用 Lambda 表达式指定搜索条件代码
现在,我们考虑一下上面的 CheckBook
接口:
interface CheckBook {
boolean test(Book book);
}
很明显,我们的 CheckBook
接口是一个函数接口,故我们这里就可以使用 Lambda 表达式来简化代码:
package com.wuxianjiezh.test;
import java.util.List;
import java.util.Objects;
/**
* 书单测试类。
*
* <p>假设我们的目的是为了筛选出书单中由“人民邮电出版社”出版,
* 并且价格在 100 元以内(包含 100 元)的图书。
*
* @author 吴仙杰
*/
public class BookListTest {
private static final String PUBLISHER = "人民邮电出版社";
private static final float MAX_AMOUNT = 100f;
// 用于指定搜索条件的接口
interface CheckBook {
boolean test(Book book);
}
/**
* 打印出符合条件的图书信息。
*
* @param bookList 书单
* @param filter 过滤器函数
*/
public static void printBooks(List<Book> bookList, BookListTest.CheckBook filter) {
for (Book book : bookList) {
if (filter.test(book)) {
book.printBook();
}
}
}
public static void main(String[] args) {
// 创建书单
List<Book> bookList = Book.createBookList();
// 使用 Lambda 表达式指定搜索条件代码
// 打印出符合条件的图书信息
printBooks(bookList,
(Book book) -> Objects.equals(book.getPublisher(), PUBLISHER)
&& book.getCny() <= MAX_AMOUNT);
}
}
将标准函数接口(Predicate)与 Lambda 表达式一起使用
我们再考虑一下上面的 CheckBook
接口:
interface CheckBook {
boolean test(Book book);
}
CheckBook
接口是一个非常简单的函数接口,只包含一个接受一个参数并返回一个 boolean
值的抽象方法。由于这个接口实在太简单,我们并不想在自己的程序中定义。那是否有办法不用自己定义这样一个如此简单的函数接口呢?
当然有办法!贴心的 JDK 已经帮助我们定义好了的几个标准的函数接口,这些接口可以在 java.util.function
包中找到。
比如,我们可以使用 Predicate<T>
接口代替 CheckBook
接口。 Predicate<T>
接口是一个泛型接口,它只包含一个根据一个参数并返回 boolean
值的抽象方法:
package java.util.function;
import java.util.Objects;
/**
* Represents a predicate (boolean-valued function) of one argument.
*
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #test(Object)}.
*
* @param <T> the type of the input to the predicate
*
* @since 1.8
*/
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
从而,我们可以通过 Predicate<T>
接口代替 CheckBook
接口来简化代码:
package com.wuxianjiezh.test;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
/**
* 书单测试类。
*
* <p>假设我们的目的是为了筛选出书单中由“人民邮电出版社”出版,
* 并且价格在 100 元以内(包含 100 元)的图书。
*
* @author 吴仙杰
*/
public class BookListTest {
private static final String PUBLISHER = "人民邮电出版社";
private static final float MAX_AMOUNT = 100f;
/**
* 打印出符合条件的图书信息。
*
* @param bookList 书单
* @param filter 过滤器函数
*/
public static void printBooks(List<Book> bookList, Predicate<Book> filter) {
for (Book book : bookList) {
if (filter.test(book)) {
book.printBook();
}
}
}
public static void main(String[] args) {
// 创建书单
List<Book> bookList = Book.createBookList();
// 使用 Lambda 表达式指定搜索条件代码
// 打印出符合条件的图书信息
printBooks(bookList,
// Lambda 表达式可以省略参数的数据类型;如果只有一个参数,则还可以省略括号
book -> Objects.equals(book.getPublisher(), PUBLISHER)
&& book.getCny() <= MAX_AMOUNT);
}
}
将标准函数接口(Consumer)与 Lambda 表达式一起使用
我们再考虑一下上面的 printBooks
方法,是否在哪里还可以再使用 Lambda 表达式呢?
public static void printBooks(List<Book> bookList, Predicate<Book> filter) {
for (Book book : bookList) {
if (filter.test(book)) {
book.printBook();
}
}
}
这个方法检查 List
参数 bookList
中包含的每个 Book
实例是否满足 Predicate
参数 filter
中指定的条件。如果 Book
实例满足 filter
指定的条件,则在 Book
实例上调用 printBook
方法。本身 printBooks
方法并不返回任何值。
对 printBooks
分析得差不多了,那么我们在 JDK 的 java.util.function
包中找一下,是否存在一个根据一个参数但没有返回值的函数接口呢?
功夫不负有心人,我们发现了 Consumer<T>
标准函数接口,它只包含一个根据一个参数但没返回值的抽象方法:
package java.util.function;
import java.util.Objects;
/**
* Represents an operation that accepts a single input argument and returns no
* result. Unlike most other functional interfaces, {@code Consumer} is expected
* to operate via side-effects.
*
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #accept(Object)}.
*
* @param <T> the type of the input to the operation
*
* @since 1.8
*/
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
现在我们就可以再次简化代码为:
package com.wuxianjiezh.test;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* 书单测试类。
*
* <p>假设我们的目的是为了筛选出书单中由“人民邮电出版社”出版,
* 并且价格在 100 元以内(包含 100 元)的图书。
*
* @author 吴仙杰
*/
public class BookListTest {
private static final String PUBLISHER = "人民邮电出版社";
private static final float MAX_AMOUNT = 100f;
/**
* 打印出符合条件的图书信息。
*
* @param bookList 书单
* @param filter 过滤器函数
* @param consumer 消费者函数
*/
public static void printBooks(List<Book> bookList,
Predicate<Book> filter,
Consumer<Book> consumer) {
for (Book book : bookList) {
if (filter.test(book)) {
consumer.accept(book);
}
}
}
public static void main(String[] args) {
// 创建书单
List<Book> bookList = Book.createBookList();
// 使用 Lambda 表达式指定搜索条件代码
// 打印出符合条件的图书信息
printBooks(bookList,
book -> Objects.equals(book.getPublisher(), PUBLISHER)
&& book.getCny() <= MAX_AMOUNT,
book -> book.printBook());
}
}
将标准函数接口(Function)与 Lambda 表达式一起使用
JDK 还提供了一个常用的标准函数接口 Function<T,R>
,它只包含一个根据一个参数并返回结果的抽象方法:
package java.util.function;
import java.util.Objects;
/**
* Represents a function that accepts one argument and produces a result.
*
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #apply(Object)}.
*
* @param <T> the type of the input to the function
* @param <R> the type of the result of the function
*
* @since 1.8
*/
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
我们可以借助这个标准函数接口,实现对数据的额外处理。假设我们现在打算打印出符合筛选条件的图书作者,则可通过以下代码实现:
package com.wuxianjiezh.test;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* 书单测试类。
*
* <p>假设我们的目的是为了筛选出书单中由“人民邮电出版社”出版,
* 并且价格在 100 元以内(包含 100 元)的图书作者。
*
* @author 吴仙杰
*/
public class BookListTest {
private static final String PUBLISHER = "人民邮电出版社";
private static final float MAX_AMOUNT = 100f;
/**
* 打印出符合条件的图书信息。
*
* @param bookList 书单
* @param filter 过滤器函数
* @param mapper 映射器函数
* @param consumer 消费者函数
*/
public static void printBooks(List<Book> bookList,
Predicate<Book> filter,
Function<Book, String> mapper,
Consumer<String> consumer) {
for (Book book : bookList) {
if (filter.test(book)) {
String data = mapper.apply(book);
consumer.accept(data);
}
}
}
public static void main(String[] args) {
// 创建书单
List<Book> bookList = Book.createBookList();
// 使用 Lambda 表达式指定搜索条件代码
// 打印出符合条件的图书作者
printBooks(bookList,
book -> Objects.equals(book.getPublisher(), PUBLISHER)
&& book.getCny() <= MAX_AMOUNT,
book -> book.getAuthor(),
author -> System.out.println(author));
}
}
更广泛地使用泛型
在 Java 中,我们可以使用泛型使得类或方法变得更加通用:
package com.wuxianjiezh.test;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* 书单测试类。
*
* <p>假设我们的目的是为了筛选出书单中由“人民邮电出版社”出版,
* 并且价格在 100 元以内(包含 100 元)的图书作者。
*
* @author 吴仙杰
*/
public class BookListTest {
private static final String PUBLISHER = "人民邮电出版社";
private static final float MAX_AMOUNT = 100f;
/**
* 打印出符合条件的数据信息。
*
* @param source 源数据列表
* @param filter 过滤器函数
* @param mapper 映射器函数
* @param consumer 消费者函数
*/
public static <X, Y> void processElements(List<X> source,
Predicate<X> filter,
Function<X, Y> mapper,
Consumer<Y> consumer) {
for (X item : source) {
if (filter.test(item)) {
Y data = mapper.apply(item);
consumer.accept(data);
}
}
}
public static void main(String[] args) {
// 创建书单
List<Book> bookList = Book.createBookList();
// 使用 Lambda 表达式指定搜索条件代码
// 打印出符合条件的图书作者
processElements(bookList,
book -> Objects.equals(book.getPublisher(), PUBLISHER)
&& book.getCny() <= MAX_AMOUNT,
book -> book.getAuthor(),
author -> System.out.println(author));
}
}
使用聚合操作
聚合操作(aggregate operation)通常接受 Lambda 表达式作为参数,并且可以按我们的需要自定义它们的行为方式。
下面我们使用聚合操作更进一步简化上面的代码:
package com.wuxianjiezh.test;
import java.util.List;
import java.util.Objects;
/**
* 书单测试类。
*
* <p>假设我们的目的是为了筛选出书单中由“人民邮电出版社”出版,
* 并且价格在 100 元以内(包含 100 元)的图书作者。
*
* @author 吴仙杰
*/
public class BookListTest {
private static final String PUBLISHER = "人民邮电出版社";
private static final float MAX_AMOUNT = 100f;
public static void main(String[] args) {
// 创建书单
List<Book> bookList = Book.createBookList();
// 使用 Lambda 表达式指定搜索条件代码
// 打印出符合条件的图书作者
bookList
// 获取源的流
.stream()
// 过滤出与 `Predicate` 对象匹配的对象
.filter(book -> Objects.equals(book.getPublisher(), PUBLISHER)
&& book.getCny() <= MAX_AMOUNT)
// 将对象映射到由 `Function` 对象指定的另一个值
.map(book -> book.getAuthor())
// 执行由 `Consumer` 对象所指定的动作
.forEach(author -> System.out.println(author));
}
}
像 filter
、map
和 forEach
这些操作被称为聚合操作。
关于聚合操作我们必须要了解以下几点:
- 聚合操作处理的是从流(stream)中来的元素,而非直接操作来自集合中的元素
- 流是一系列元素;与集合不同,它不是存储元素的数据结构
- 流通过管道(pipeline)传递来自源(例如集合)的值
- 管道是一系列流操作,例如:
filter
→map
→forEach
Lambda 表达式的语法
Lambda 表达式由以下几部分组成:
- 参数部分
- 参数类型是可选的,若忽略参数类型,则编译器会自动从上下文中推断参数的类型。例如:
(int a)
可简写为(a)
- 参数可以有零个、一个或多个:
- 零个:空的小括号用于表示一组空的参数。例如:
() -> 1
- 一个:如果不显式指明类型,则不必使用小括号。例如:
a -> return a*a
- 多个:参数用小括号括起来,并以逗号分隔。例如:
(a, b) -> a+b
- 零个:空的小括号用于表示一组空的参数。例如:
- 参数类型是可选的,若忽略参数类型,则编译器会自动从上下文中推断参数的类型。例如:
- 箭头标记(
->
) - 表达式体,由单个表达式或语句块组成
- 单个表达式:Java 运行时将计算该表达式,然后返回其值。可以用小括号(
()
)括起来 - 语句块:若有多条语句则必须用大括号(
{}
)括起来。否则,可以省略大括号
- 单个表达式:Java 运行时将计算该表达式,然后返回其值。可以用小括号(
以下表达式体中的单个表达式:
book -> Objects.equals(book.getPublisher(), PUBLISHER)
&& book.getCny() <= MAX_AMOUNT
与以下表达式体中的语句块是等效的:
book -> {
return Objects.equals(book.getPublisher(), PUBLISHER)
&& book.getCny() <= MAX_AMOUNT;
}
Lambda 表达式看起来很像方法声明。我们可以将 Lambda 表达式视为匿名方法——没有名称的方法。