文章目录
- 前言
- 一、第一个Lambda表达式
- 二、如何辨别Lambda表达式
- 三、引用值,而不是变量
- 四、函数接口
- 五、类型推断
- 总结
前言
Java 8 的最大变化是引入了 Lambda 表达式——一种紧凑的、传递行为的方式。
一、第一个Lambda表达式
Swing 是一个与平台无关的 Java 类库,用来编写图形用户界面(GUI)。该类库有一个常见用法:为了响应用户操作,需要注册一个事件监听器。用户一输入,监听器就会执行一些操作:
Button button = new Button();
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("button clicked");
}
});
设计匿名内部类的目的,就是为了方便 Java 程序员将代码作为数据传递。不过,匿名内部类还是不够简便。为了调用一行重要的逻辑代码,不得不加上 4 行冗繁的样板代码。这些代码还相当难读,因为它没有清楚地表达程序员的意图。我们不想传入对象,只想传入行为。在 Java 8 中,上述代码可以写成一个
Lambda 表达式,如下所示。
Button button = new Button();
button.addActionListener(e -> System.out.println("button clicked"));
和传入一个实现某接口的对象不同,我们传入了一段代码块——一个没有名字的函数。event 是参数名,和上面匿名内部类示例中的是同一个参数。-> 将参数和 Lambda 表达式的主体分开,而主体是用户点击按钮时会运行的一些代码。
和使用匿名内部类的另一处不同在于声明 event 参数的方式。使用匿名内部类时需要显式地声明参数类型 ActionEvent event,而在 Lambda 表达式中无需指定类型,程序依然可以编译。这是因为 javac 根据程序的上下文(addActionListener 方法的签名)在后台推断出了参数 event 的类型。这意味着如果参数类型不言而明,则无需显式指定。
二、如何辨别Lambda表达式
Lambda 表达式除了基本的形式之外,还有几种变体,如下所示:
Runnable runnable = () -> System.out.println("hello world"); // 1
ActionListener actionListener = e -> System.out.println("button clicked");// 2
Runnable runnable1 = () ->{
System.out.print("Hello");
System.out.println(" World");
}; // 3
BinaryOperator<Long> add = (x, y) -> x + y; // 4
BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y; // 5
- 中所示的 Lambda 表达式不包含参数,使用空括号 () 表示没有参数。该 Lambda 表达式实现了 Runnable 接口,该接口也只有一个 run 方法,没有参数,且返回类型为 void。
- 中所示的 Lambda 表达式包含且只包含一个参数,可省略参数的括号。
- Lambda 表达式的主体不仅可以是一个表达式,而且也可以是一段代码块,使用大括号({})将代码块括起来,该代码块和普通方法遵循的规则别无二致,可以用返回或抛出异常来退出。只有一行代码的 Lambda 表达式也可使用大括号,用以明确 Lambda表达式从何处开始、到哪里结束。
- Lambda 表达式也可以表示包含多个参数的方法,这行代码并不是将两个数字相加,而是创建了一个函数,用来计算两个数字相加的结果。变量 add 的类型是 BinaryOperator<Long>,它不是两个数字的和,而是将两个数字相加的那行代码。
- 到目前为止,所有 Lambda 表达式中的参数类型都是由编译器推断得出的。这当然不错,但有时最好也可以显式声明参数类型,此时就需要使用小括号将参数括起来,多个参数的情况也是如此。
三、引用值,而不是变量
如果你曾使用过匿名内部类,也许遇到过这样的情况:需要引用它所在方法里的变量。这时,需要将变量声明为 final。将变量声明为 final,意味着不能为其重复赋值。同时也意味着在使用 final 变量时,实际上是在使用赋给该变量的一个特定的值。
String name = "saa";
Button button = new Button();
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("button clicked");
System.out.println(name);
}
});
Java 8 虽然放松了这一限制,可以引用非 final 变量,但是该变量在既成事实上必须是final。虽然无需将变量声明为 final,但在 Lambda 表达式中,也无法用作非终态变量。既成事实上的 final 是指只能给该变量赋值一次。换句话说,Lambda 表达式引用的是值,而不是变量
。如果你试图给该变量多次赋值,然后在 Lambda 表达式中引用它,编译器就会报错。
这种行为也解释了为什么 Lambda 表达式也被称为闭包。未赋值的变量与周边环境隔离起来,进而被绑定到一个特定的值。在众说纷纭的计算机编程语言圈子里,Java 是否拥有真正的闭包一直备受争议,因为在 Java 中只能引用既成事实上的 final 变量。名字虽异,功能相同。
四、函数接口
函数接口是只有一个抽象方法的接口,用作 Lambda 表达式的类型。
使用只有一个方法的接口来表示某特定方法并反复使用,是很早就有的习惯。,Lambda 表达式也使用同样的技巧,并将这种接口称为函数接口。如下展示了前面例子中所用的函数接口:
public interface ActionListener extends EventListener {
public void actionPerformed(ActionEvent e);
}
ActionListener 只有一个抽象方法:actionPerformed,被用来表示行为:接受一个参数,返回空。记住,由于 actionPerformed 定义在一个接口里,因此 abstract 关键字不是必需的。该接口也继承自一个不具有任何方法的父接口:EventListener。
这就是函数接口,接口中单一方法的命名并不重要,只要方法签名和 Lambda 表达式的类型匹配即可。可在函数接口中为参数起一个有意义的名字,增加代码易读性,便于更透彻地理解参数的用途。
常用的函数式接口:
五、类型推断
某些情况下,用户需要手动指明类型,建议大家根据自己或项目组的习惯,采用让代码最便于阅读的方法。有时省略类型信息可以减少干扰,更易弄清状况;而有时却需要类型信息帮助理解代码。经验证发现,一开始类型信息是有用的,但随后可以只在真正需要时才加上类型信息。
Lambda 表达式中的类型推断,实际上是 Java 7 就引入的目标类型推断的扩展。我们已经知道 Java 7 中的菱形操作符,它可使 javac 推断出泛型参数的类型。
Map<String, Integer> oldWordCounts = new HashMap<String, Integer>(); // 1
Map<String, Integer> diamondWordCounts = new HashMap<>(); // 2
我们为变量 oldWordCounts 1明确指定了泛型的类型,而变量 diamondWordCounts 2则使用了菱形操作符。不用明确声明泛型类型,编译器就可以自己推断出来。
如果将构造函数直接传递给一个方法,也可根据方法签名来推断类型。在下面例子中,如果将构造函数直接传递给一个方法,也可根据方法签名来推断类型。
useHashmap(new HashMap<>());
private void useHashmap(Map<String, String> values);
Java 7 中程序员可省略构造函数的泛型类型,Java 8 更进一步,程序员可省略 Lambda 表达式中的所有参数类型。javac 根据 Lambda 表达式上下文信息就能推断出参数的正确类型。程序依然要经过类型检查来保证运行的安全性,但不用再显式声明类型罢了。这就是所谓的类型推断。
总结
Lambda 表达式是一个匿名方法,将行为像数据一样进行传递。Lambda 表达式的常见结构:BinaryOperator<Integer> add = (x, y) → x + y。函数接口指仅具有单个抽象方法的接口,用来表示 Lambda 表达式的类型。