跳到主要内容

Java Lambda 表达式

提示
  1. 函数式接口概念:函数式接口是只包含一个抽象方法的接口,用于定义 lambda 表达式的结构。
  2. Lambda 表达式定义:Lambda 表达式是一个匿名方法,用于实现函数式接口的抽象方法,具有简洁的语法 (参数) -> 表达式/代码块
  3. 泛型和流 API:Lambda 表达式可以用于泛型函数式接口和结合 Stream API 进行集合操作,提高代码的简洁性和效率。

Java 8 首次引入了 lambda 表达式。其主要目的是增强语言的表达能力。

但是,在深入了解 lambdas 之前,我们首先需要理解函数式接口。

什么是函数式接口?

如果一个 Java 接口只包含一个抽象方法,那么它被称为函数式接口。这唯一的一个方法指明了接口的预期用途。

例如,来自 java.lang 包的 Runnable 接口;是一个函数式接口,因为它只包含一个方法,即 run()

示例 1:在 java 中定义一个函数式接口

import java.lang.FunctionalInterface;
@FunctionalInterface
public interface MyInterface{
// 单个抽象方法
double getValue();
}

在上面的示例中,接口 MyInterface 只有一个抽象方法 getValue()。因此,它是一个函数式接口。

这里,我们使用了注解 @FunctionalInterface。这个注解强制 Java 编译器表明该接口是一个函数式接口。因此,不允许有多于一个的抽象方法。然而,这并不是强制性的。

在 Java 7 中,函数式接口被视为 单一抽象方法SAM 类型。在 Java 7 中,SAMs 通常通过匿名类来实现。

示例 2:在 java 中使用匿名类实现 SAM

public class FunctionInterfaceTest {
public static void main(String[] args) {

// 匿名类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我刚刚实现了 Runnable 函数式接口。");
}
}).start();
}
}

输出:

我刚刚实现了 Runnable 函数式接口。

这里,我们可以将匿名类传递给一个方法。这有助于用更少的代码编写程序。然而,语法仍然复杂,需要编写许多额外的代码行。

Java 8 通过进一步扩展了 SAMs 的功能。由于我们知道函数式接口只有一个方法,因此在将其作为参数传递时,没有必要定义该方法的名称。Lambda 表达式正是允许我们做到这一点。

介绍 lambda 表达式

Lambda 表达式本质上是一个匿名或未命名的方法。Lambda 表达式本身不执行。相反,它用于实现由函数式接口定义的方法。

如何在 Java 中定义 lambda 表达式?

以下是我们在 Java 中定义 lambda 表达式的方式。

(参数列表) -> lambda 体

这里使用的新操作符 (->) 被称为箭头操作符或 lambda 操作符。目前这个语法可能不太清晰。让我们通过一些示例来探索,

假设,我们有这样一个方法:

double getPiValue() {
return 3.1415;
}

我们可以使用 lambda 表达式来写这个方法:

() -> 3.1415

这里,方法没有任何参数。因此,操作符的左侧包含一个空的参数。右侧是 lambda 体,指定了 lambda 表达式的操作。在这种情况下,它返回值 3.1415。

Lambda 体的类型

在 Java 中,lambda 体有两种类型。

1. 单表达式的体

() -> System.out.println("Lambdas 很棒");

这种类型的 lambda 体被称为表达式体。

2. 由代码块组成的体。

() -> {
double pi = 3.1415;
return pi;
};

这种类型的 lambda 体被称为块体。块体允许 lambda 体包含多条语句。这些语句被包含在大括号内,并且在大括号后需要添加分号。

注意:对于块体,如果体返回值,则可以有返回语句。然而,表达式体不需要返回语句。

示例 3:Lambda 表达式

让我们编写一个 Java 程序,使用 lambda 表达式返回 Pi 的值。

如前所述,lambda 表达式不会自行执行。相反,它构成了函数式接口定义的抽象方法的实现。

因此,我们首先需要定义一个函数式接口。

import java.lang.FunctionalInterface;

// 这是函数式接口
@FunctionalInterface
interface MyInterface{

// 抽象方法
double getPiValue();
}

public class Main {

public static void main( String[] args ) {

// 声明一个对 MyInterface 的引用
MyInterface ref;

// lambda 表达式
ref = () -> 3.1415;

System.out.println("Pi 的值 = " + ref.getPiValue());
}
}

输出:

Pi 的值 = 3.1415

在上述示例中,

  • 我们创建了一个名为 MyInterface 的函数式接口。它包含一个名为 getPiValue() 的单一抽象方法
  • Main 类中,我们声明了一个对 MyInterface 的引用。注意,我们可以声明接口的引用,但我们不能实例化接口。即,
// 这将抛出错误
MyInterface ref = new myInterface();

// 这是有效的
MyInterface ref;
  • 然后我们将一个 lambda 表达式分配给引用。
ref = () -> 3.1415;
  • 最后,我们使用引用接口调用 getPiValue() 方法。当
System.out.println("Pi 的值 = " + ref.getPiValue());

带参数的 Lambda 表达式

到目前为止,我们创建的 lambda 表达式都没有参数。然而,与方法类似,lambda 表达式也可以有参数。例如,

(n) -> (n%2)==0

这里,括号内的变量 n 是传递给 lambda 表达式的参数。lambda 体接受参数并检查它是偶数还是奇数。

示例 4:使用带参数的 lambda 表达式

@FunctionalInterface
interface MyInterface {

// 抽象方法
String reverse(String n);
}

public class Main {

public static void main( String[] args ) {

// 声明对 MyInterface 的引用
// 将 lambda 表达式分配给引用
MyInterface ref = (str) -> {

String result = "";
for (int i = str.length()-1; i >= 0 ; i--)
result += str.charAt(i);
return result;
};

// 调用接口的方法
System.out.println("Lambda 反转 = " + ref.reverse("Lambda"));
}

}

输出:

Lambda 反转 = adbmaL

泛型函数式接口

到目前为止,我们使用的函数式接口只接受一种类型的值。例如,

@FunctionalInterface
interface MyInterface {
String reverseString(String n);
}

上述函数式接口只接受 String 并返回 String。然而,我们可以使函数式接口泛型化,以便接受任何数据类型。如果您对泛型不太了解,请访问 Java 泛型

示例 5:泛型函数式接口和 Lambda 表达式

// GenericInterface.java


@FunctionalInterface
interface GenericInterface<T> {

// 泛型方法
T func(T t);
}

// GenericLambda.java
public class Main {

public static void main( String[] args ) {

// 声明对 GenericInterface 的引用
// GenericInterface 操作 String 数据
// 将 lambda 表达式分配给它
GenericInterface<String> reverse = (str) -> {

String result = "";
for (int i = str.length()-1; i >= 0 ; i--)
result += str.charAt(i);
return result;
};

System.out.println("Lambda 反转 = " + reverse.func("Lambda"));

// 声明另一个对 GenericInterface 的引用
// GenericInterface 操作 Integer 数据
// 将 lambda 表达式分配给它
GenericInterface<Integer> factorial = (n) -> {

int result = 1;
for (int i = 1; i <= n; i++)
result = i * result;
return result;
};

System.out.println("5 的阶乘 = " + factorial.func(5));
}
}

输出:

Lambda 反转 = adbmaL
5 的阶乘 = 120

在上述示例中,我们创建了一个名为 GenericInterface 的泛型函数式接口。它包含一个名为 func() 的泛型方法。

在 Main 类中,

  • GenericInterface<String> reverse - 创建了对接口的引用。现在,该接口操作 String 类型的数据。
  • GenericInterface<Integer> factorial - 创建了对接口的引用。在这种情况下,接口操作 Integer 类型的数据。

Lambda 表达式和 Stream API

JDK8 新增了 java.util.stream 包,使 Java 开发人员能够执行像搜索、过滤、映射、归约或操作诸如 Lists 的集合等操作。

例如,我们有一个数据流(在我们的案例中,是一个 String 类型的 List),其中每个字符串都是国家名称和该国地点的组合。现在,我们可以处理这个数据流,仅检索来自尼泊尔的地点。

为此,我们可以通过结合 Stream API 和 Lambda 表达式在流中执行批量操作。

示例 6:演示如何将 lambdas 与 Stream API 结合使用

import java.util.ArrayList;
import java.util.List;

public class StreamMain {

// 使用 ArrayList 创建一个列表对象
static List<String> places = new ArrayList<>();

// 准备我们的数据
public static List getPlaces(){

// 向列表中添加地点和国家
places.add("Nepal, Kathmandu");
places.add("Nepal, Pokhara");
places.add("India, Delhi");
places.add("USA, New York");
places.add("Africa, Nigeria");

return places;
}

public static void main( String[] args ) {

List<String> myPlaces = getPlaces();
System.out.println("来自尼泊尔的地点:");

// 过滤出来自尼泊尔的地点
myPlaces.stream()
.filter((p) -> p.startsWith("Nepal"))
.map((p) -> p.toUpperCase())
.sorted()
.forEach((p) -> System.out.println(p));
}

}

输出:

来自尼泊尔的地点:
NEPAL, KATHMANDU
NEPAL, POKHARA

在上述示例中,请注意语句

myPlaces.stream()
.filter((p) -> p.startsWith("Nepal"))
.map((p) -> p.toUpperCase())
.sorted()
.forEach((p) -> System.out.println(p));

这里,我们使用了 Stream API 的 filter()map()forEach() 等方法。这些方法可以接受一个 lambda 表达式作为输入。

我们还可以根据上面学到的语法定义自己的表达式。这使我们能够大大减少代码行数,正如我们在上面的示例中看到的那样。