Java--lambda(λ)表达式

2018-02-27 11:51:15来源:oschina作者:Superheros人点击

分享

在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()else return 1;
}

如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型,如:


//在这里,first和second必然是字符串
Comparaor 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 names = ...;
Stream stream = names.stream().map(Person::new);
List 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
get
提供一个T类型的值


Consumer
T
void
accept
处理一个T类型的值


BiConsumer
T,U
void
accept
处理T和U类型的值


Function
T
R
apply
有一个T类型参数的函数


BiFunction
T,U
R
apply
有T和U类型参数的函数


UnaryOperator
T
T
apply
类型T上的一元操作符


BinaryOperator
T,T
T
apply
类型T上的二元操作符


Predicate
T
boolean
test
布尔值函数


BiPredicate
T,U
boolean
test
有两个参数的布尔值函数

最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台