参考自–《Java核心技术卷1》
lambda 表达式是一个可传递的代码块,可以在以后执行一次或多次。
如下:定制比较器完成数组排序,按长度而不是默认的字典顺序对字符串排序:
class LengthComparator implements Comparator<String>{
//代码块
public int compare(String first,String second){
return first.length()-second.length();
}
}
Arrays.sort(strings,new LengthComparator()); //strings是数组,LengthComparator为指定的比较器
比较器中的 compare
方法不是立即调用。实际上,在数组完成排序之前,sort
方法会一直调用 compare
方法对数组中两两元素进行比较,只要数组元素的顺序不正确就会重新排列元素。将比较元素所需的代码块放在 sort 方法中,这个代码将与其余的排序逻辑集成(可能你并不打算重新实现其余的这部分逻辑)。
使用 lambda 表达式的情况都有这样的一些特点:将一个代码块传递到某个对象,这个代码块会在将来某个时间调用。
到目前为止,在 Java 中传递一个代码块并不容易,不能直接传递代码块。Java 是一种面向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法能包含所需要的代码块。
lambda 表达式的语法:
如上述的排序比较器代码块:它需要比较一个字符串是否比另一个字符串短
first.length()-second.length();
Java 是一种强类型语言,所以必须指定 first 和 second 的类型:
(String first,String second)
-> first.length()-second.length();
这就是一个 lambda 表达式。lambda 表达式就是一个代码块,以及必须传入代码的变量规范。
上述是 lambda 表达式的一种表达形式:参数,箭头(->)以及一个表达式。
如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,将这些代码放在 {} 中,并包含显式的 return 语句:例如
(String first,String second) -> {
if(first.length()<second.length())
return -1;
else if(first.length()>second.length())
return 1;
else
return 0;
}
即使 lambda 表达式没有参数,也要提供空括号,就像无参数方法一样:
() -> {
for(int i=10;i>=0;i--){
System.out.println(i);
}
}
如果可以推导出一个 lambda 表达式的参数类型,则可以忽略其类型:
Comparator<String> comp
= (first,second)
-> first.length()-second.length();
在这里,编译器可以推导出 first 和 second 必然是字符串,因为这个 lambda 表达式将赋给一个字符串比较器。
如果方法只有一个参数,而且这个参数的类型可以推导出,那么甚至可以省略小括号(参数).
另外,无须指定 lambda 表达式的返回类型。它的返回类型总是会由上下文推导得出。
注:如果一个 lambda 表达式只在某些分支返回一个值,而在另外一些分支不返回值,则不合法。如:
(int x) -> {if(x>=0) return 1;} //不合法
在程序中加入 lambda 表达式:
public static void main(String[] args){
String[] planets = new String[]{"Mercury","Venus","Earth","Mars","Jupiter",
"Saturn","Uranus","Neptune"};
System.out.println(Arrays.toString(planets));
//默认字典顺序排序
Arrays.sort(planets);
System.out.println(Arrays.toString(planets));
System.out.println("++++++++++++++++++");
//在比较器中使用lambda表达式,比较字符串长度进行排序
Arrays.sort(planets,(first,second)->first.length()-second.length());
System.out.println(Arrays.toString(planets));
}
上述的lambda表达式就是一个 Comparator 接口实例:
Comparator comparator = (first,second)->first.length()-second.length();
Java 中已经有很多封装代码块的接口,如 Comparator
等等,lambda 表达式与这些接口是兼容的。
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个 lambda 表达式。这种接口称为函数式接口。
注:在 Java SE 8中,接口可以声明非抽象方法。
下面再看看 Arrays.sort(T[] a,Comparator c)
方法。它的第二个参数需要一个 Comparator
实例,Comparator
就是只有一个方法的接口,所以可以提供一个 lambda 表达式:
Arrays.sort(planets,
(first,second)->first.length()-second.length());
在底层,Arrays.sort()
方法会接收实现了 Comparator
接口的某个类的对象。在这个对象上调用 compare
方法会执行这个 lambda 表达式的体。这些对象和类的管理完全取决于具体实现,与使用传统的内联类相比,这样可能要高效得多。最好把 lambda 表达式看作是一个函数,而不是一个对象,另外要接受 lambda 表达式可以传递到函数式接口。
lambda 表达式可以转换为接口,这一点让 lambda 表达式很有吸引力。具体的语法也很简短。
例如:计时器的实现中,第一个参数为delay延迟时间(毫秒),第二个参数为 ActionListener
接口的实现
//每个一段时间打印当前时间
Timer t = new Timer(1000,event -> {
System.out.println(new Date());
});
t.start();
//弹出组件
JOptionPane.showMessageDialog(null,"Quit?");
System.exit(0);
上述语句中,编译器执行完 Timer 后创建一个定时器,每隔 1 秒打印一次当前时间,然后等待 JOptionPane.showMessageDialog(null,"Quit?");
语句执行结束(即对弹出窗口进行操作完毕后),执行 exit(0) 退出程序。
上述通过 lambda 表达式实现 ActionListener
接口要比实现ActionListener
接口的类的代码可读性好得多。
通过实现ActionListener
接口的类完成上述定时器:
//实现ActionListener接口的类
public class TimePrinter implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("new Time: "+new Date());
}
}
public class TimerTest {
//接口回调
public static void main(String[] args){
ActionListener listener = new TimePrinter();
Timer t = new Timer(1000,listener);
t.start();
JOptionPane.showMessageDialog(null,"Quit?");
System.exit(0);
}
}
实际上,在 Java 中,对 lambda 表达式所能做的也只是能转换为函数式接口。在其他支持函数字面量的程序设计语言中,可以声明函数类型(如(String,String)->int)、声明这些类型的变量,还可以使用变量保存函数表达式。不过,Java 还是保持着接口概念,没有增加函数类型。
注:甚至不能把 lambda 表达式赋给类型为 Object 的变量,Object 不是一个函数式接口。
了解: Java API 在 java.util.function
包中定义了很多非常通用的函数式接口。比如其中一个接口 BiFunction
描述了参数类型为 T,U而且返回类型为 R 的函数。如:
BiFunction<String,String,Integer> comp
= (first,second) -> first.length()-second.length();
java.util.function
有一个尤其有用的接口 Predicate:
public interface Predicate<T>{
boolean test(T t);
}
在 ArrayList 类有一个 removeIf
方法,它的参数就是一个 Predicate ,这个接口专门用来传递 lambda 表达式:
list.removeIf(e -> e == null);
有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。例如,假设每隔一段时间打印定时器事件对象,可以调用:
Timer t = new Timer(1000,event -> {
System.out.println(event);
});
但是,如果直接把 println 方法传递到 Timer 构造器就更好了:
Timer t = new Timer(1000,System.out::println);
上述表达式 System.out::println
是一个方法引用,它等价于 lambda 表达式 x -> System.out.println(x)
System.out::println
的使用:
Consumer<String> con = System.out::println;
con.accept("123456"); //打印出 123456
再看,若对字符串排序(按字典顺序),而不考虑字母的大小写,可以传递如下的方法表达式:
Arrays.sort(strings,String::compareToIgnoreCase);
用 ::
分隔方法名与对象或类名,主要分3种情况:
object::instanceMethod
对象的实例方法(没有static的就是实例方法)Class::staticMethod
类的静态方法Class::instanceMethod
类的实例方法对于前两种情况,方法引用等价于提供方法参数的 lambda 表达式。如 System.out::println
等价于 x -> System.out.println(x)
,类似的,Math::pow
等价于 (x,y) -> Math.pow(x,y)
.
对于第三种情况,第一个参数会成为方法的目标参数。例如:String::compareToIgnoreCase
等价于 (x,y) -> x.compareToIgnoreCase(y)
.
注:如果有多个同名的重载方法,编译器就会尝试从上下文中找出需要的方法。例如,Math.max
方法就有两个版本,一个用于整数,一个用于 double 值。选择哪个版本的方法取决于 Math::max
转换为哪个函数式接口的方法参数。类似于 lambda 表达式,方法引用不能独立存在,总会转换为函数式接口的实例。
方法引用中也可以使用 this 参数,例如:this::equals
等价于 x->this.equals(x)
。
同时,方法引用也可以使用 super 关键字,其表达式如:super::instanceMethod
调用超类的实例方法
//超类
class Greeter{
public void greet(){
System.out.println("hello world");
}
}
//子类
class TimeGreeter extends Greeter{
public void greet(){
Timer t = new Timer(1000,super::greet);
t.start();
}
}
TimeGreeter.greet
方法开始执行时,首先会创建一个 Timer,它会在每次定时器过 1 秒后执行 super::greet
方法,而这个方法会调用超类的 greet 方法
构造器引用与方法引用类似,只不过方法名为 new。例如,Person::new
是 Person 构造器的一个引用。而此过程中引用哪一个构造器取决为上下文。
构造器引用示例:
//Employee类
public class Employee{
private String name;
private double salary;
public Employee() {
}
public Employee(String name) {
this.name = name;
}
public Employee(String name,double salary) {
this.name = name;
this.salary = salary;
}
}
//通过lambda创建Employee对象
public class test{
public static void main(String[] args) {
//无参构造
Supplier<Employee> lam = ()->new Employee();
//带一个参数的构造方法
Function<String,Employee> lam1 = (x)->new Employee(x);
//带两个参数的构造方法
BiFunction<String, Double,Employee> lam2 = (x, y)->new Employee(x,y);
//使用lambda表示的构造器方法创建Employee对象
Employee e0 = lam.get();
Employee e1 = lam1.apply("zs");
Employee e2 = lam2.apply("lisi",100.0);
}
}
public class test{
public static void main(String[] args) {
//通过构造引用创建Employee对象
//无参构造
Supplier<Employee> fun0 = Employee::new;
//带一个参数的构造方法
Function<String,Employee> fun1 = Employee::new;
//带两个参数的构造方法
BiFunction<String, Double,Employee> fun2 = Employee::new;
Employee e0 = fun0.get();
Employee e1 = fun1.apply("zs");
Employee e2 = fun2.apply("lisi",100.0);
//对象的实例方法引用
Supplier<String> fun = e1::getName;
//通过方法引用的形式获取
String name = fun.get(); //zs
}
}
数组的构造器引用:
public static void main(String[] args){
//lambda表达式
Function<Integer,String[]> lam = (x) -> new String[x];
//创建长度为10的数组
String[] arr1 = lam.apply(10);
//构造器调用
Function<Integer,String[]> fun = String[]::new;
String[] arr2 = fun.apply(10);
}
lambda 表达式是如何获取外围方法或类中的变量呢?lambda 表达式共有3部分:
1)1段代码块
2)参数
3)自由变量的值,指的是非参数而且不在代码块中定义的变量
看看下面这个例子:
public static void repeatMessage(String text,int delay){
ActionListener listener = event -> {
System.out.println(text);
};
new Timer(delay,listener).start();
}
调用上述定义的方法:
repeatMessage("hello",1000); //每隔1秒打印一次"hello"
上述例子中,lambda 有一个自由变量 text(值为"hello")。我们可以把一个lambda表达式看作包含一个方法的对象,自由变量就是这个对象的实例变量。
在 lambda 表达式中获取外围作用域中的变量有一个重要的限制:在 lambda 表达式中,只能引用值不会改变的变量(即 lambda 表达式引用的变量无论在 lambda 中或者 lambda 表达式外部都不能发生改变)。
规则:lambda 表达式捕获引用的变量必须实际上是最终变量(即此变量初始化之后就不会再为它赋新值)。
其次,lambda 表达式的体与嵌套块(语句块)有相同的作用域,那么就需要注意命名冲突的问题:
String first = "abc";
Comparator<String> comparator = (first,second)->first.length()-second.length();
//error:lambda表达式中声明了一个与外部局部变量同名的参数
在 lambda 表达式中使用 this 关键字时,是指创建这个 lambda 表达式的方法的 this 参数:
public class Application(){
public void init(){
ActionListener listener = event -> {
System.out.println(this.toString());
...
}
}
}
this.toString()
会调用 Application 对象的 toString 方法。
使用 lambda 表达式的重点是延迟执行。毕竟,如果想要立即执行代码,完全可以直接执行,而不需要包装在一个 lambda 表达式中。延迟执行代码块的原因有很多:
例如:想要重复执行某段代码(重复打印 “hello”)
要接受 lambda 表达式,需要选择(或提供)一个函数式接口,这里使用 Runnable 接口:
public static void repeat(int n,Runnable action){
for(int i=0;i<n;i++){
action.run();
}
}
//这是重复运行10次的 lambda 表达式
repeat(10,()->System.out.println("hello"));
只调用一次:
Runnable fun = ()->System.out.println("nm");
fun.run(); //打印一次 "nm"
调用 run 方法时,会执行 lambda 表达式的主体。
常见函数式接口:
基本类型的函数式接口:最好使用这些带参数规范的函数式接口,这样可以减少自动装箱
注:如果需要设计自己的函数式接口,其中只有一个抽象方法,可以使用 @FunctionalInterface
注解来标记这个接口,这样有两个优点:1.如果无意中增加了另一个非抽象方法,编译器会报错;2.Javadoc 页会指出你的接口是一个函数式接口。当然,不是必须使用注解,任何有一个抽象方法的接口都是函数式接口。不过使用注解是比较好的做法。