Java Lambda 表达式的各种形态和使用场景,看这篇就够了

·  阅读 7910

本文为掘金社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

Lambda 表达式是 Java 8 中添加的功能。引入 Lambda 表达式的主要目的是为了让 Java 支持函数式编程。 Lambda 表达式是一个可以在不属于任何类的情况下创建的函数,并且可以像对象一样被传递和执行。

Java lambda 表达式用于实现简单的单方法接口,与 Java Streams API 配合进行函数式编程

在前几篇关于 List、Set 和 Map 的文章中,我们已经看到了这几个 Java 容器很多操作都是通过 Stream 完成的,比如过滤出对象 List 中符合条件的子集时,会使用类似下面的 Stream 操作。

List<A> list = aList.filter(a -> a.getId() > 10).collect(Colletors.toList);
复制代码

其中filter方法里用到的a -> a.getId() > 10就是一个 Lambda 表达式,前面几篇文章我们主要策略讲集合框架里那几个常用 Java 容器的使用,对用到 Lambda 的地方知识简单的说了一下,如果你对各种 Stream 操作有疑问,可以先把本篇 Lambda 相关的内容学完,接下来再仔细梳理 Stream 时就会好理解很多了。

本文内容大纲如下: Java Lambda 表达式大纲

Lambda 表达式和函数式接口

上面说了 lambda 表达式便于实现只拥有单一方法的接口,同样在 Java 里匿名类也用于快速实现接口,只不过 lambda 相较于匿名类更方便些,在书写的时候连创建类的步骤也免去了,更适合用在函数式编程。

举个例子来说,函数式编程经常用在实现事件 Listener 的时候 。 在 Java 中的事件侦听器通常被定义为具有单个方法的 Java 接口。下面是一个 Listener 接口示例:

public interface StateChangeListener {

    public void onStateChange(State oldState, State newState);

}
复制代码

上面这个 Java 接口定义了一个只要被监听对象的状态发生变化,就会调用的 onStateChange 方法(这里不用管监听的是什么,举例而已)。 在 Java 8 版本以前,监听事件变更的程序必须实现此接口才能侦听状态更改。

比如说,有一个名为 StateOwner 的类,它可以注册状态的事件侦听器。

public class StateOwner {

    public void addStateListener(StateChangeListener listener) { ... }

}
复制代码

我们可以使用匿名类实现 StateChangeListener 接口,然后为 StateOwner 实例添加侦听器。

StateOwner stateOwner = new StateOwner();

stateOwner.addStateListener(new StateChangeListener() {

    public void onStateChange(State oldState, State newState) {
        // do something with the old and new state.
        System.out.println("State changed")
    }
});
复制代码

在 Java 8 引入Lambda 表达式后,我们可以用 Lambda 表达式实现 StateChangeListener 接口会更加方便。现在,把上面例子接口的匿名类实现改为 Lambda 实现,程序会变成这样:

StateOwner stateOwner = new StateOwner();

stateOwner.addStateListener(
    (oldState, newState) -> System.out.println("State changed")
);
复制代码

在这里,我们使用的 Lambda 表达式是:

(oldState, newState) -> System.out.println("State changed")
复制代码

这个 lambda 表达式与 StateChangeListener 接口的 onStateChange() 方法的参数列表和返回值类型相匹配。如果一个 lambda 表达式匹配单方法接口中方法的参数列表和返回值(比如本例中的 StateChangeListener 接口的 onStateChange 方法),则 lambda 表达式将转换为拥有相同方法签名的接口实现。 这句话听着有点绕,下面详细解释一下 Lambda 表达式和接口匹配的详细规则。

匹配Lambda 与接口的规则

上面例子里使用的 StateChangeListener 接口有一个特点,其只有一个未实现的抽象方法,在 Java 里这样的接口也叫做函数式接口 (Functional Interface)。将 Java lambda 表达式与接口匹配需要满足一下三个规则:

  • 接口是否只有一个抽象(未实现)方法,即是一个函数式接口?
  • lambda 表达式的参数是否与抽象方法的参数匹配?
  • lambda 表达式的返回类型是否与单个方法的返回类型匹配?

如果能满足这三个条件,那么给定的 lambda 表达式就能与接口成功匹配类型。

函数式接口

只有一个抽象方法的接口被称为函数是式接口,从 Java 8 开始,Java 接口中可以包含默认方法和静态方法。默认方法和静态方法都有直接在接口声明中定义的实现。这意味着,Java lambda 表达式可以实现拥有多个方法的接口——只要接口中只有一个未实现的抽象方法就行。

所以在文章一开头我说lambda 用于实现单方法接口,是为了让大家更好的理解,真实的情况是只要接口中只存在一个抽象方法,那么这个接口就能用 lambda 实现。

换句话说,即使接口包含默认方法和静态方法,只要接口只包含一个未实现的抽象方法,它就是函数式接口。比如下面这个接口:

import java.io.IOException;
import java.io.OutputStream;

public interface MyInterface {

    void printIt(String text);

    default public void printUtf8To(String text, OutputStream outputStream){
        try {
            outputStream.write(text.getBytes("UTF-8"));
        } catch (IOException e) {
            throw new RuntimeException("Error writing String as UTF-8 to OutputStream", e);
        }
    }

    static void printItToSystemOut(String text){
        System.out.println(text);
    }
}
复制代码

即使这个接口包含 3 个方法,它也可以通过 lambda 表达式实现,因为接口中只有一个抽象方法 printIt没有被实现。

MyInterface myInterface = (String text) -> {
    System.out.print(text);
};
复制代码

Lambda VS 匿名类

尽管 lambda 表达式和匿名类看起来差不多,但还是有一些值得注意的差异。 主要区别在于,匿名类可以有自己的内部状态--即成员变量,而 lambda 表达式则不能。

public interface MyEventConsumer {

    public void consume(Object event);

}
复制代码

比如上面这个接口,通过匿名类实现

MyEventConsumer consumer = new MyEventConsumer() {
    public void consume(Object event){
        System.out.println(event.toString() + " consumed");
    }
};
复制代码

MyEventConsumer 接口的匿名类可以有自己的内部状态。

MyEventConsumer myEventConsumer = new MyEventConsumer() {
    private int eventCount = 0;
    public void consume(Object event) {
        System.out.println(event.toString() + " consumed " + this.eventCount++ + " times.");
    }
};
复制代码

我们给匿名类,加了一个名为 eventCount 的整型成员变量,用来记录匿名类 consume 方法被执行的次数。Lambda 表达式则不能像匿名类一样添加成员变量,所以也成 Lambda 表达式是无状态的。

推断 Lamdba 的接口类型

使用匿名类实现函数式接口的时候,必须在 new 关键字后指明实现的是哪个接口。比如上面使用过的匿名类例子

stateOwner.addStateListener(new StateChangeListener() {

    public void onStateChange(State oldState, State newState) {
        // do something with the old and new state.
    }
});
复制代码

但是 lambda 表达式,通常可以从上下文中推断出类型。例如,可以从 addStateListener() 方法声明中参数的类型 StateChangeListener 推断出来,Lambda 表达式要实现的是 StateChangeListener 接口。

stateOwner.addStateListener(
    (oldState, newState) -> System.out.println("State changed")
);
复制代码

通常 lambda 表达式参数的类型也可以推断出来。在上面的示例中,编译器可以从StateChangeListener 接口的抽象方法 onStateChange() 的方法声明中推断出参数 oldState 和 newState 的类型。

Lambda 的参数形式

由于 lambda 表达式实际上只是个方法,因此 lambda 表达式可以像方法一样接受参数。Lambda 表达式参数根据参数数量以及是否需要添加类型会有下面几个形式。

如果表达式的方法不带参数,那么可以像下面这样编写 Lambda 表达式:

() -> System.out.println("Zero parameter lambda");
复制代码

如果表达式的方法接受一个参数,则可以像下面这样编写 Lambda 表达式:

(param) -> System.out.println("One parameter: " + param);
复制代码

当 Lambda 表达式只接收单个参数时,参数列表外的小括号也可以省略掉。

param -> System.out.println("One parameter: " + param);
复制代码

当 Lambda 表达式接收多个参数时,参数列表的括号就没法省略了。

如果编译器无法从 Lambda 匹配的函数式接口的方法声明推断出参数类型(出现这种情况时,编译器会提示),则有时可能需要为 Lambda 表达式的参数指定类型。

(Car car) -> System.out.println("The car is: " + car.getName());
复制代码

Lambda 的方法体

lambda 表达式的方法的方法体,在 Lambda 声明中的 -> 右侧指定:

(oldState, newState) -> System.out.println("State changed")
复制代码

如果 Lambda 表达式的方法体需要由多行组成,则需要把多行代码写在用{ }括起来的代码块内。

(oldState, newState) -> {
    System.out.println("Old state: " + oldState);
    System.out.println("New state: " + newState);
}
复制代码

Lamdba 表达式的返回值

可以从 Lambda 表达式返回值,就像从方法中返回值一样。只需在 Lambda 的方法体中添加一个 return 语句即可:

(param) -> {
    System.out.println("param: " + param);
    return "return value";
}
复制代码

如果 Lambda 表达式所做的只是计算返回值并返回它,我们甚至可以省略 return 语句。

(a1, a2) -> { return a1 > a2; }
// 上面的可以简写成,不需要return 语句的
(a1, a2) -> { a1 > a2; }
复制代码

Lambda 表达式本质上是一个对象,跟其他任何我们使用过的对象一样, 我们可以将 Lambda 表达式赋值给变量并进行传递和使用。

public interface MyComparator {

    public boolean compare(int a1, int a2);

}

---
    
MyComparator myComparator = (a1, a2) -> a1 > a2;

boolean result = myComparator.compare(2, 5);
复制代码

上面的这个例子展示 Lambda 表达式的定义,以及如何将 Lambda 表达式赋值给给变量,最后通过调用它实现的接口方法来调用 Lambda 表达式。

外部变量在 Lambda 内的可见性

在某些情况下,Lambda 表达式能够访问在 Lambda 函数体之外声明的变量。 Lambda 可以访问以下类型的变量:

  • 局部变量
  • 实例变量
  • 静态变量

**Lambda 内访问局部变量,**Lambda 可以访问在 Lambda 方法体之外声明的局部变量的值

public interface MyFactory {
    public String create(char[] chars);
}

String myString = "Test";

MyFactory myFactory = (chars) -> {
    return myString + ":" + new String(chars);
};
复制代码

Lambda 访问实例变量,Lambda 表达式还可以访问创建了 Lambda 的对象中的实例变量。

public class EventConsumerImpl {

    private String name = "MyConsumer";

    public void attach(MyEventProducer eventProducer){
        eventProducer.listen(e -> {
            System.out.println(this.name);
        });
    }
}
复制代码

这里实际上也是 Lambda 与匿名类的差别之一。匿名类因为可以有自己的实例变量,这些变量通过 this 引用来引用。但是,Lambda 不能有自己的实例变量,因此 this 始终指向外面包裹 Lambda 的对象。

**Lambda 访问静态变量,**Lambda 表达式也可以访问静态变量。这也不奇怪,因为静态变量可以从 Java 应用程序中的任何地方访问,只要静态变量是公共的。

public class EventConsumerImpl {
    private static String someStaticVar = "Some text";

    public void attach(MyEventProducer eventProducer){
        eventProducer.listen(e -> {
            System.out.println(someStaticVar);
        });
    }
}
复制代码

把方法引用作为 Lambda

如过编写的 lambda 表达式所做的只是使用传递给 Lambda 的参数调用另一个方法,那么 Java里为 Lambda 实现提供了一种更简短的形式来表达方法调用。比如说,下面是一个函数式数接口:

public interface MyPrinter{
    public void print(String s);
}
复制代码

接下来我们用 Lambda 表达式实现这个 MyPrinter 接口

MyPrinter myPrinter = (s) -> { System.out.println(s); };
复制代码

因为 Lambda 的参数只有一个,方法体也只包含一行,所以可以简写成

MyPrinter myPrinter = s ->  System.out.println(s);
复制代码

又因为 Lambda 方法体内所做的只是将字符串参数转发给 System.out.println() 方法,因此我们可以将上面的 Lambda 声明替换为方法引用。

MyPrinter myPrinter = System.out::println;
复制代码

注意双冒号 :: 向 Java 的编译器指明这是一个方法的引用。引用的方法是双冒号之后的方法。而拥有引用方法的类或对象则位于双冒号之前。

我们可以引用以下类型的方法:

  • 静态方法
  • 参数对象的实例方法
  • 实例方法
  • 类的构造方法

引用类的静态方法

最容易引用的方法是静态方法,比如有这么一个函数式接口和类

public interface Finder {
    public int find(String s1, String s2);
}

public class MyClass{
    public static int doFind(String s1, String s2){
        return s1.lastIndexOf(s2);
    }
}
复制代码

如果我们创建 Lambda 去调用 MyClass 的静态方法 doFind

Finder finder = (s1, s2) -> MyClass.doFind(s1, s2);
复制代码

所以我们可以使用 Lambda 直接引用 Myclass 的 doFind 方法。

Finder finder = MyClass::doFind;
复制代码

引用参数的方法

接下来,如果我们在 Lambda 直接转发调用的方法是来自参数的方法

public interface Finder {
    public int find(String s1, String s2);
}

Finder finder = (s1, s2) -> s1.indexOf(s2);
复制代码

依然可以通过 Lambda 直接引用

Finder finder = String::indexOf;
复制代码

这个与上面完全形态的 Lambda 在功能上完全一样,不过要注意简版 Lambda 是如何引用单个方法的。 Java 编译器会尝试将引用的方法与第一个参数的类型匹配,使用第二个参数类型作为引用方法的参数。

引用实例方法

我们还也可以从 Lambda 定义中引用实例方法。首先,设想有如下接口

public interface Deserializer {
    public int deserialize(String v1);
}
复制代码

该接口表示一个能够将字符串“反序列化”为 int 的组件。现在有一个 StringConvert 类

public class StringConverter {
    public int convertToInt(String v1){
        return Integer.valueOf(v1);
    }
}
复制代码

StringConvert 类 的 convertToInt() 方法与 Deserializer 接口的 deserialize() 方法具有相同的签名。因此,我们可以创建 StringConverter 的实例并从 Lambda 表达式中引用其 convertToInt() 方法,如下所示:

StringConverter stringConverter = new StringConverter();

Deserializer des = stringConverter::convertToInt;
// 等同于 Deserializer des = (value) -> stringConverter.convertToInt(value)
复制代码

上面第二行代码创建的 Lambda 表达式引用了在第一行创建的 StringConverter 实例的 convertToInt 方法。

引用构造方法

最后如果 Lambda 的作用是调用一个类的构造方法,那么可以通过 Lambda 直接引用类的构造方法。在 Lambda 引用类构造方法的形式如下:

ClassName::new
复制代码

那么如何将构造方法用作 lambda 表达式呢,假设我们有这样一个函数式接口

public interface Factory {
    public String create(char[] val);
}
复制代码

Factory 接口的 create() 方法与 String 类中的其中一个构造方法的签名相匹配(String 类有多个重载版本的构造方法)。因此,String类的该构造方法也可以用作 Lambda 表达式。

Factory factory = String::new;
// 等同于 Factory factory (chars) -> String.new(chars);
复制代码

总结

今天这篇文章把 Lambda 表达式的知识梳理的了一遍,相信看完了这里的内容,再看到 Lambda 表达式的各种形态就不觉得迷惑了,虽然今天的文章看起来有点枯燥,不过是接下来 咱们系统学习 Stream 操作的基础,以及后面介绍 Java 中提供的几个函数式编程 interface 也会用到 Lambda 里的知识,后面的内容可以继续期待一下。

分类:
后端
收藏成功!
已添加到「」, 点击更改