JDK1.8新特性(三):Lambda表达式

概述

JDK8 为我们带来了 Lambda 表达式和函数式接口,这一点在前文介绍 Stream 和 Collectors 的时候已有提及。通过使用这些特性,我们可以更简洁的创建匿名内部类,也可以将方法作为参数直接传入方法中调用。本文将就这两点简要的总结一下 Lambda 的使用。

一、函数式接口

我们知道,java 中允许将接口作为方法的参数类型,但是我们只能传入其实现类。实际开发中,有些接口的仅有少数的方法,并且往往其实现类只在特定的地方使用,为此专门去创建一个新的实现类其实是有点繁琐的,为此 JDK8 引入了函数式接口。

函数式接口有且仅有一个抽象方法,抽象方法允许有一个默认实现(实际上接口的默认实现也是 JDK8 的新特性)。当我们调用方法时,可以直接通过 Lambda 表达式直接以匿名内部类的形式去实现他的方法。表现为直接在参数小括号中: void test(param1, () -> System.out.print("hello world"))

当使用作为方法参数类型是,通过在接口上添加@FunctionalInterface注解来声明。

我们举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 函数式接口
@FunctionalInterface
public interface TestInterface {
void test();
}

// 将接口作为方法参数
public static void test(TestInterface testInterface){
// 有且仅有一个抽象方法
testInterface.test();
}

// 调用方法
test(()-> System.out.println("hello world"));

我们可以看到,实现的代码非常的简洁,对于一些不复杂的功能用起来非常方便,而如果使用原本的实现方式:

1
2
3
4
5
6
7
8
9
TestInterface testInterface = new TestInterface() {
@Override
public void test() {
System.out.println("hello world")
}
}

// 调用方法
test(testInterface);

Lambda 写法的方便简洁可见一斑。

二、Lambda 表达式

1.语法

Lambda 表达式,也可称为闭包。一个典型的表达式由一对圆括号和括号中的参数,一个横杠加箭头,和一对大括号组成:

1
(int x, int y) -> { System.out.println("hello world")) }

实际上,表达式并不是所有时候都需要写的那么标准:

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

2.配合函数式接口使用

Lambda 表达式需要配合函数式接口使用。

对于函数式接口,我们可以像上文举的例子一样,自己创建一个函数式接口,也可以使用 java.util.function包下已经提供好的函数式接口。

java.util.function包提供了很多现成的接口,菜鸟教程上介绍的很详细,这里就不复制黏贴了。

现在,我们用用看:

1
2
3
4
5
6
7
8
9
10
// 使用自己的函数式接口
TestInterface testInterface = () -> {
int i = 0;
System.out.println(i++);
};
testInterface.test();

// 使用 java.util.function 包的函数式接口
IntFunction intFunction = x -> x + 1;
System.out.println(intFunction.apply(15));

我们可以发现,对于我们自己的定义的接口,唯一的抽象方法是test(),所以放入实现以后实际上就是test()方法的实现,而 IntFunction 通过表达式实现的就是apply()方法。

其中,函数式接口总是有且仅有一个抽象方法,当作为参数使用的时候,通过 Lambda 表达式传入匿名方法默认就是实现这个唯一的抽象方法。另外,和正常的接口一样,函数式接口也允许拥有多个 default 修饰的默认实现方法。

3.变量作用域

Lambda 实际上可以理解为一个匿名的内部类,他可以访问外部的变量,但是不可以对变量做出改变

1
2
3
4
5
6
7
int i = 0;
IntFunction intFunction = x -> {
// lambda表达式中使用的变量应该是final
i++;
return x + 1;
};
i++;

上述这段代码会报编译错误,他会提示 lambda 表达式中使用的变量应该是 final。当然,我们删去 i++这行代码,让方法返回 i + 1就没影响了。可以见我们并不需要加 final 也可以。以前 java 的匿名内部类在访问外部变量的时候,外部变量必须用 final 修饰。在JDK8 对这个限制做了优化,可以不用显示使用final修饰,编译器自己隐式当成 final 来处理。

4.表达式的 this

我们可以通过一个简单的例子来了解一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class BeanA {

String name = "BeanA";

public void test() {

// 匿名内部类
new Thread(new Runnable() {
private String name = "Runnable";
@Override
public void run() {
System.out.println("这里的this指向匿名类:" + this.name);
}
}).start();

// Lambda表达式
new Thread(() -> {
System.out.println("这里的this指向当前的ThisDemo类:" + this.name);
}).start();
}
}

// 结果:
// 这里的this指向匿名类:Runnable
// 这里的this指向当前的ThisDemo类:张三

匿名内部类的 this 指向的是内部类自己,而 Lambda 表达式里的 this 实际上指向的是离他最近的那一层的外部类

之所以这样,是因为当要编译 Lambda 表达式的时候,JVM会把 Lambda 表达式编译为一个在本类中的以lambda$+数字的方法

值得一提的是,我们知道静态方法通过类调用,所以静态方法是获取不到 this 实例的,而Lambda 表达式会被编译为一个方法,如果表达式中使用了 this,那么就会编译为一个非静态方法,而未使用 this,就会编译为一个带 static 关键字的静态方法

三、方法引用

1.语法

方法引用通过方法的名字来指向一个方法,他使 Lambada 更加简洁易懂。

方法引用的写法类似这样 类/实例::方法名

  • 引用静态方法/成员方法:Class::StaticMethod 或 Class::Method
  • 引用成员方法:Instance::Method

下面举个例子来说明一下这些引用方式的差异:

假如我们有这么一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class BeanA {

String name;

/**
* 静态方法
* @param a
*/
public static void staticMethod(BeanA a) {
System.out.println(a.getName() + "被staticMethod输出了!");
}

/**
* 成员方法
*/
public void instanceMethod() {
System.out.println(this.getName() + "被instanceMethod方法输出了!this指向" + this.hashCode());
}

}

我们通过方法引用来使用这些方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1.引用静态方法
Consumer<BeanA> c = BeanA::staticMethod;
c.accept(a);

// 2.通过类引用成员方法
Consumer<BeanA> c2 = BeanA::instanceMethod;
c2.accept(a);

// 3.通过实例引用成员方法
Supplier<String> c3 = a::instanceMethod;
c3.get();

// 结果
张三被staticMethod输出了!
张三被instanceMethod方法输出了!this指向45721950
张三被instanceMethod方法输出了!this指向45721950

2.方法引用中的this

我们可以在上述的例子中注意到一个很有趣的地方,成员方法使用了this,但是仍然可以通过跟静态一样的形式去调用,但是却需要传递一个方法参数,而通过实例去调用就不需要传入参数

instanceMethod()本身是个无参的方法,但是在第二中引用方式中却传递了 a 这个对象进去,我们是不是可以认为这就是被传入使用的 this?

我们再写一个方法作为对照:

1
2
3
4
5
6
7
8
9
10
// 传入李四
Consumer<BeanA> c2 = BeanA::instanceMethod;
c2.accept(new BeanA("李四", 18));
// 传入张三
Consumer<BeanA> c25 = BeanA::instanceMethod;
c25.accept(a);

// 结果
李四被instanceMethod方法输出了!this指向49685098
张三被instanceMethod方法输出了!this指向45721950

我们可以看到,同样一个方法,传入了不同的实例,this 就指向了不同的实例。我们可以进一步推测,是不是跟 python 中的类的成员方法中的 self 一样,java 的成员方法也有一个 this 作为参数,只是平时编译器帮我们省略了呢?

1
2
3
4
5
6
7
8
// 手动添加一个 this
public String testMethod(BeanA this) {
System.out.println("参数this指向" + this.hashCode() + ",关键字的this指向" + this.hashCode());
return this.getName();
}

// 调用时可以发现,并不需要传入 this 这个参数
a.testMethod();

至此问题就明朗了,this 其实也是一个方法参数。

0%