一、为什么要引入lambda表达式
lambda 表达式是一个可传递的代码块 , 可以在以后执行一次或多次 。
在介绍lambda表达式之前,我们看一下,以前,我们对于一个问题的通常写法。
假设你已经了解了如何按指定时间间隔完成工作,当然不了解也没关系,只是作为例子说明。 将这个工作放在一个 ActionListener 的 actionPerformed 方法中 :
class Worker implements ActionListener
{public void actionPerformed(ActionEvent event){// do some work}
}
想要反复执行这个代码时, 可以构造 Worker 类的一个实例 。 然后把这个实例提交到一个 Timer 对象 。 这里的重点是 actionPerformed 方法包含希望以后执行的代码 。
或者可以考虑如何用一个定制比较器完成排序。 如果想按长度而不是默认的字典顺序对
字符串排序 , 可以向 sort 方法传人一个 Comparator 对象 :
class LengthComparator implements Comparator<String>
{public int compare(String first, String second){return first.length() - second.length();}
}
这两个例子有一些共同点, 都是将一个代码块传递到某个对象 ( 一个定时器 , 或者一个 sort 方法)。这个代码块会在将来某个时间调用。
到目前为止, 在 Java 中传递一个代码段并不容易 , 不能直接传递代码段 。 Java 是一种面
向对象语言 , 所以必须构造一个对象 , 这个对象的类需要有一个方法能包含所需的代码。
在其他语言中, 可以直接处理代码块 。 Java 设计者很长时间以来一直拒绝增加这个特性 。 毕竟, Java 的强大之处就在于其简单性和一致性 。 如果只要一个特性能够让代码稍简洁一些 , 就把这个特性增加到语言中, 这个语言很快就会变得一团糟 , 无法管理 。 不过 , 在另外那些 语言中, 并不只是创建线程或注册按钮点击事件处理器更容易 ; 它们的大部分 API 都更简单 、 更一致而且更强大。 在 Java 中 , 也可以编写类似的 API 利用类对象实现特定的功能 , 不过这种 API 使用可能很不方便 。
二、lambda表达式的语法
再来考虑上面讨论的排序例子。 我们传入代码来检查一个字符串是否比另一个字符串短。 这里要计算 :
first. length() - second . length()
first 和 second 是什么 ? 它们都是字符串 。 Java 是一种强类型语言 , 所以我们还要指定它 们的类型:
(String first, String second)
-> first.length() - second.length()
这就是一个lambda表达式。lambda 表达式就是一个代码块 , 以及必须传入代码的变量规范。
你已经见过 Java 中的一种 lambda 表达式形式 : 参数 , 箭头 ( - > ) 以及一个表达式 。 如果代码要完成的计算无法放在一个表达式中, 就可以像写方法一样 , 把这些代码放在 {} 中 , 并包含显式的 return 语句 。 例如 :
即使 lambda 表达式没有参数 , 仍然要提供空括号 , 就像无参数方法一样 :
如果可以推导出一个 lambda 表达式的参数类型 , 则可以忽略其类型 。 例如 :
在这里, 编译器可以推导出 first 和 second 必然是字符串 , 因为这个 lambda 表达式将赋给一个字符串比较器。
如果方法只有一个参数 , 而且这个参数的类型可以推导得出 , 那么甚至还可以省略小括号 :
无需指定 lambda 表达式的返回类型 。 lambda 表达式的返回类型总是会由上下文推导得出。 例如 , 下面的表达式:
可以在需要 int 类型结果的上下文中使用 。
如果一个 lambda 表达式只在某些分支返回一个值 , 而在另外一些分支不返回值 , 这是不合法的。 例如 , ( int x ) - > { if ( x > = 0 ) return 1 ; } 就不合法 。
代码示例:
package FunctionProm;import javax.swing.*;
import java.util.Arrays;
import java.util.Date;public class LambdaTest {public static void main(String[] args) {String[] planets = new String[] { "Mercury" , "Venus" , "Earth" , "Mars" , "Jupiter" , "Saturn" , "Uranus" , "Neptune" };System.out.println(Arrays.toString(planets));System.out. println("Sorted in dictionary order:") ;Arrays.sort(planets);System.out.println (Arrays.toString(planets));System.out . println ("Sorted by length:");Arrays.sort(planets, (first, second) -> first.length() - second.length()) ;System.out. println(Arrays.toString(planets));Timer t = new Timer(1000, event ->System.out.println ("The time is " + new Date()));t.start();// keep program running until user selects "0k"JOptionPane.showMessageDialog (null , "Quit program?");System.exit(0);}
}
三、函数式接口
前 面 已 经 讨 论 过, Java 中 已 经 有 很 多 封 装 代 码 块 的 接 口 , 如 ActionListener 或 Comparator。 lambda 表达式与这些接口是兼容的。
对于只有一个抽象方法的接口, 需要这种接口的对象时 , 就可以提供一个 lambda 表达式。 这种接口称为函数式接口 ( functional interface ) 。
为了展示如何转换为函数式接口, 下面考虑 Arrays . sort 方法 。 它的第二个参数需要一个 Comparator 实例 , Comparator 就是只有一个方法的接口 , 所以可以提供一个 lambda 表达式 :
在底层, Arrays . sort 方法会接收实现了 Comparator < String > 的某个类的对象 。 在这个对象上调用 compare 方法会执行这个 lambda 表达式的体。这些对象和类的管理完全取决于具体实现, 与使用传统的内联类相比,这样可能要高效得多。最好把 lambda 表达式看作是一个函数,而不是一个对象, 另外要接受 lambda 表达式可以传递到函数式接口。
lambda 表达式可以转换为接口 ,这一点让 lambda 表达式很有吸引力 。 具体的语法很简短。 下面再来看一个例子 :
与使用实现了 ActionListener 接口的类相比 , 这个代码可读性要好得多 。
实际上, 在 Java 中, 对 lambda 表达式所能做的也只是能转换为函数式接口 。 在其他支 持函数字面量的程序设计语言中, 可以声明函数类型 ( 如 ( String , String ) - > int ) 、 声明这些类 型的变量, 还可以使用变量保存函数表达式 。 不过 , Java 设计者还是决定保持我们熟悉的接口概念, 没有为Java 语言增加函数类型 。
Java API 在 java . util . fimction 包中定义了很多非常通用的函数式接口 。 其中一个接口BiFunction< T , U , R > 描述了参数类型为 T 和 U 而且返回类型为 R 的函数 。 可以把我们的字符串比较 lambda 表达式保存在这个类型的变量中 :
不过, 这对于排序并没有帮助 。 没有哪个 Arrays . sort 方法想要接收一个 BiFunction 。 如果你之前用过某种函数式程序设计语言, 可能会发现这很奇怪 。 不过 , 对于 Java 程序员而言, 这非常自然 。 类似 Comparator 的接口往往有一个特定的用途 , 而不只是提供一个有指定参数和返回类型的方法。 Java SE 8 沿袭了这种思路 。 想要用 lambda 表达式做某些处理 , 还是要谨记表达式的用途, 为它建立一个特定的函数式接口 。
java . util . function 包中有一个尤其有用的接口 Predicate :
ArrayList 类有一个 removelf 方法 , 它的参数就是一个 Predicate 。 这个接口专门用来传递 lambda 表达式 。 例如 , 下面的语句将从一个数组列表删除所有 null 值 :
list. removelf ( e - > e = = null ) ;
四、方法引用
有时, 可能已经有现成的方法可以完成你想要传递到其他代码的某个动作 。 例如 , 假设你希望只要出现一个定时器事件就打印这个事件对象。 当然 , 为此也可以调用 :
但是, 如果直接把 println 方法传递到 Timer 构造器就更好了 。 具体做法如下 :
表达式 System . out :: println 是一个方法引用 ( method reference ) , 它等价于 lambda 表达式 x 一 > System . out . println ( x ) 。
再来看一个例子, 假设你想对字符串排序 , 而不考虑字母的大小写 。 可以传递以下方法表达式:
从这些例子可以看出, 要用:: 操作符分隔方法名与对象或类名 。 主要有 3 种情况 :
在前 2 种情况中 , 方法引用等价于提供方法参数的 lambda 表达式 。 前面已经提到 , System. out :: println 等价于 x - > System . out . println ( x)。 类似地, Math : : pow 等价于 ( x , y ) - >
Math . pow ( x , y)。
对于第 3 种情况 , 第 1 个参数会成为方法的目标 。 例如 , String : : compareToIgnoreCase 等
同于 ( x , y ) - > x . compareToIgnoreCase ( y ) 。
如果有多个同名的重栽方法, 编译器就会尝试从上下文中找出你指的那一个方法 。 例如, Math . max 方法有两个版本 , 一个用于整数 , 另一个用于 double 值 。 选择哪一个版 本取决于 Math :: max 转换为哪个函数式接口的方法参数 。 类似于 lambda 表达式 , 方法引用不能独立存在, 总是会转换为 函数式接口 的实例 。
可以在方法引用中使用 this 参数 。 例如 , this :: equals 等同于 x - > this . equals ( x ) 。 使用
super 也是合法的 。 下面的方法表达式:
super: : instanceMethod
使用 this 作为目标 , 会调用给定方法的超类版本
为了展示这一点 , 下面给出一个假想的例子 :
TimedGreeter. greet 方法开始执行时 , 会构造一个 Timer , 它会在每次定时器滴答时执行 super:: greet 方法 。 这个方法会调用超类的 greet 方法 。
五、构造器引用
构造器引用与方法引用很类似, 只不过方法名为 new 。 例如 , Person : : new 是 Person 构造 器的一个引用。 哪一个构造器呢 ? 这取决于上下文 。 假设你有一个字符串列表 。 可以把它转换为一个 Person 对象数组 , 为此要在各个字符串上调用构造器 , 调用如下 :
map 方法会为各个列表元素调用 Person ( String ) 构造器 。 如果有多个 Person 构造器 , 编译器会选择有一个 String 参数的构造器 , 因为它从上下文推导出这是在对一个字符串调用构造器。
可以用数组类型建立构造器引用。 例如 , int [] :: new 是一个构造器引用 , 它有一个参数 :即数组的长度。 这等价于 lambda 表达式 x - > new int [ x ]。
Java 有一个限制 , 无法构造泛型类型 T 的数组 。 数组构造器引用对于克服这个限制很有用。 表达式 new T [ n ] 会产生错误 , 因为这会改为 new Object [ n]。
对于开发类库的人来说, 这是一个问题。 例如 , 假设我们需要一个 Person 对象数组。
Stream 接口有一个 toArray 方法可以返回 Object 数组 :
不过, 这并不让人满意 。 用户希望得到一个 Person 引用数组 , 而不是 Object 引用数组 。流库利用构造器引用解决了这个问题。 可以把 Person [ ] : : new 传入 toArray 方法 :
toArray方法调用这个构造器来得到一个正确类型的数组 。 然后填充这个数组并返回 。
六、变量作用域
通常, 你可能希望能够在 lambda 表达式中访问外围方法或类中的变量 。 考虑下面这个例子:
public static void repeatMessage(String text, int delay)
{ActionListener listener = event ->{System.out.println(text);Toolkit.getDefaultToolkit().beep():};new Timer(delay, listener).start();
}
来看这样一个调用:
现在来看 lambda 表达式中的变量 text 。 注意这个变量并不是在这个 lambda 表达式中定义的。 实际上 , 这是 repeatMessage 方法的一个参数变量 。
如果再想想看, 这里好像会有问题 , 尽管不那么明显 。 lambda 表达式的代码可能会在 repeatMessage 调用返回很久以后才运行 , 而那时这个参数变量已经不存在了 。 如何保留 text
变量呢 ?
要了解到底会发生什么, 下面来巩固我们对 lambda 表达式的理解 lambda 表达式有 3 个部分:
1 ) 一个代码块 ;
2 ) 参数 ;
3 ) 自由变量的值 , 这是指非参数而且不在代码中定义的变量 。
在我们的例子中, 这个 lambda 表达式有 1 个自由变量 text 。 表示 lambda 表达式的数据结构必须存储自由变量的值, 在这里就是字符串 " Hello " 。 我们说它被 lambda 表达式捕获(下面来看具体的实现细节 。 例如 , 可以把一个 lambda 表达式转换为包含一个方法的对象, 这样自由变量的值就会复制到这个对象的实例变量中 。 )