lambda表达式简介

在JDK8版本中,新增了一个功能:LAMBDA表达式,他显著增强了Java语言的表达功能,lambda表达式不仅为Java语言新增了一些语法元素,而且还简化了某些通用结构的实现方式.如同几年前泛型出现从根本上改变了Java语言一样,如今lambda表达式的出现也从根本上改变了Java语言,他们的出现确实都非常重要.
lambda表达式的引入也催生了其他新的Java功能,如前面已经介绍过的默认方法和后面将要介绍的方法引用.默认方法允许为接口方法定义默认行为,而方法引用允许在不执行方法的情况下即可引用方法.
另外,lambda表达式的引入让API库也增加了一些新功能.
除了带给语言的好处,还有另一个原因让lambda表达式成为Java的重要新增功能.在过去几年中,lambda表达式已经成为计算机语言设计的重点关注对象,例如,C#和C++等语言都添加了LAMBDA表达式,Java语言中添加的LAMBDA表达式帮助使Java继续保持程序员所期望的活力和创新性.
1.lambda表达式介绍
对于理解LAMBDA表达式的Java实现,有两个结构十分关键.第一个就是lambda表达式自身,第二个是函数式接口.下面首先为这两个结构下一个定义.
LAMBDA表达式本质上就是一个匿名(即未命名)方法.但是,这个方法不是独立执行的,而是用于实现由函数式接口定义的另一个方法.因此,LAMBDA表达式会导致一个匿名类,LAMBDA表达式常被称为闭包.
函数式接口仅包含一个抽象方法的接口,一般来说,这个方法指明了接口的目标用途.因此,函数式接口通常表示单个动作.例如,标准接口tunnable是一个函数式接口,因为他只定义了一个方法run(),因此,run()定义了Runnable的动作.此外,函数式接口定义了lambda表达式的目标类型.特别注意:lambda表达式只能用于其目标类型已被指定的上下文.另外,函数式接口有时候被称为SAM类型,意思是单抽象方法(Single Abstract Method)
注意:函数式接口可以指定Object定义的任何公有方法,例如equals(),而不影响其作为”函数式接口”的状态.Object的公有方法被视为函数式接口的隐式成员,因为函数式接口的实例会默认自动实现他们.
(1)lambda表达式的基础知识
lambda表达式在Java语言中引入了一个新的语法元素和运算符,这个运算符是->,有时候被称为lambda运算符或箭头运算符.他将lambda表达式分成两个部分.左侧指定了lambda表达式需要的所有参数.右侧是lambda体,他指定了lambda表达式的动作.Java定义了两种lambda体.一种包含单独一个表达式,另一种包含一个代码块.我们首先讨论第一种类型的lambda表达式.
在继续讨论之前,先看几个lambda表达式的例子会有帮助.首先看一个可能是最简单的lambda表达式的例子.他的计算结果是一个常量,如下所示:
()->98.6
这个lambda表达式没有形参,所以形参为空.他返回常量值98.6,返回值的类型推断为double类型,因此,这个表达式的作用类似于下面的方法:
double myMeth(){return 98.6;}
当然,lambda表达式定义的方法没有名称,下面给出了一个更有意思的lambda表达式:
()->Math.ramdom()*100
这个lambda表达式使用Math.random()获得一个伪随机数,将其乘以100,然后返回返回结果.这个LAMBDA表达式也不需要形参.
当LAMBDA表达式需要形参时,需要在运算符左侧的形参列表中加以指定.下面是一个简单的例子:
(n)->1.0/n
这个LAMBDA表达式返回形参n的值的倒数.因此,如果n为4.0,则倒数为0.25,尽管可以显式指定形参类型,例如本例中的n,但是通常不需要这么做,因为很多时候,形参的类型是可以推断出来的.与命名方法一样,lambda表达式可以指定需要用到的任意数量的形参.
任何有效的类型都可以用作lambda表达式的返回值类型.例如,如果形参n的值为偶数,则下面的lambda表达式返回true;否则,返回false.
(n)->(n%2)==0
因此,该LAMBDA表达式返回值类型是boolean类型
在继续讨论之前还有一点值得提及,当lambda表达式仅有一个形参时,不必将lambda运算符左侧指定的形参名称用圆括号括起来,例如,下面是一种编写lambda表达式的有效方式:
n -> (n%2)==0
(2)函数式接口
如前所述,函数式接口是指仅指定了一个抽象方法的接口,在继续讲解之前,先回忆一下之前介绍过并非所有的接口方法都是抽象方法.从JDK8开始,可以为接口声明的方法指定一个或多个默认方法,默认方法是非抽象方法.两者都是静态的接口方法.因此,只有当未指定默认实现时,接口方法才是抽象方法.这意味着函数式接口可以包含默认方法和静态方法,但在所有情况下,他必须包含一个且仅包含一个抽象方法.因为非默认方法和非静态的接口方法隐式的是抽象方法,所以没有必要使用abstract修饰符.
下面是函数式接口的一个例子:
interface MyValue
{
    double getValue();
}
在本例中,getValue()方法隐式的是抽象方法,并且是MyValue定义的唯一方法.因此,MyValue是一个函数式接口,其功能由getValue()定义.
如前所述,lambda表达式不是独立执行的,而是构成了一个函数式接口定义的抽象方法的实现,该函数式接口定义了他的目标类型.结果,只有在定义了lambda表达式的目标类型的上下文中,才能使用该表达式.当把一个lambda表达式赋给一个函数式接口的引用时,就创建了一个这样的上下文.其他目标类型上下文包括变量初始化,return语句和方法实参等.
下面通过一个简单示例来说明如何在上下文中使用lambda表达式,首先,声明对函数式接口MyValue的一个引用:
MyValue myValue;
接下来,将一个lambda表达式赋给该接口引用
myVal->()->98.6
这个lambda表达式与getValue()兼容,因为同getValue()一样,他没有形参并返回一个double类型的值.一般而言,函数式接口定义的抽象方法的类型与lambda表达式的类型必须兼容.如果不兼容,就会导致编译时错误.
如你所猜,如果愿意,可以将这两步组合到一条语句中完成,如下所示:
MyValue myVal = ()->98.6
这里的myVal在lambda表达式中被初始化
当目标类型上下文中出现lambda表达式时,会自动创建实现了函数式接口的一个类的实例,函数式接口声明的抽象方法的行为由lambda表达式定义.当通过目标调用该方法时,就会执行lambda表达式.因此,lambda表达式提供了一种将代码片段转换为对象的方法.
在前面的例子中,lambda表达式成了getValue()方法的实现.因此,下面的代码将显示值98.6:
Sysetm.out.println("a console value:"+myVal.getValue())
因为赋给myVal的lambda表达式返回值98.6,所以调用 getValue()方法时返回的值也是98.6.
如果lambda表达式包含一个或多个形参,那么函数式接口中抽象方法的形参的数量也必须相同.例如,下面的函数式接口MyParamValue允许 将值传递给getValue():
interface MyParamValue
{
    double getValue(double v);
}
可以使用这个接口来实现本节前面介绍的返回倒数的lambda表达式,例如:
MyParmValue myPval = (n)->1.0/n;
然后,可以使用myPval,如下所示:
System.out.println("Reciprocal of 4 is "+myPval.getValue(4.0))
这里,getValue()的实现是通过myPval引用的lambda表达式完成,myPval返回实参的倒数.在本例中,向getValue()传入了值4.0,返回值为0.25.
在前面的示例中,还有一点值得注意,注意,没有指定n的类型,不过可以从上下文推断出他的类型 ,在本例中可以从getValue()的形参类型推断出他的类型为double,这与MyParamValue接口中定义的类型一样.也可以显式的指定lambda表达式中的形参的类型,例如,下面对前面的示例进行改写的方法是有效的:
(double n)->1.0/n;
这里,n的类型显式指定为double,通常,没有必要显式指定该类型.
在继续讨论之前,有个重点需要强调一下:为了在目标类型上下文中使用lambda表达式,抽象方法的类型和lambda表达式的类型必须兼容.例如,如果抽象方法指定了两个int类型的形参,那么lambda表达式也必须指定两个形参,其类型要么显式的指定为int类型,要么在上下文中可以被隐式的推断为int类型.总的来说,lambda表达式的形参的类型和数量必须与方法的形参及其返回值的类型兼容.
(3)几个lambda表达式示例
在做了前面的讨论以后,接下来用几个简单的示例来延迟lambda表达式的基本概念.第一个例子将前面的代码放到了一起,组成一个完成的程序,可以运行并体验该程序:
public class test2
{
    public static void main(String args[])
    {
     MyValue myVal;
     
     myVal = ()->98.6;
     System.out.println("a constant  value:"+myVal.getValue());
     
     MyParamValue myPval = (n)->1.0/n;
     System.out.println("Reciprocal of 4 is  "+myPval.getValue(4.0));
     System.out.println("Reciprocal of 8 is  "+myPval.getValue(8.0));
    //myVal = ()->"test";
    }
}
interface MyValue
{
    double getValue();
}
interface MyParamValue
{
    double getValue(double v);
}
程序样本输出如下所示:
a constant value:98.6
Reciprocal of 4 is 0.25
Reciprocal of 8 is 0.125
如前所示,lambda表达式必须与其想要实现的抽象方法兼容,因此,注释掉上面程序中的最后一行代码是非法的.首先,因为String类型的值与double类型不兼容,而getValue()返回类型是double;其次,因为MyParamValue中的getValue(int)需要一个形参,但没有提供这个形参.
函数式接口的一个重要方面是,他可以用在与其兼容的任何lambda表达式中.例如,考虑下面的程序,该程序定义了一个名为NumericTest的函数式接口,其中声明了抽象方法test(),text()方法带有两个int类型的形参并返回一个boolean类型的结果.该方法旨在确定传给他的相关实参是否满足某些条件,并返回测试结果.在main()方法中,可以使用lambda表达式创建3种不同的测试.第一种测试第一个实参是否可以被第二个实参平分;第二种测试第一个实参是否小于第二个实参;第三种在两个实参的绝对值相等的情况下返回true.注意,完成这些测试的lambda表达式都带有两个形参并且返回boolean类型的值.当然,这是必须的,因为test()方法也带有两个形参且也返回boolean类型的值.
public class test2
{
    public static void main(String args[])
    {
     NumberTest isFactor = (n,d)->(n%d)==0;
     
     if(isFactor.test(10, 2))
         System.out.println("2 is a factor of  10");
     if(!isFactor.test(10, 3))
         System.out.println("3 is not a factor of  10");
     
     NumberTest lessThan = (n,m)->(n<m);
     
     if(lessThan.test(2, 10))
         System.out.println("2 is less than 10");
     if(!lessThan.test(10,2))
         System.out.println("10 is not less than  2");
     
     NumberTest absEqual = (n,m)->(n < 0 ? -n :  0) == (m < 0 ? -m : m);
     if(absEqual.test(4, -4))
         System.out.println("Absolute values of 4  and -4 are equal");
     if(!absEqual.test(4, 5))
         System.out.println("Absolute values of 4  and 5 are not equal");
    }
}
interface NumberTest
{
    boolean test(int n,int m);
}
下面是该程序的输出
2 is a factor of 10
3 is not a factor of 10
2 is less than 10
10 is not less than 2
Absolute values of 4 and 5 are not equal
如程序中所示,因为三个lambda表达式与test()方法兼容,所以可以通过NumberTest引用来执行.事实上,没有必要使用三个独立的NumberTest引用变量,因为对于这三种测试使用一个即可.例如,可以创建变量myTest,然后使用他来依次引用每个测试,如下所示:
    public static void main(String args[])
    {
     NumberTest myTest;
     myTest = (n,d)->(n%d)==0;
     
     if(myTest.test(10, 2))
         System.out.println("2 is a factor of  10");
     if(!myTest.test(10, 3))
         System.out.println("3 is not a factor of  10");
     
     myTest = (n,m)->(n<m);
     
     if(myTest.test(2, 10))
         System.out.println("2 is less than 10");
     if(!myTest.test(10,2))
         System.out.println("10 is not less than  2");
     
     myTest = (n,m)->(n < 0 ? -n : 0) == (m < 0 ?  -m : m);
     if(myTest.test(4, -4))
         System.out.println("Absolute values of 4  and -4 are equal");
     if(!myTest.test(4, 5))
         System.out.println("Absolute values of 4  and 5 are not equal");
    }
当然,如果同传统程序那样,使用三个不同的引用变量isFactor,lessThan和absQual,那么他们所引用的lambda表达式将十分清楚.
在前面的程序中还有一点值得一提,注意为lambda表达式指定两个形参的方式,例如,下面的lambda表达式判定一个数是否是另一个数的因子:
(n,d) -> (n%d) == 0;
注意,n和d之间用逗号分隔开.一般而言,每当需要一个以上的参数时,就在lambda运算符的左侧,使用一个带括号的参数列表来指定参数,参数之间使用逗号分隔开.
虽然前面的示例中,使用基本类型作为函数式接口定义的抽象方法的形参类型和返回类型,但事实上并不存在这种限制.
例如,下面的程序声明了一个名为StringTest的函数式接口,其中包含test()方法带有两个String类型的形参并返回一个boolean类型的值.因此,可以使用该方法来测试与字符串相关的一些条件.下面的lambda表达式测试一个字符串是否是另一个字符串的子串:
public class test2
{
    public static void main(String args[])
    {
     StringTest isIn = (a,b)->a.indexOf(b) != -1;
     String str = "this is a test";
     System.out.println("testing string:"+str);
     
     if(isIn.test(str, "is a"))
         System.out.println("is a found");
     
     if(!isIn.test(str, "xyz"))
         System.out.println("xyz not found");
    }
}
interface StringTest
{
    boolean test(String n,String m);
}
程序输出:
testing string:this is a test
is a found
xyz not found
注意,lambda表达式中使用String类定义的indexOf()方法,来测试一个字符串是否是另一个字符串的子串.该程序正常运行,因为通过类型推断可以确定形参a和b的类型为String,因此,可以对a调用String类的方法.
专家解答:
问:在前面提到过,如果需要,可以在lambda表达式中显式声明形参的类型,在lambda表达式需要两个或两个以上的形参的情况下,必须指定所有形参的类型吗?可以对一个或多个形参进行类型推断吗?
答:在需要显式声明形参的类型时,列表中所有形参的类型都必须已声明.例如,下面的lambda表达式是合法的:
(int n,int d) -> (n%d) == 0
下面是非法的:
(int n,d) -> (n%d) == 0
(n,int d) -> (n%d) == 0