Java Lambda 表达式(一):入门

Java Lambda 表达式(一):入门

  • 为什么要使用 Lambda 表达式
  • 使用 Lambda 表达式的前置条件
  • Lambda 表达式的理想用例
    • 在局部类中指定搜索条件代码
    • 在匿名类中指定搜索条件代码
    • 使用 Lambda 表达式指定搜索条件代码
    • 将标准函数接口(Predicate)与 Lambda 表达式一起使用
    • 将标准函数接口(Consumer)与 Lambda 表达式一起使用
    • 将标准函数接口(Function)与 Lambda 表达式一起使用
    • 更广泛地使用泛型
    • 使用聚合操作
  • 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;

/**
 * 书单测试类。
 *
 * 

假设我们的目的是为了筛选出书单中由“人民邮电出版社”出版, * 并且价格在 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;

/**
 * 书单测试类。
 *
 * 

假设我们的目的是为了筛选出书单中由“人民邮电出版社”出版, * 并且价格在 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;

/**
 * 书单测试类。
 *
 * 

假设我们的目的是为了筛选出书单中由“人民邮电出版社”出版, * 并且价格在 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 接口代替 CheckBook 接口。 Predicate 接口是一个泛型接口,它只包含一个根据一个参数并返回 boolean 值的抽象方法:

package java.util.function;

import java.util.Objects;

/**
 * Represents a predicate (boolean-valued function) of one argument.
 *
 * 

This is a functional interface * whose functional method is {@link #test(Object)}. * * @param 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 接口代替 CheckBook 接口来简化代码:

package com.wuxianjiezh.test;

import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;

/**
 * 书单测试类。
 *
 * 

假设我们的目的是为了筛选出书单中由“人民邮电出版社”出版, * 并且价格在 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 标准函数接口,它只包含一个根据一个参数但没返回值的抽象方法:

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.
 *
 * 

This is a functional interface * whose functional method is {@link #accept(Object)}. * * @param 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;

/**
 * 书单测试类。
 *
 * 

假设我们的目的是为了筛选出书单中由“人民邮电出版社”出版, * 并且价格在 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 ,它只包含一个根据一个参数并返回结果的抽象方法:

package java.util.function;

import java.util.Objects;

/**
 * Represents a function that accepts one argument and produces a result.
 *
 * 

This is a functional interface * whose functional method is {@link #apply(Object)}. * * @param the type of the input to the function * @param 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;

/**
 * 书单测试类。
 *
 * 

假设我们的目的是为了筛选出书单中由“人民邮电出版社”出版, * 并且价格在 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;

/**
 * 书单测试类。
 *
 * 

假设我们的目的是为了筛选出书单中由“人民邮电出版社”出版, * 并且价格在 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;

/**
 * 书单测试类。
 *
 * 

假设我们的目的是为了筛选出书单中由“人民邮电出版社”出版, * 并且价格在 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)); } }

filtermapforEach 这些操作被称为聚合操作。

关于聚合操作我们必须要了解以下几点:

  • 聚合操作处理的是从流(stream)中来的元素,而非直接操作来自集合中的元素
  • 流是一系列元素;与集合不同,它不是存储元素的数据结构
  • 流通过管道(pipeline)传递来自源(例如集合)的值
  • 管道是一系列流操作,例如:filtermapforEach

Lambda 表达式的语法

Lambda 表达式由以下几部分组成:

  • 参数部分
    • 参数类型是可选的,若忽略参数类型,则编译器会自动从上下文中推断参数的类型。例如:(int a) 可简写为 (a)
    • 参数可以有零个、一个或多个:
      • 零个:空的小括号用于表示一组空的参数。例如:() -> 1
      • 一个:如果不显式指明类型,则不必使用小括号。例如:a -> return a*a
      • 多个:参数用小括号括起来,并以逗号分隔。例如:(a, b) -> a+b
  • 箭头标记(->
  • 表达式体,由单个表达式或语句块组成
    • 单个表达式:Java 运行时将计算该表达式,然后返回其值。可以用小括号(())括起来
    • 语句块:若有多条语句则必须用大括号({})括起来。否则,可以省略大括号

以下表达式体中的单个表达式:

book -> Objects.equals(book.getPublisher(), PUBLISHER)
&& book.getCny() <= MAX_AMOUNT

与以下表达式体中的语句块是等效的:

book -> {
    return Objects.equals(book.getPublisher(), PUBLISHER)
    && book.getCny() <= MAX_AMOUNT;
}

Lambda 表达式看起来很像方法声明。我们可以将 Lambda 表达式视为匿名方法——没有名称的方法。

你可能感兴趣的:(Java)