在Java引入lambda表达式之前,并不能在Java中传递一个代码段。因为Java是严格的面向对象编程,所以必须构造一个对象,这个对象的类需要有一个方法来包含所需的代码。
Java SE8中加入了lambda表达式来处理代码块,增强Java来支持函数式编程。
lambda表达式的语法:
//表达式形式:参数,箭头以及一个表达式
(String first, String second) -> first.length() - second.length()
//如果代码要完成的计算无法放在一个表达式中,可以像方法一样使用{},并包含显式return语句
(String first, String second) ->
{if(first.length()<second.length()) return -1;else return 1;
}
如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型,如:
//在这里,first和second必然是字符串
Comparaor<String> comp = (first,second)->first.length()-second.length()
如果方法只有一个参数,而且这个参数的类型可以推导出,还可以省略小括号:
//这里event可以写作(event) or (ActionEvent event)
ActionListener listener = event->System.out.println("the time is"+ new Date());
分析上面的所有代码发现,无需指定lambda表达式的返回类型,因为lambda表达式的返回类型总会根据上下文推导得出。
函数式接口:
Java在不支持lambda表达式之前,已经存在很多封装代码块的接口,如Comparator, lambda表达式和这些接口是兼容的。
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口成为函数式接口(使用lambda表达式比创建一个类并实现该接口更加简单,同时该名称也体现了函数式编程的概念)。
例如,Arrays.sort()方法提供了自定义排序。其第一个参数是待排序的数组,第二个参数是排序规则--一个实现了Comparator接口的实例。因为Comparator是只有一个抽象方法的接口,我们不必再去写一个类去实现这个接口,直接用lambda表达式:
Arrays.sort(arrays, (first,second)->first.length()-second.length());
这行代码可以实现字符串数组arrays中字符串的按长度排序。
上面的实例就是lambda表达式可以转换为函数式接口。但也只能做到这些。相比较而言,其他支持函数式编程的程序设计语言可以声明函数类型,声明这些类型的变量,还可以使用变量保存函数表达式。
方法引用:
如果有一个现成的方法可以完成想要传递的代码段的操作,那么我们可以直接传递该方法:
Timer t = new Timer(1000,System.out::println);
上面代码的功能是每出现一个定时器事件(1秒)就打印这个事件对象,上面的代码等同于:
Timer t = new Timer(1000,event->System.out.println(event));
再比如,如果想对字符串排序,并且不考虑大小写,可以传递以下方法表达式:
Arrays.sort(strings,String::compareToIgnoreCase);
等价于:
Arrays.sort(strings,(x,y)->x.compareToIgnoreCase(y));
从上面例子可以看出,要用 " :: " 分隔对象名/类名与方法名,主要有三种情况:
- object::instanceMethod
- Class::staticMethod
- Class::instanceMethod
当然,this和super关键字也可以用在方法引用中。
构造器引用:
构造器引用和方法引用很类似,只不过方法名为new。例如,Person::new是Person构造器的一个引用。哪一个构造器取决于上下文。
例如,假设有一个字符串列表要把它转换成Person数组,为此要在各个字符串上调用构造器:
ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());
可以用数组类型建立构造器引用,如:int[]::new是一个构造器引用,它有一个参数:数组长度,等价于: x->new int[x];
Java无法构造泛型类型T的数组,因为new T[n]会变成new Object[n]。但流苦利用构造器引用可以解决这个问题。例如需要一个Person数组,Stream接口有一个toArray方法可以返回一个Object数组:
Object[] people = stream.toArray();
但我们真正想要的是Person数组,可以这样做:
Person[] people = stream.toArray(Person::new);
变量的作用域:
我们可能会在lambda表达式中访问外围变量,如:
public static void repeatMessage(String text,int delay){ActionListener listener = event->{System.out.println(text);Toolkit.getDefaultToolkit().beep();};new Timer(delay,listener).start();
}
然后我们这样调用:
repeatMessage("Hello",1000); //每隔1秒打印一个“Hello"
这里会有一个问题,lambda表达式可能在repeatMessage调用返回很久以后才运行,那时参数变量text已经不存在了,如何保留text?
来看一下lambda表达式的组成:
- 参数;
- 一个代码块;
- 自由变量的值,这是指非参数而且不在代码中定义的变量。
这里text就是自由变量,是lambda表达式的数据结构中必须保存的值。我们说它被lambda表达式捕获。
注意:关于代码块和自由变量值有一个术语:闭包。在Java中,lambda表达式就是闭包。
在Java中,要确保捕获的值是明确定义的,且有一个重要的限制:lambda表达式要捕获的变量必须是实际上的最终变量(该变量初始化之后不会再为它赋新值)。
处理lambda表达式:
上面讲了如何编写lambda表达式以及如何将lambda表达式传递到方法中。但如果我们写一个方法,如何保证我们的方法可以处理lambda表达式呢?
例如,我们要重复一个动作n次,将这个动作和重复次数传进repeat:
repeat(10,()->System.out.println("Hello World!"));
要接受这个lambda表达式,需要选择(有时需要提供)一个函数式接口。例如我们选用Runable接口:
public static void repeat(int n,Runnable action){for(int = 0; i < n; i++) action.run();
}
当我们吧lambda表达式传入方法的第二个参数后,调用action.run()时会执行lambda表达式的主体。
补充:
什么时候使用lambda表达式:
- 在一个单独的线程中运行代码
- 多次运行代码
- 在算法的适当位置运行代码(例如排序中的比较操作)
- 发生某种事件时执行代码
- 只在必要时才运行的代码
常用的函数式接口:
函数式接口 | 参数类型 | 返回类型 | 抽象方法名 | 描述 |
Runnable | 无 | void | run | 作为无参数或返回值的动作执行 |
Supplier<T> | 无 | T | get | 提供一个T类型的值 |
Consumer<T> | T | void | accept | 处理一个T类型的值 |
BiConsumer<T> | T,U | void | accept | 处理T和U类型的值 |
Function<T,R> | T | R | apply | 有一个T类型参数的函数 |
BiFunction<T,U,R> | T,U | R | apply | 有T和U类型参数的函数 |
UnaryOperator<T> | T | T | apply | 类型T上的一元操作符 |
BinaryOperator<T> | T,T | T | apply | 类型T上的二元操作符 |
Predicate<T> | T | boolean | test | 布尔值函数 |
BiPredicate<T> | T,U | boolean | test | 有两个参数的布尔值函数 |