lambda表达式
Java一直处在发展和演化的过程中。自最早的1.0版本以来,已经增加了许多功能。其中有两个最为突出,对Java语言产生了深远的影响,从根本上改变了代码的编写方式。第一个就是JDK5增加的泛型,第二个则是lambda表达式。
lambda表达式(及其相关功能)是JDK8新增的功能,它们显著增强了Java,原因有两点。首先,它们增加了新的语法元素,使Java语言的表达能力得以提升,并流线化了一些常用结构的实现方式。其次,lambda表达式的加入也导致API库中增加了新的功能,包括利用多核环境的并行处理功能(尤其是在处理for-each风格的操作时)变得更加容易,以及支持对数据执行管道操作的新的流API。lambda表达式的引入也催生了其他新的Java功能,包括 默认方法和方法引用。默认方法允许定义接口方法的默认行为,而方法引用允许引用而不执行方法。
除了带给语言的好处,还有另外一个原因让lambda表达式成为Java的重要新增功能。lambda表达式已经成为计算机语言设计的重点关注对象。例如C#和C++等语言都添加了lambda表达式。JDK8包含它们,以帮助使Java继续保持活力和创新性。
1.1 lambda表达式简介
对于理解lambda表达式的Java实现,有两个结构十分关键。第一个就是lambda表达式自身,第二个是函数式接口。下面首先为这两个结构下一个定义:
lambda表达式本质上就是一个匿名(即未命名)方法。但是,这个方法不是独立执行的。而是用于实现由函数式接口定义的另一个方法。因此,lambda表达式会导致产生一个匿名类。lambda表达式也常被称为闭包。
函数式接口是仅包含一个抽象方法的接口。一般来说,这个方法指明了接口的目标用途。因此,函数式接口通常表示单个动作。例如,标准接口Runnable是一个函数式接口,因为它只定义了一个方法run()。因此,run()定义了Runnable的动作。此外函数式接口定义了lambda表达式的目标类型。特别注意:lambda表达式只能用于其目标类型已被指定的上下文中。另外,函数式接口有时被称为SAM类型,意思是单抽象方法(Single Abstract Method)。
注意
函数式接口可以指定Object定义的任何公有方法,例如equals(),而不影响其作为“函数式接口”的状态。Object的公有方法被视为函数式接口的隐式成员,因为函数式接口的实例会默认自动实现它们。
1.1.1 lambda表达式的基础知识
lambda表达式在Java语言中引入了一个新的语法元素和操作符。这个操作符是->,有时被称为lambda操作符或箭头操作符。它将lambda表达式分成两个部分。左侧指定了lambda表达式需要的所有参数(如果不需要参数,则使用空的参数列表)。右侧中了lambda体,即lambda表达式要执行的动作。在用语言描述时,可以把->表达成“成了”或“进入”。
Java定义了两种lambda体。一种包含单独一个表达式,另一种包含一个代码块。首先讨论第一种类型的lambda表达式。
在继续讨论之前,看几个lambda表达式的例子会有帮助。首先看一个可能是最简单的lambda表达式的例子。它的计算结过是一个常量值,如下所示:
() -> 123.45
这个lambda表达式没有参数,所以参数列表为空。它返回常量值123.45。因此,这个表达式的作用类似于下面的方法:
double myMeth(){return 123.45;}
当然,lambda表达式定义的方法没有名称。
下面给出了一个更有意思的lambda表达式
() -> Math.random() * 100
这个lambda表达式使用Math.random()获得一个伪随机数,将其乘以100,然后返回结果。这个lambda表达式也不需要参数。
当lambda表达式需要参数时,需要在操作符左侧参数列表中加以指定。下面时一个简单的例子:
(n) -> (n % 2)==0
如果参数n的值是偶数,这个lambda表达式会返回true。尽管可以显式指定参数的类型,例如本例中的n,但是通常不需要这么做,因为很多时候,参数的类型是可以推断出来的。与命名方法一样,lambda表达式可以指定需要用到的任意数量的参数。
1.1.2 函数式接口
如前所述,函数式接口是仅指定了一个抽象方法的接口。我们可能认为所有接口方法都隐式地是抽象方法。在JDK8以前,这么认为没有问题,但是现在情况发生了变化。从JDK8开始,可以为接口声明的方法指定默认行为,即所谓的默认方法。如今,只有当没有指定默认实现时,接口方法才是抽象方法。因为没有指定默认实现的接口方法隐式地是抽象方法,所以没有必要使用abstract修饰符(如果愿意的话,也可以指定该修饰符)。
下面是函数式接口的一个例子:
interface MyNumber{
double getValue();
}
在本例中,getValue()方法隐式地是抽象方法,并且是MyNumber定义的唯一方法。因此,MyNumber是一个函数式接口,其功能由getValue()定义。
如前所述,lambda表达式不是独立执行的,而是构成了一个函数式接口定义的抽象方法的实现,该函数式接口定义了它的目标类型。结果,只有在定义了lambda表达式的目标类型的上下文中,才能使用该表达式。当把一个lambda表达式赋给一个函数式接口引用时,就创建了这样的上下文。其他目标类型上下文包括变量初始化、return语句和方法参数等。
下面通过一个例子来说明如何在参数上下文中使用lambda表达式。首先,声明对函数式接口MyNumber的一个引用:
//Create a reference to a MyNumber instance.
MyNumber myNum;
接下来,将一个lambda表达式赋给该接口引用:
//Use a lambda in an assignment content.
myNum = () -> 123.45;
当目标类型上下文中出现lambda表达式时,会自动创建实现了函数式接口的一个类的实例,函数式接口声明的抽象方法的行为由lambda表达式定义。当通过目标调用该方法时,就会执行lambda表达式。因此,lambda表达式成了getValue()方法的实现。因此,下面的代码将显示123.45:
//Call getValue(),which is implemented by previously assigned
//lambda expression.
System.out.println(myNum.getValue());
因为赋给myNum的lambda表达式返回值123.45,所以调用getValue()方法时返回的值也是123.45。
为了在目标类型上下文中使用lambda表达式,抽象方法的类型和lambda表达式的类型必须兼容。例如,如果抽象方法指定了两个int类型的参数,那么lambda表达式也必须指定两个参数,其类型要么被显示指定为int类型,要么在上下文可以被隐式的推断为int类型。总的来说lambda表达式的参数的类型和数量必须与方法的参数兼容;返回类型必须兼容;并且lambda表达式可能抛出的异常必须能被方法接受。
1.1.3 几个lambda表达式示例
下面用几个简单的示例来演示lambda表达式的基本概念。第一个例子将前面的代码放到了一起:
//Demonstrate a simple lambda expression.
//A functional interface.
interface MyNumber {
double getValue();
}
public class lambdaDemo {
public static void main(String[] args) {
MyNumber myNum;//declare an interface reference
//Here,the lambda expression is simply a constant expression.
//When it is assigned to MyNum,a class instance is
//constructed in which the lambda expression implements
//the getValue() method in MyNumber.
myNum = () -> 123.45;
//Call getValue(),which is provided by previously assigned
//lambda expression.
System.out.println("A fixed value: " + myNum.getValue());
//Here,a more complex expression is used.
myNum = () -> Math.random() * 100;
//These call the lambda expression in the previous line.
System.out.println("A random value: " + myNum.getValue());
System.out.println("Another random value: " + myNum.getValue());
//A lambda expression must be compatible with the method
//defined by functional interface.Therefore,this won't work:
//myNum=()->"123.45";
/**
* 输出:
* A fixed value: 123.45
* A random value: 55.28584000341114
* Another random value: 94.98774340059411
*/
}
}
如前所述lambda表达式必须与其想要的实现的抽象方法兼容。因此,上面程序中最后注释掉的一行时非法的,因为String类型的值与double类型不兼容,而getValue()的返回类型是double。
下面的例子演示了如何使用带参数的lambda表达式:
//Another functional interface.
interface NumericTest {
boolean test(int n);
}
//Demonstrate a lambda expression that takes a parameter.
class lambdaDemo2 {
public static void main(String[] args) {
//A lambda expression that tests if a number is even.
NumericTest isEven = (n) -> n % 2 == 0;
if (isEven.test(10)) System.out.println("10 is Even");
if (!isEven.test(9)) System.out.println("9 is not Even");
//Now,use a lambda expression that tests if a number
//is non-negative.
NumericTest isNonNeg = (n) -> n >= 0;
if (isNonNeg.test(1)) System.out.println("1 is non-negative");
if (!isNonNeg.test(-1)) System.out.println("-1 is negative");
/**
* 输出:
* 10 is Even
* 9 is not Even
* 1 is non-negative
* -1 is negative
*/
}
}
这个程序演示了lambda表达式的关键一点,所以需要仔细分析。特别要注意测试奇偶性的lambda表达式,如下所示:
(n) -> (n%2)==0
注意,这里没有指定n的类型。相反,n的类型是从上下文推断出来的。本例中,其类型是从NumericTest接口定义的test()方法参数类型推断出来的,而该参数的类型是int,在lambda表达式中,也可以显式指定参数的类型。例如,下面的写法也是合法的:
(int n) -> (n % 2) == 0
其中,n被显式指定为int类型。通常没有必要显式指定类型,但是在需要的时候是可以指定的。
这个程序演示了关于lambda表达式的另外一个重要的地方:函数式接口引用可以用来执行任何与其兼容的lambda表达式。注意,程序中定义了两个lambda表达式,它们都与函数式接口NumericTest的test()方法兼容。第一个式isEven,用于确定值是否是偶数。第二个是isNonNeg,用于检查值是否为非负值。两种情况下都会测试参数n的值。因为每个lambda表达式都与test()兼容,所以都可以通过NumericTest引用执行。
继续讨论之前,还需要知道另外一点。如果lambda表达式只有一个参数,在操作符的左侧指定该参数时,没有必要使用括号括住该参数的名称。例如,对于程序中使用的lambda表达式,下面这种写法也是合法的:
n -> (n % 2)==0
下一个程序演示了接受两个参数的lambda表达式。这里,lambda表达式测试一个数字是否时另外一个数字的因子:
interface NumericTest2 {
boolean test(int n,int d);
}
class lambdaDemo3 {
public static void main(String[] args) {
//This lambda expression determines if one number is
//a factor of another.
NumericTest2 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");
/**
* 输出
* 2 is a factor of 10
* 3 is not a factor of 10
*/
}
}
在这个版本中,test()方法指定了两个参数。因此,与test()方法兼容的lambda表达式也必须指定两个参数。注意指定这种lambda表达式的方式:
(n,d) -> (n % d) == 0
两个参数n和d在参数列表中指定,并使用逗号分隔开。可以把这个例子推而广之。每当需要一个以上的参数时,就在lambda操作符的左侧,使用一个带括号的参数列表指定参数,参数之间使用逗号隔开。
对于lambda表达式中的多个参数,有一点十分重要:如果需要显式声明一个参数的类型,那么必须为所有的参数声明类型。例如,下面的代码是合法的:
(int n,int d) -> (n % d) == 0
但下面的不合法:
(int n,d) -> (n % d) == 0
1.2 块lambda表达式
前面例子中显示的lambda体只包含单个表达式。这种类型的lambda体被称为表达式体,具有表达式体的lambda表达式有时候被称为表达式lambda。在表达式体中,操作符右侧的代码必须包含单独一个表达式。尽管表达式lambda十分有用,但是有时候会要求使用一个以上的表达式。为了处理这类情况,Java支持另外一种类型的lambda表达式,其中操作符右侧的代码可以由一个代码块构成,其中可以包含多条语句。这种类型的lambda体被称为块体。具有块体的lambda表达式有时候被称为块lambda。
块lambda扩展了lambda表达式内部可以处理的操作类型,因为它允许lambda体包含多条语句。例如,在块lambda中,可以声明变量、使用循环、指定if和switch语句、创建嵌套代码块等。创建块lambda很容易,只需要使用花括号包围lambda体,就像创建其他语句块一样。
除了允许多条语句,块lambda的使用方法与刚才讨论过的表达式lambda十分相似。但是,也有一个重要区别:在块lambda中必须显式使用return语句来返回值。必须这么做,因为块lambda体代表的不是单独一个表达式。
下面这个例子使用块lambda来计算并返回一个int类型值的阶乘:
//A block lambda that computes the factorial of an int value.
interface NumericFunc {
int func(int n);
}
class BlocklambdaDemo {
public static void main(String[] args) {
//This block lambda computes the factorial of an int value.
NumericFunc factorial = (n) -> {
int result = 1;
for (int i = 1; i <= n; i++) {
result = i * result;
}
return result;
};
System.out.println("The factorial of 3 is " + factorial.func(3));
System.out.println("The factorial of 5 is " + factorial.func(5));
/**
* 输出:
* The factorial of 3 is 6
* The factorial of 5 is 120
*/
}
}
在程序中,注意块lambda声明了变量result,使用了一个for循环,并且具有一条return语句。在块lambda体内,这么做是合法的。块lambda体在本质上与方法体类似。另外一点:当lambda表达式中出现return语句时,只是从lambda体返回,而不会导致包围lambda体的方法返回。
下面的程序给出了块lambda的另外一个例子。这里使用块lambda颠倒了一个字符串中的字符:
//A block lambda that reverses the characters in a string.
interface StringFunc {
String func(String n);
}
class BlocklambdaDemo2 {
public static void main(String[] args) {
//This block lambda reverses the characters in a string.
StringFunc reverse = (str) -> {
String result = "";
int i;
for (i = str.length() - 1; i >= 0; i--)
result += str.charAt(i);
return result;
};
System.out.println("lambda reversed is " + reverse.func("lambda"));
System.out.println("Expression reversed is " + reverse.func("Expression"));
/**
* 输出:
* lambda reversed is adbmal
* Expression reversed is noisserpxE
*/
}
}
在这个例子中,函数式接口StringFunc声明了func()方法。该方法接受一个String类型的参数,并返回一个String类型的结果。因此,在reverse lambda表达式中,推断出str的类型为String。注意,对str调用了charAt()方法。之所以能够这么调用,是因为推断出了str的类型是String。
1.3 泛型函数式接口
lambda表达式自身不能指定类型参数。因此,lambda表达式不能是泛型(当然,由于存在类型推断,所有lambda表达式都展现出一些类似泛型的特征)。然而,与lambda表达式关联的函数式接口可以是泛型。此时,lambda表达式的目标类型部分由声明函数式接口引用时指定的参数类型决定。
为了理解泛型函数式接口的值,考虑这样的情况。前一节的两个示例使用两个不同的函数式接口,一个叫做NumericFunc,另一个叫做StringFunc。但是,这两个接口都定义了一个叫做func()的方法,该方法接受一个参数,返回一个结果。对于第一个接口,func()方法的参数类型和返回类型为int。对于第二个接口,func()方法的参数和返回类型是String。因此,两个方法的唯一区别是它们需要的数据的类型。相较于使用两个函数式接口,它们的方法只是在需要的数据类型方面存在区别,也可以只声明一个泛型接口来处理这两种情况。下面的程序演示了这种方法;
//A generic functional interface.
interface SomeFunc<T> {
T func(T t);
}
public class GenericFunctionalInterfaceDemo {
public static void main(String[] args) {
//Use a String-based version of SomeFunc.
SomeFunc<String> reverse = (str) -> {
String result = "";
int i;
for (i = str.length() - 1; i >= 0; i--)
result += str.charAt(i);
return result;
};
System.out.println("lambda reversed is " + reverse.func("lambda"));
System.out.println("Expression reversed is " + reverse.func("Expression"));
//Now,use an Integer-based version of SomeFunc.
SomeFunc<Integer> factorial = (n) -> {
int result = 1;
for (int i = 1; i <= n; i++)
result = i * result;
return result;
};
System.out.println("The factorial of 3 is " + factorial.func(3));
System.out.println("The factorial of 5 is " + factorial.func(5));
/**
* 输出
* lambda reversed is adbmal
* Expression reversed is noisserpxE
* The factorial of 3 is 6
* The factorial of 5 is 120
*/
}
}
在程序中,泛型函数式接口SomeFunc的声明如下所示:
interface SomeFunc<T>{
T func(T t);
}
其中,T指定了func()函数的返回类型和参数类型。这意味着它与任何接受一个参数,并返回一个相同类型的值的lambda表达式兼容。
SomeFunc接口用于提供对两种不同类型的lambda表达式的引用。第一种表达式使用String类型,第二种表达式使用Integer类型。因此,同一个函数式接口可以引用reverse lambda表达式和factorial lambda表达式。区别仅在于传递给SomeFunc的类型参数。
1.4 作为参数传递lambda表达式
如前所述,lambda表达式可以用在任何提供了目标类型的上下文中。一种情况就是作为参数传递lambda表达式。事实上,这是lambda表达式的一种常见用途。另外,这也是lambda表达式的一种强大用途,因为可以将可执行代码作为参数传递给方法。这极大地增强了Java的表达力。
为了将lambda表达式作为参数传递,接受lambda表达式的参数的类型必须是与该lambda表达式兼容的函数式接口的类型。虽然使用lambda表达式作为参数十分直观,但是使用例子进行演示仍然会有帮助。下面的程序演示了这个过程:
//Use lambda expressions as an argument to a method.
interface StringFunc {
String func(String n);
}
class lambdasAsArgumentsDemo {
//This method has a functional interface as the type of
//its first parameter.Thus,it can be passed a reference to
//any instance of that interface,including the instance created
//by a lambda expression.
//The second parameter specifies the string to operate on.
static String stringOp(StringFunc sf, String s) {
return sf.func(s);
}
public static void main(String[] args) {
String inStr = "lambdas add power to Java";
String outStr;
System.out.println("Here is input string: " + inStr);
//Here,a simple expression lambda that uppercases a string
//is passed to stringOP().
outStr = stringOp((str) -> str.toUpperCase(), inStr);
System.out.println("The string in uppercase: " + outStr);
//This passes a block lambda that removes spaces.
outStr = stringOp((str) -> {
String result = "";
int i;
for (i = 0; i < str.length(); i++)
if (str.charAt(i) != ' ')
result += str.charAt(i);
return result;
}, inStr);
System.out.println("The string with space removed: " + outStr);
//Of course,it is also possible to pass a StringFunc instance
//create by earlier lambda expression.For example,
//instance of StringFunc.
StringFunc reverse = (str) -> {
String result = "";
int i;
for (i = str.length() - 1; i >= 0; i--)
result += str.charAt(i);
return result;
};
//Now,reverse can be passed as the first parameter to stringOp()
//since it refers to a StringFunc object.
System.out.println("This string reversed: " + stringOp(reverse, inStr));
}
}
在该程序中,首先注意stringOp()方法。它有两个参数,第一个参数的类型是StringFunc,而StringFunc是一个函数式接口。因此,这个参数可以接受对任何StringFunc实例的引用,包括由lambda表达式创建的实例。stringOp()的第二个参数是String类型,也就是要操作的字符串。
接下来,注意对stringOp()的第一次调用,如下所示:
outStr = stringOp((str)->str.toUpperCase(),inStr);
这里,传递了一个简单的表达式lambda作为参数。这会创建函数式接口StringFunc的一个实例,并把对该实例的一个引用传递给stringOp()方法的第一个参数。这就把嵌入在一个类实例中的lambda代码传递给了方法。目标类型上下文由参数的类型决定。因此lambda表达式与该类兼容,调用是合法的。在方法调用内嵌入简单的lambda表达式,比如刚才展示的这种,通常是一种很方便的技巧,尤其是当lambda表达式只使用一次时。
接下来,程序向stringOp()传递了一个块lambda。这个lambda表达式删除字符串中的空格,如下所示:
outStr = stringOp((str) -> {
String result = "";
int i;
for (i = 0; i < str.length(); i++)
if (str.charAt(i) != ' ')
result += str.charAt(i);
return result;
}, inStr);
虽然这里使用了一个块lambda,但是传递过程与刚才讨论的传递简单的表达式lambda相同,但是,在本例中,我们可能发现语法有些笨拙。
当块lambda看上去特别长,不适合嵌入到方法调用中时,很容易把块lambda赋给一个函数式接口变量,正如前面的几个例子中所做的那样。然后,可以简单地把该引用传递给方法。本程序最后的部分演示了这种做法。在那里,定义了一个颠倒字符串的块lambda,然后把该块lambda赋给reverse。reverse是一个对StringFunc实例的引用。因此,可以传递reverse作为stringOp()方法的第一个参数。然后,程序调用stringOp()方法,并传入reverse和要操作的字符串。因为计算每个lambda表达式得到的实例是StringFunc实现,所以可以用作stringOp()方法的第一个参数。
最后一点:除了变量初始化、赋值和参数传递以外,以下这些也构成了目标类型上下文:类型转换、?运算符、数组初始化器、return语句以及lambda表达式自身。
1.5 lambda表达式与异常
lambda表达式可以抛出异常。但是,如果抛出经检查的异常,该异常就必须与函数式接口的抽象方法的throws子句中列出的异常兼容。下面的例子演示了这个事实。它计算一个double数组的平均值。但是,如果传递了长度为0的数组,就会抛出自定义异常EmptyArrayException。从示例可以看出,DoubleNumericArrayFunc函数式接口内声明的func()方法的throws子句列出了此异常。
//Throw an exception from a lambda exception.
interface DoubleNumericArrayFunc {
double func(double[] n) throws EmptyArrayException;
}
class EmptyArrayException extends Exception{
EmptyArrayException(){
super("Array Empty");
}
}
public class lambdaExceptionDemo {
public static void main(String[] args) throws EmptyArrayException {
double[] values = {1.0, 2.0, 3.0, 4.0};
//This block lambda computes the average of an array of doubles
DoubleNumericArrayFunc average = (n) -> {
double sum = 0;
if (n.length == 0)
throw new EmptyArrayException();
for (int i = 0; i < n.length; i++)
sum += n[i];
return sum / n.length;
};
System.out.println("The average is " + average.func(values));
//This causes an exception to be thrown.
System.out.println("The average is " + average.func(new double[0]));
}
}
对average.func()的第一次调用返回了值2.5。在第二次调用中,由于传递了一个长度为0的数组,EmptyArrayException异常被抛出。记住,在func()方法中包含throws子句是必要的。如果不这么做,那么由于lambda表达式不再与func()兼容,程序将无法通过编译。
这个示例演示了关于lambda表达式的另外一个重要的地方。注意,函数式接口DoubleNumericArrayFunc的func()方法指定的参数是数组。然而,lambda表达式的参数是n,而不是n[]。回忆一下,lambda表达式的参数类型将从目标上下文推断得出。在这里,目标上下文是double[],所以n的类型将会是double[]。没有必要指定n。将参数显式地声明为double[] n是合法的,但是在本例中这么做不会有什么好处。
1.6 lambda表达式和变量捕获
在lambda表达式中,可以访问其外层作用域内定义的变量。例如,lambda表达式可以使用其外层类定义的实例或静态变量。lambda表达式也可以显示或隐式地访问this变量,该变量引用lambda表达式外层类的调用实例。因此,lambda表达式可以获取或设置外层类的实例或静态变量的值,以及调用其外层类定义的方法。
但是,当lambda表达式使用其外层作用域内定义的局部变量时,会产生一种特殊的情况,称为变量捕获。在这种情况下,lambda表达式只能使用实质上final的局部变量。实质上final的变量是指在第一次赋值以后,值不再发生变化的变量。没有必要显式地将这种变量声明为final,不过那样做也不是错误(外层作用域的this参数自动是实质上final的变量,lambda表达式没有自己的this参数)。
lambda表达式不能修改外层作用域内的局部变量,理解这一点很重要。修改局部变量会移除其实质上的final状态,从而使捕获该变量变得不合法。
下面的程序演示了实质上final的局部变量和可变局部变量的区别:
//An example of capturing a local variable from the enclosing scope.
interface MyFunc {
int func(int n);
}
class VarCapture {
public static void main(String[] args) {
//A local variable that can be captured.
int num = 10;
MyFunc mylambda = (n) -> {
//This use of num is OK.It does not modify num.
int v = num + n;
//However,the following is illegal because it attempts
//to modify the value of num.
// num++;
return v;
};
//The following line would also cause an error,because
//it would remove the effectively final status from num.
//num=9;
}
}
正如注释所指出的,num实质上是final变量,所以可以在mylambda内使用。但是,如果修改了num,不管是在lambda表达式内还是表达式外,num就会丢失其实质上final的状态。这会导致发生错误,程序将无法通过编译。
需要重点强调,lambda表达式可以使用和修改其调用类的实例变量,只是不能使用其外层作用域内的局部变量,除非该变量实质上是final变量。
1.7 方法引用
有一个重要的特性与lambda表达式相关,叫做方法引用。方法引用提供了一种引用而不执行方法的方式。这种特性与lambda表达式相关,因为它也需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用也会创建函数式接口的一个实例。
方法引用的类型有许多种。我们首先看静态方法的方法引用。
1.7.1 静态方法的方法引用
要创建静态方法引用,需要使用下面的一般语法:
ClassName::methodName
注意,类名与方法名之间使用双冒号分隔开。::是JDK8新增的一个分隔符,专门用于此目的。在与目标类型兼容的任何地方,都可以使用这个方法引用。
下面的程序演示了一个静态方法引用:
//Demonstrate a method reference for a static method.
//A functional interface for string operations.
interface StringFunc {
String func(String n);
}
//This class defines a static method called strReverse.
class MyStringOps {
//A static method that reverses a string.
static String strReverse(String str) {
String result = "";
int i;
for (i = str.length() - 1; i >= 0; i--)
result += str.charAt(i);
return result;
}
}
class MethodRefDemo {
//This method has a functional interface as type of
//its first parameter.Thus,it can be passed any instance
//of that interface,including a method reference.
static String stringOp(StringFunc sf, String s) {
return sf.func(s);
}
public static void main(String[] args) {
String inStr = "lambdas add power to Java";
String outStr;
//Here,a method reference to strReverse is passed to stringOp().
outStr = stringOp(MyStringOps::strReverse, inStr);
System.out.println("Original string: " + inStr);
System.out.println("String reversed: " + outStr);
/**
* 输出:
* Original string: lambdas add power to Java
* String reversed: avaJ ot rewop dda sadbmal
*/
}
}
在程序中,特别注意下面这行代码:
outStr = stringOp(MyStringOps::strReverse, inStr);
这里,将对MyStringOps内声明的静态方法strReverse()的引用传递给了stringOp()方法的第一个参数。可以这么做,因为strReverse与StringFunc函数式接口兼容。因此,表达式MyStringOps::strReverse的计算结果为对象引用,其中strReverse提供了StringFunc的func()方法的实现。
1.7.2 实例方法的方法引用
要传递对某个对象的实例方法的引用,需要使用下面的基本语法:
objRef::methodName
可以看到,这种语法与用于静态方法的语法类似,只不过这里使用对象引用而不是类名。下面使用实例方法引用重写了前面的程序:
//Demonstrate a method reference for an instance method.
//A functional interface for string operations.
interface StringFunc {
String func(String n);
}
//Now,this class defines an instance method called strReverse.
class MyStringOps {
String strReverse(String str) {
String result = "";
int i;
for (i = str.length() - 1; i >= 0; i--)
result += str.charAt(i);
return result;
}
}
class MethodRefDemo2 {
//This method has a functional interface as type of
//its first parameter.Thus,it can be passed any instance
//of that interface,including a method references.
static String stringOp(StringFunc sf, String s) {
return sf.func(s);
}
public static void main(String[] args) {
String inStr = "lambdas add power to Java";
String outStr;
//Create a MyStringOps object.
MyStringOps strOps = new MyStringOps();
//Now,a method reference to the instance method strReverse
outStr = stringOp(strOps::strReverse, inStr);
System.out.println("Original string: " + inStr);
System.out.println("String reversed: " + outStr);
/**
* 输出:
* Original string: lambdas add power to Java
* String reversed: avaJ ot rewop dda sadbmal
*/
}
}
这个程序产生的输出与上一个版本相同。
在程序中,注意strReverse()现在是MyStringOps的一个实例方法。在main()方法内,创建了MyStringOps的一个实例strOps。在调用stringOp时,这个实例用于创建对strReverse的引用,如下所示:
outStr = stringOp(strOps::strReverse, inStr);
在本例中,对strOps对象调用strReverse()方法。
也可以指定一个实例方法,使其能用于给定类的任何对象而不仅指定对象。此时,需要向下面这样创建方法引用:
ClassName::instanceMethodName
这里使用了类的名称,而不是具体的对象,尽管指定的是实例方法。使用这种形式时,函数式接口的第一个参数匹配调用对象,第二个参数匹配方法指定的参数。下面是一个例子。
它定义了一个方法counter(),用于统计某个数组中,满足函数式接口MyFunc的func()方法定义的条件的对象个数。本例中,它统计HighTemp类的实例个数:
//Use an instance method reference with different objects.
//A functional interface that takes two reference arguments
//and returns a boolean result
interface MyFunc<T> {
boolean func(T v1,T v2);
}
//A class that stores the temperature high for a day.
class HighTemp {
private int hTemp;
HighTemp(int ht) {
hTemp = ht;
}
//Return true if the invoking HighTemp object has the same
//temperature an ht2
boolean sameTemp(HighTemp ht2) {
return hTemp == ht2.hTemp;
}
//Return true if the invoking HighTemp object has a temperature
//that is less than ht2
boolean lessThanTemp(HighTemp ht2) {
return hTemp < ht2.hTemp;
}
}
class InstanceMethWithObjectRefDemo {
//A method that returns the number of occurrences
//of an object for which some criteria,as specified by
//the MyFunc parameter,is true.
static <T> int counter(T[] vals, MyFunc<T> f, T v) {
int count = 0;
for (int i = 0; i < vals.length; i++)
if (f.func(vals[i], v)) count++;
return count;
}
public static void main(String[] args) {
int count;
//Create an array of HighTemp objects.
HighTemp[] weekDayHighs = {new HighTemp(89), new HighTemp(82),
new HighTemp(90), new HighTemp(89),
new HighTemp(89), new HighTemp(91),
new HighTemp(84), new HighTemp(83)};
//Use counter() with arrays of the class HighTemp.
//Notice that a reference to the instance method
//sameTemp() is passed as the second argument.
count = counter(weekDayHighs, HighTemp::sameTemp, new HighTemp(89));
System.out.println(count + " days had a high of 89");
//Now,create and use another array of HighTmep objects.
HighTemp[] weekDayHighs2 = {new HighTemp(32), new HighTemp(12),
new HighTemp(24), new HighTemp(19),
new HighTemp(18), new HighTemp(12),
new HighTemp(-1), new HighTemp(13)};
count = counter(weekDayHighs2, HighTemp::sameTemp, new HighTemp(12));
System.out.println(count + " days had a high of 12");
//Now,use lessThanTemp() to find days when temperature was less
//than a specified value.
count = counter(weekDayHighs, HighTemp::lessThanTemp, new HighTemp(89));
System.out.println(count + " days had a high less than 89");
count = counter(weekDayHighs2, HighTemp::lessThanTemp, new HighTemp(19));
System.out.println(count + " days had a high less than 19");
/**
* 输出:
*3 days had a high of 89
* 2 days had a high of 12
* 3 days had a high less than 89
* 5 days had a high less than 19
*/
}
}
在程序中,注意HighTemp有两个实例方法:sameTemp()和lessThanTemp(),如果两个HighTemp对象包含相同的温度,sameTemp()方法返回true。如果调用对象的温度小于被传递的对象的温度,lessThanTemp()方法返回true。每个方法都有一个HighTemp类型的参数,并且都返回boolean结果。因此,这两个方法都与MyFunc函数式接口兼容,因为调用对象类型可以映射到func()的第一个参数,传递的实参可以映射到func()的第二个参数。因此,当下面的表达式:
HighTemp::sameTemp
被传递给counter()方法时,会创建函数式接口MyFunc的一个实例,其中第一个参数的参数类型就是实例方法的调用对象的类型,也就是HighTemp。第二个参数的类型也是HighTemp,因为这是sameTemp()方法的参数。对于lessThanTemp(),这也是成立的。
另外一点,通过使用super,可以引用方法的超类版本,如下所示:
super::name
方法的名称由name指定。
1.7.3 泛型中的方法引用
在泛型类和/或泛型方法中,也可以使用方法引用。例如,分析下面的程序:
//Demonstrate a method reference to a generic method
//declared inside a non-generic class.
//A functional interface that operates on an array
//and a value,and returns an int result.
interface MyFunc<T> {
int func(T[] vals, T v);
}
//This class defines a method called countMatching() that
//returns the number of items in an array that are equal
//to a specified value.Notice that countMatching()
//is generic,but MyArrayOps is not.
class MyArrayOps {
static <T> int countMatching(T[] vals, T v) {
int count = 0;
for (int i = 0; i < vals.length; i++)
if (vals[i] == v) count++;
return count;
}
}
class GenericMethodRefDemo {
//This method has the MyFunc functional interface an the
//type of its first parameter.The other two parameters
//receive an array and a value,both of type T.
static <T> int myOp(MyFunc<T> f, T[] vals, T v) {
return f.func(vals, v);
}
public static void main(String[] args) {
Integer[] vals = {1, 2, 3, 4, 2, 3, 4, 4, 5};
String[] strs = {"One", "Two", "Three", "Two"};
int count;
count = myOp(MyArrayOps::<Integer>countMatching, vals, 4);
System.out.println("vals contains " + count + " 4s");
count = myOp(MyArrayOps::<String>countMatching, strs, "Two");
System.out.println("strs contains " + count + " Twos");
/**
* 输出:
* vals contains 3 4s
* strs contains 2 Twos
*/
}
}
在程序中,MyArrayOps是非泛型类,包含泛型方法countMatching()。该方法返回数组中与指定值匹配的元素的个数。注意这里如何指定泛型类型参数。例如,在main()方法中,对countMatching()方法的第一次调用如下所示:
count = myOp(MyArrayOps::<Integer>countMatching,vals,4);
这里传递了类型参数Integer。注意,参数传递发生在::的后面。这种语法可以推广。当把泛型方法指定为方法引用时,类型参数出现在::之后、方法名称之前。但是,需要指出的是,在这种情况(和其他许多种情况)下,并非必须显式指定类型参数,因为类型参数会被自动推断得出。对于指定泛型类型的情况,类型参数位于类名的后面、::前面。
前面的例子显示了使用方法引用的机制,但是没有展现它们真正的优势。方法引用能够一展拳脚的一处地方是在与集合框架一起使用时。这里包含一个简单而有效的例子,使用方法引用来帮助确定集合中最大的元素。
找到集合中最大元素的一种方法是使用Collections类定义的max()方法。对于这里使用的max()版本,必须传递一个集合引用,以及一个实现了Comparator<T>接口的对象的实例。Comparator<T>接口指定如何比较两个对象,它只定义了抽象方法compare(),该方法接受两个参数,其类型均为要比较的对象的类型。如果第一个参数大于第二个参数,该方法返回一个正数;如果两个参数相等,返回0;如果第一个参数小于第二个参数,返回一个负数。
过去,要在max()方法中使用用户定义的对象,必须首先通过一个类显示实现Comparator<T>接口,然后创建该类的一个实例,通过这种方法获得Comparator<T>接口的一个实例。然后,把这个实例作为比较器传递给max()方法。在JDK8中,现在可以简单地将比较方法的引用传递给max()方法,因为这将自动实现比较器。下面的简单示例显示了这个过程。该例创建MyClass对象的一个ArrayList,然后找出列表中具有最大值的对象(这是由比较方法定义的):
class MyClass {
private int val;
MyClass(int v) {
val = v;
}
int getVal() {
return val;
}
}
class UseMethodRef {
//A compare() method compatible with the one defined by Comparator<T>.
static int compareMC(MyClass a,MyClass b){
return a.getVal()-b.getVal();
}
public static void main(String[] args) {
ArrayList<MyClass> al = new ArrayList<MyClass>();
al.add(new MyClass(1));
al.add(new MyClass(4));
al.add(new MyClass(2));
al.add(new MyClass(9));
al.add(new MyClass(3));
al.add(new MyClass(7));
//Find the maximum value in al using the compareMC() method.
MyClass maxValObj = Collections.max(al,UseMethodRef::compareMC);
System.out.println("Maximum value is: "+maxValObj.getVal());
/**
* 输出:
* Maximum value is: 9
*/
}
}
在程序中,注意MyClass既没有定义自己的比较方法,也没有实现Comparator接口。但是,通过调用max()方法,仍然可以获得MyClass对象列表中的最大值,这是因为UseMethodRef定义了静态方法compareMC(),它与Comparator定义的compare()方法兼容。因此,没有必要显式地实现Comparator接口并创建其实例。
1.8 构造函数引用
与创建方法引用相似,可以创建构造函数的引用。所需语法的一般形式如下所示:
classname::new
可以把这个引用赋值给定义的方法与构造函数兼容的任何函数式接口的引用。下面是一个例子:
//Demonstrate a Constructor reference.
//MyFunc is a functional interface whose method returns
//a MyClass reference.
interface MyFunc {
MyClass func(int n);
}
class MyClass {
private int val;
//This constructor takes an argument.
MyClass(int v) {
val = v;
}
//This is the default constructor.
MyClass() {
val = 0;
}
//...
int getVal() {
return val;
}
}
class ConstructorRefDemo {
public static void main(String[] args) {
//Create a reference to the MyClass constructor.
//Because func() in MyFunc takes an argument,new
//refers to the parameterized constructor in MyClass,
//not the default constructor.
MyFunc myClassCons = MyClass::new;
//Create an instance of MyClass via that constructor reference.
MyClass mc = myClassCons.func(100);
//Use the instance of MyClass just created
System.out.println("val in mc is "+mc.getVal());
/**
* 输出:
*val in mc is 100
*/
}
}
在程序中,注意MyFunc的func()方法返回MyClass类型的引用,并且有一个int类型的参数,接下来,注意MyClass定义了两个构造函数,第一个构造函数指定一个int类型的参数,第二个构造函数是默认的无参数构造函数。现在,分析下面这样代码:
MyFunc myClassCons = MyClass::new;
这里,表达式MyClass::new创建了对MyClass构造函数的构造函数引用。在本例中,因为MyFunc的func()方法接受一个int类型的参数,所以被引用的构造函数是MyClass(int v),它是正确匹配的构造函数。还要注意,对这个构造函数的引用被赋给了名为myClassCons的MyFunc引用。这条语句执行后,可以使用myClassCons来创建MyClass的一个实例,如下面这行代码所示:
MyClass mc = myClassCons.func(100);
实质上,myClassCons成了调用MyClass(int v)的另一种方式。
创建泛型类的构造函数引用的方法与此相同。唯一区别在于可以指定类型参数。这与使用泛型类创建方法引用相同:只需要在类名后指定类型参数。下面的例子通过修改前一个例子,是MyFunc和MyClass成为泛型类演示了这一点。
//Demonstrate a constructor reference with a generic class.
//MyFunc is now a generic functional interface.
//a MyClass reference.
interface MyFunc<T> {
MyClass<T> func(T n);
}
class MyClass<T> {
private T val;
//This constructor takes an argument.
MyClass(T v) {
val = v;
}
//This is the default constructor.
MyClass() {
val = null;
}
//...
T getVal() {
return val;
}
}
class ConstructorRefDemo2 {
public static void main(String[] args) {
//Create a reference to the MyClass<T> constructor.
MyFunc<Integer> myClassCons = MyClass<Integer>::new;
//Create an instance of MyClass<T> via that constructor reference.
MyClass<Integer> mc = myClassCons.func(100);
//Use the instance of MyClass<T> just created
System.out.println("val in mc is "+mc.getVal());
}
}
这个程序产生的输出与前一个版本相同。区别在于,现在MyFunc和MyClass都是泛型类。因此,创建构造函数引用的代码序列可以包含一个参数类型(不过并不一定总是需要类型参数),如下所示:
MyFunc<Integer> myClassCons = MyClass<Integer>::new;
因为创建myClassCons 时,已经指定了参数类型Integer,所以这里可以将其用来创建一个MyFunc对象,如下一行代码所示:
MyClass<Integer> mc = myClassCons.func(100);
前面的例子演示了如何使用构造函数引用,但是没有人会像前面展示的那样使用构造函数引用,因为这样得不到什么好处。另外,让同一个构造函数相当于具有两个名称会让人产生迷惑。不过,我们可以看到一种更加实际的用法,下面的程序使用了一个静态方法myClassFactory(),这是任意类型的MyFunc对象的工厂。可以使用该方法来创建构造函数与其第一个参数兼容的任意类型的对象。
//Implement a simple class factory using a constructor reference.
interface MyFunc<R,T> {
R func(T n);
}
//A simple generic class.
class MyClass<T> {
private T val;
//A constructor that takes an argument.
MyClass(T v) {
val = v;
}
//The default constructor.This constructor
//is NOT used by this program.
MyClass() {
val = null;
}
//...
T getVal() {
return val;
}
}
//A simple non-generic class.
class MyClass2<T> {
String str;
//A constructor that takes an argument.
MyClass2(String s) {
str = s;
}
//The default constructor.This constructor
//is NOT used by this program.
MyClass2() {
str = "";
}
//...
String getVal() {
return str;
}
}
class ConstructorRefDemo3 {
//A factory method for class objects.The class must
//have a constructor that takes one parameter of type T.
//R specifies the type of object being created.
static <R, T> R myClassFactory(MyFunc<R, T> cons, T v) {
return cons.func(v);
}
public static void main(String[] args) {
//Create a reference to a MyClass constructor.
//In this case,new refers to the constructor that
//takes an argument.
MyFunc<MyClass<Double>, Double> myClassCons = MyClass<Double>::new;
//Create an instance of MyClass by use of the factory method.
MyClass<Double> mc = myClassFactory(myClassCons, 100.1);
//Use the instance of MyClass just created.
System.out.println("val in mc is " + mc.getVal());
//Now,create a different class by use of myClassFactory().
MyFunc<MyClass2, String> myClassCons2 = MyClass2::new;
//Create an instance of MyClass2 by use of the factory method.
MyClass2 mc2 =myClassFactory(myClassCons2,"lambda");
//Use the instance of MyClass just created.
System.out.println("str in mc2 is "+mc2.getVal());
}
}
可以看到,程序中使用myClassFactory()方法来创建MyClass<Double>和MyClass2类型的对象。虽然这两个类不同,比如MyClass是泛型类,而MyClass2不是,但是这两个类都可以使用myClassFactory()方法创建,因为这两个类的构造函数都与MyFunc的func()方法兼容。这种方式可以工作,因为传递给myClassFactory()方法的参数就是该方法要构建的对象的构造函数。可以使用自己创建的不同的类,自由尝试这个程序。另外,还可以尝试创建不同类型的MyClass对象的实例。我们将看到,只要对象的类的构造函数与MyFunc的func()方法兼容,myClassFactory()方法就可以创建任意类型的对象。虽然这个例子十分简单,但却能从中窥出构造函数引用带给Java的强大功能。
在继续讨论之前,需要指出用于数组的构造函数引用的语法形式,要为数组创建构造函数引用,需要使用如下所示的语法:
type[]::new
其中,type指定要创建的对象的类型。例如,假设有一个MyClass类,其形式与第一个构造函数引用示例(ConstructorRefDemo)相同,并且还有如下所示的MyArrayCreator接口:
interface MyArrayCreator<T>{
T func(int n);
}
下面的代码创建了两个MyClass对象的数组,并赋给每个元素初值:
MyArrayCreator<MyClass[]> mcArrayCons = MyClass[]::new;
MyClass[] a = mcArrayCons.func(2);
a[0] = new MyClass(1);
a[1] = new MyClass(2);
这里,调用func(2)会创建一个包含两个元素的数组。一般来说,如果函数式接口中包含的方法要用于引用数组构造函数,那么该方法只接受一个int类型的参数。
1.9 预定义的函数式接口
直到现在,本篇都定义了自己的函数式接口,以便清晰地演示lambda表达式和函数式接口背后的基本概念。很多时候,并不需要自己定义函数式接口,因为JDK8中包含了新包java.util.function,其中提供了一些预定义的函数式接口。下表给出了它们的简介:
接口 | 用 途 |
---|---|
UnaryOperator | 对类型为T的对象应用一元运算符,并返回结果。结果的类型也是T。包含的方法名为apply() |
BinaryOperator | 对类型为T的两个对象应用操作 ,并返回结果。结果的类型也是T。包含的方法名为apply() |
Consumer | 对类型为T的对象应用操作。包含的方法名为accept() |
Supplier | 返回类型为T的对象。包含的方法名为get() |
Function<T,R> | 对类型为T的对象应用操作,并返回结果。j结果类型为R的对象。包含的方法名为apply() |
Predicate | 确定类型为T的对象是否满足某种约束,并返回指出结果的布尔值。包含的方法名为test() |
下面的程序通过使用Function接口重写前面的BlocklambdaDemo示例,演示了Function接口的实际应用。BlocklambdaDemo示例通过实现一个阶乘,演示了块lambda。该例创建了自己的函数式接口NumericFunc,但其实也可以使用内置的Function接口,如程序的下面这个版本所示:
//Import the Function interface
import java.util.function.Function;
class UseFunctionInterfaceDemo {
public static void main(String[] args) {
//This block lambda computes the factorial of an int value.
//This time,Function is the functional interface.
Function<Integer, Integer> factorial = (n) -> {
int result = 1;
for (int i = 1; i <= n; i++)
result = i * result;
return result;
};
System.out.println("The factorial of 3 is "+factorial.apply(3));
System.out.println("The factorial of 5 is "+factorial.apply(5));
/**
* 输出:
* The factorial of 3 is 6
* The factorial of 5 is 120
*/
}
}
这个版本的输出与前一个版本相同。