作者:Deepak Vohra
学习如何充分利用 lambda 和虚拟扩展方法。
2013 年 8 月发布
Lambda 表达式也称为闭包,是匿名类的简短形式。Lambda 表达式简化了单一抽象方法声明接口的使用,因此 lambda 表达式也称为功能接口。在 Java SE 7 中,单一方法接口可使用下列选项之一实现。
可以使用 lambda 表达式实现功能接口,无需创建类或匿名类。Lambda 表达式只能用于单一方法声明接口。
Lambda 表达式旨在支持多核处理器架构,这种架构依赖于提供并行机制的软件,而该机制可以提高性能、减少完成时间。
Lambda 表达式具有以下优点:
要跟随本文中的示例,请下载并安装以下软件:
Lambda 表达式的语法如下所示。
(formal parameter list) ->{ expression or statements }
参数列表是一个逗号分隔的形式参数列表,这些参数与功能接口中单一方法的形式参数相对应。指定参数类型是可选项;如果未指定参数类型,将从上下文推断。
参数列表必须用括号括起来,但当指定的单一参数不带参数类型时除外;指定单一形式参数时可以不带括号。如果功能接口方法不指定任何形式参数,则必须指定空括号。
参数列表后面是 ->
运算符,然后是 lambda 主体,即单一表达式或语句块。Lambda 主体的结果必须是下列值之一:
void
,如果功能接口方法的结果是 void
Lambda 主体根据以下选项之一返回结果:
return
语句返回值。void
,可以提供一个 return
语句,但这不是必需的。语句块必须包含在大括号 ({}
) 内,除非语句块是一个方法调用语句,而其调用的方法的结果是 void
。Lambda 主体的结果必须与功能接口中单一方法的结果相同。例如,如果功能接口方法的结果是 void
,则 lambda 表达式主体不能返回值。如果功能接口方法具有返回类型 String
,则 lambda 表达式主体必须返回 String
。如果 lambda 主体是一条语句,并且该方法具有一个返回类型,则该语句必须是 return
语句。调用 lambda 表达式时,将运行 lambda 主体中的代码。
Lambda 表达式与功能接口一起使用,功能接口实际上是一种只有一个抽象方法的接口;功能接口可以包含一个同时也存在于 Object
类中的方法。功能接口的示例有 java.util.concurrent.Callable
(具有单一方法 call()
)和 java.lang.Runnable
(具有单一方法 run()
)。
区别在于,匿名接口类需要指定一个实例创建表达式,以便接口和编译器用来创建接口实现类的实例。与指定接口类型(或类类型)的匿名类不同,lambda 表达式不指定接口类型。从上下文推断为其调用 lambda 表达式的功能接口,也称为 lambda 表达式的目标类型。
Lambda 表达式有一个隐式的目标类型与之关联,因为未明确指定接口类型。在 lambda 表达式中,lambda 转换的目标类型必须是一个功能接口。从上下文推断目标类型。因此,lambda 表达式只能用在可以推断目标类型的上下文中。此类上下文包括
要在 Eclipse IDE 中使用 Java 8,您需要下载一个支持 JDK 8 的 Eclipse 版本。
图 1
接下来,我们将通过一些示例讨论如何使用 lambda 表达式。
Hello
应用程序我们都很熟悉 Hello
应用程序,当我们提供一个姓名时,它会输出一条消息。Hello
类声明了两个字段、两个构造函数和一个 hello()
方法来输出消息,如下所示。
public class Hello { String firstname; String lastname; public Hello() {} public Hello(String firstname, String lastname) { this.firstname = firstname; this.lastname = lastname;} public void hello() { System.out.println("Hello " + firstname + " " + lastname);} public static void main(String[] args) { Hello hello = new Hello(args[0], args[1]); hello.hello(); } }
现在,我们来看看 lambda 表达式如何简化 Hello
示例中的语法。首先,我们需要创建一个功能接口,该接口包含一个返回“Hello”消息的方法。
interface HelloService {String hello(String firstname, String lastname); }
创建一个 lambda 表达式,它包含两个参数,与接口方法的参数相匹配。在 lambda 表达式的主体中,使用 return
语句创建并返回根据 firstname
和 lastname
构造的“Hello”消息。返回值的类型必须与接口方法的返回类型相同,并且 lambda 表达式的目标必须是功能接口 HelloService
。参见清单 1。
public class Hello { interface HelloService { String hello(String firstname, String lastname); } public static void main(String[] args) { HelloService helloService=(String firstname, String lastname) -> { String hello="Hello " + firstname + " " + lastname; return hello; }; System.out.println(helloService.hello(args[0], args[1])); } }
清单 1
我们需要先为 hello()
的方法参数提供一些程序参数,然后才能运行 Hello
应用程序。在 Package Explorer 中右键单击 Hello.java
,然后选择 Run As > Run Configurations。在 Run Configurations 中,选择 Arguments 选项卡,在 Program arguments 字段中指定参数,然后单击 Apply。然后单击 Close。
要运行 Hello.java
应用程序,在 Package Explorer 中右键单击 Hello.java
,然后选择 Run As > Java Application。应用程序的输出将显示在 Eclipse 控制台中,如图 2 所示。
图 2
Lambda 表达式不会定义新的作用域;lambda 表达式的作用域与封闭作用域相同。例如,如果 Lambda 主体声明的局部变量与封闭作用域内的变量重名,将产生编译器错误 Lambda expression's local variable i cannot re-declare another local variable defined in an enclosing scope
,如图 3 所示。
图 3
局部变量无论是在 lambda 表达式主体中声明,还是在封闭作用域中声明,使用之前都必须先初始化。要证明这一点,请在封闭方法中声明一个局部变量:
int i;
在 lambda 表达式中使用该局部变量。将产生编译器错误 The local variable i may not have been initialized
,如图 4 所示。
图 4
lambda 表达式中使用的变量必须处于终态或等效终态。要证明这一点,请声明并初始化局部变量:
int i=5;
给 lambda 表达式主体中的变量赋值。将产生编译器错误 Variable i is required to be final or effectively final
,如图 5 所示。
图 5
可按如下方式将变量 i
声明为终态。
final int i=5;
否则,该变量必须为等效终态,即不能在 lambda 表达式中对该变量赋值。封闭上下文中的方法参数变量和异常参数变量也必须处于终态或等效终态。
Lambda 主体中的 this
和 super
引用与封闭上下文中一样,因为 lambda 表达式不会引入新的作用域,这与匿名类不同。
Lambda 表达式实际上是一种匿名方法实现;指定形式参数,并使用 return
语句返回值。匿名方法必须按照以下规则所规定的与其实现的功能接口方法兼容。
void
,则 lambda 主体必须与 void 兼容。如果返回一个值,则 lambda 主体必须与值兼容。返回值的类型可以是功能接口方法声明中返回类型的子类型。throws
子句中声明了异常类型或异常超类型的异常。要证明如果功能接口方法返回一个结果,lambda 表达式也必须返回一个结果,请在目标类型为 HelloService
的 lambda 表达式中注释掉 return
语句。因为功能接口 HelloService
中的 hello()
方法具有一个 String
返回类型,因此产生编译器错误,如图 6 所示。
图 6
如果功能接口方法将结果声明为 void
,而 lambda 表达式返回了一个值,那么将产生编译器错误,如图 7 所示。
图 7
如果 lambda 表达式签名与功能接口方法签名不完全相同,将产生编译器错误。要证明这一点,请使 lambda 表达式参数列表为空,同时功能接口方法声明两个形式参数。将产生编译器错误 Lambda expression's signature does not match the signature of the functional interface method
,如图 8 所示。
图 8
可变参数与数组参数之间不作区分。例如,功能接口方法按如下方式声明数组类型参数:
interface Int { void setInt(int[] i); }
Lambda 表达式的参数列表可以声明可变参数:
Int int1 =(int... i)->{};
Lambda 表达式主体抛出的异常不能超出功能接口方法的 throws
子句中指定的异常数。如果 lambda 表达式主体抛出异常,功能接口方法的 throws
子句必须声明相同的异常类型或其超类型。
要证明这一点,在 HelloService
接口的 hello
方法中不声明 throws
子句,从 lambda 表达式主体抛出异常。将产生编译器错误 Unhandled exception type Exception
,如图 9 所示。
图 9
如果在功能接口方法中添加与所抛出异常相同的异常类型,编译器错误将得以解决,如图 10 所示。但如果使用以 lambda 表达式结果赋值的引用变量来调用 hello
方法,将产生编译器错误,因为 main
方法中未对异常进行处理,如图 10 所示。
图 10
Lambda 表达式的类型是从目标类型推导出来的类型。相同的 lambda 表达式在不同的上下文中可以有不同的类型。此类表达式称为多态表达式。要证明这一点,请定义与 HelloService
具有相同抽象方法签名的另一功能接口,例如:
interface HelloService2 { String hello(String firstname, String lastname); }
例如,相同的 lambda 表达式(下面的表达式)可用于所声明的方法具有相同签名、返回类型以及 throws
子句的两个功能接口:
(String firstname, String lastname) -> { String hello = "Hello " + firstname + " " + lastname; return hello; }
没有上下文,前面的 lambda 表达式没有类型,因为它没有目标类型。但是,如果在具有目标类型的上下文中使用,则 lambda 表达式可以根据目标类型具有不同的类型。在以下两种情况下,前面的 lambda 表达式具有不同的类型,因为目标类型不同:HelloService
和 HelloService2
。
HelloService helloService =(String firstname, String lastname) -> { String hello = "Hello " + firstname + " " + lastname; return hello; };
HelloService2 helloService2 =(String firstname, String lastname) -> { String hello = "Hello " + firstname + " " + lastname; return hello; };
不支持泛型 Lambda。Lambda 表达式不能引入类型变量。
java.awt
软件包中的 GUI 组件使用 java.awt.event.ActionListener
接口注册组件的操作事件。java.awt.event.ActionListener
接口是一个只有一个方法的功能接口:actionPerformed(ActionEvent e)
。
使用 addActionListener(ActionListener l)
方法将 java.awt.event.ActionListener
注册到组件。例如,可以使用应用程序中的匿名内部类按如下方式将 java.awt.event.ActionListener
注册到 java.awt.Button
组件,用以计算 Button
对象(称为 b
)被单击的次数。(更多详细信息,请参见“如何编写操作监听程序”)
b.addActionListener (new ActionListener() { int numClicks = 0; public void actionPerformed(ActionEvent e) { numClicks++; text.setText("Button Clicked " + numClicks + " times"); } });
可以用 Lambda 表达式代替匿名内部类,使语法更加简洁。以下是使用 lambda 表达式将 ActionListener
注册到 ActionListener
组件的一个示例:
b.addActionListener(e -> { numClicks++; text.setText("Button Clicked " + numClicks + " times"); }); }
对于只有一个参数的 lambda 表达式,用于指定参数的括号可以省略。Lambda 表达式的目标类型(功能接口 ActionListener
)是从上下文(即一个方法调用)推断出来的。
在本节中,我们将讨论一些常见的功能接口如何与 lambda 表达式结合使用。
FileFilter
接口FileFilter
接口具有单一方法 accept()
,用于筛选文件。在 Java 教程 ImageFilter
示例中,ImageFilter
类实现了 FileFilter
接口,并提供了 accept()
方法的实现。accept()
方法用来使用 Utils
类只接受图像文件(和目录)。
我们可以使用一个返回 boolean
的 lambda 表达式提供 FileFilter
接口的实现,如清单 2 所示。
import java.io.FileFilter; import java.io.File; public class ImageFilter { public static void main(String[] args) { FileFilter fileFilter = (f) -> { String extension = null; String s = f.getName(); int i = s.lastIndexOf('.'); if (i > 0 && i << s.length() - 1) { extension = s.substring(i + 1).toLowerCase(); } if (extension != null) { if (extension.equals("tiff") || extension.equals("tif") || extension.equals("gif") || extension.equals("jpeg") || extension.equals("jpg") || extension.equals("png") || extension.equals("bmp")) { return true; } else { return false; } } return false; }; File file = new File("C:/JDK8/Figure10.bmp"); System.out.println("File is an image file: " + fileFilter.accept(file)); } }
清单 2
Eclipse 控制台显示了 ImageFilter
类的输出,如图 11 所示。
图 11
Runnable
接口在 Java 教程“定义和启动线程”一节中,HelloRunnable
类实现了 Runnable
接口,并使用 HelloRunnable
类的实例创建了 Thread
。可以使用 lambda 表达式创建 Thread
的 Runnable
,如清单 3 所示。Lambda 表达式没有 return
语句,因为 Runnable
中 run()
方法的结果是 void
。
import java.lang.Runnable; public class HelloRunnable { public static void main(String args[]) { (new Thread(() -> { System.out.println("Hello from a thread"); })).start(); } }
HelloRunnable
类的输出如图 12 所示。
图 12
Callable
接口如果我们创建了一个实现 java.util.concurrent.Callable<V>
泛型功能接口的类,该类需要实现 call()
方法。在清单 4 中,HelloCallable
类实现了参数化类型 Callable<String>
。
package lambda; import java.util.concurrent.Callable; public class HelloCallable implements Callable<String> { @Override public String call() throws Exception { return "Hello from Callable"; } public static void main(String[] args) { try { HelloCallable helloCallable = new HelloCallable(); System.out.println(helloCallable.call()); } catch (Exception e) { System.err.println(e.getMessage()); } } }
清单 4
我们可以使用 lambda 表达式提供 call()
泛型方法的实现。由于 call()
方法不需要任何参数,因此 lambda 表达式中的括号为空;由于该方法为参数化类型 Callable<String>
返回 String
,所以 lambda 表达式必须返回 String
。
import java.util.concurrent.Callable; public class HelloCallable { public static void main(String[] args) { try { Callable<String> c = () -> "Hello from Callable"; System.out.println(c.call()); } catch (Exception e) { System.err.println(e.getMessage()); } } }
清单 5
HelloCallable
的输出如图 13 所示。
图 13
PathMatcher
接口java.nio.file.PathMatcher
接口用于匹配路径。该功能接口具有单一方法 matches(Path path)
,用于匹配 Path
。我们可以使用 lambda 表达式提供 matches()
方法的实现,如清单 6 所示。Lambda 表达式返回 boolean
,目标类型是功能接口 PathMatcher
。
import java.nio.file.PathMatcher; import java.nio.file.Path; import java.nio.file.FileSystems; public class FileMatcher { public static void main(String[] args) { PathMatcher matcher = (f) -> { boolean fileMatch = false; String path = f.toString(); if (path.endsWith("HelloCallable.java")) fileMatch = true; return fileMatch; }; Path filename = FileSystems.getDefault().getPath( "C:/JDK8/HelloCallable.java"); System.out.println("Path matches: " + matcher.matches(filename)); } }
清单 6
FileMatcher 类的输出如图 14 所示。
图 14
Comparator
接口功能接口 Comparator
具有单一方法:compares()
。虽然该接口还有 equals()
方法,但 equals()
方法也存在于 Object
类中。除了另一个方法外,功能接口还可以有 Object
类方法。如果我们使用 Comparator
比较 Employee
实体的实例,首先需要定义 Employee
POJO,它具有 empId
、firstName
和 lastName
属性和针对这些属性的 getter/setter 方法,如清单 7 所示。
import java.util.*; public class Employee { private int empId; private String lastName; private String firstName; public Employee() { } public Employee(int empId, String lastName, String firstName) { this.empId = empId; this.firstName = firstName; this.lastName = lastName; } // setters and getters public int getEmpId() { return empId; } public void setEmpId(int empId) { this.empId = empId; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public int compareByLastName(Employee x, Employee y) { return x.getLastName().compareTo(y.getLastName()); } /** * * public static int compareByLastName(Employee x, Employee y) { return x.getLastName().compareTo(y.getLastName()); } */ }
清单 7
如清单 8 所示,创建一个名为 EmployeeSort
的类,根据 lastName
对 Employee
实体的 List
进行排序。在 EmployeeSort
类中,创建 List
对象并向其添加 Employee
对象。使用 Collections.sort
方法对 List
进行排序,并使用匿名内部类为 sort()
方法创建 Comparator
对象。
package lambda; import java.util.*; public class EmployeeSort { public static void main(String[] args) { Employee e1 = new Employee(1, "Smith", "John"); Employee e2 = new Employee(2, "Bloggs", "Joe"); List<Employee> list = new ArrayList<Employee>(); list.add(e1); list.add(e2); Collections.sort(list, new Comparator<Employee>() { public int compare(Employee x, Employee y) { return x.getLastName().compareTo(y.getLastName()); } }); ListIterator<Employee> litr = list.listIterator(); while (litr.hasNext()) { Employee element = litr.next(); System.out.print(element.getLastName() + " "); } } }
清单 8
可以用 lambda 表达式替换匿名内部类,该表达式有两个 Employee
类型参数,根据 lastName
比较返回 int
值,如清单 9 所示。
import java.util.*; public class EmployeeSort { public static void main(String[] args) { Employee e1 = new Employee(1, "Smith", "John"); Employee e2 = new Employee(2, "Bloggs", "Joe"); List<Employee> list = new ArrayList<Employee>(); list.add(e1); list.add(e2); Collections.sort(list, (x, y) -> x.getLastName().compareTo(y.getLastName())); ListIterator<Employee> litr = list.listIterator(); while (litr.hasNext()) { Employee element = litr.next(); System.out.print(element.getLastName() + " "); } } }
清单 9
EmployeeSort
的输出如图 15 所示。
图 15
对于 lambda 表达式,从上下文推断目标类型。因此,lambda 表达式只能用在可以推断目标类型的上下文中。这类上下文包括:变量声明、赋值语句、return
语句、数组初始值设定项、方法或构造函数的参数、lambda 表达式主体、条件表达式和转换表达式。
Lambda 的形式参数类型也从上下文推断。除了清单 1 中的 Hello
示例,在前面的所有示例中,参数类型都是从上下文推断出来的。在后续章节中,我们将讨论可以使用 lambda 表达式的上下文。
return
语句中的 Lambda 表达式Lambda 表达式可以用在 return
语句中。return
语句中使用 lambda 表达式的方法的返回类型必须是一个功能接口。例如,返回 Runnable
的方法的 return
语句中包含一个 lambda 表达式,如清单 10 所示。
import java.lang.Runnable; public class HelloRunnable2 { public static Runnable getRunnable() { return () -> { System.out.println("Hello from a thread"); }; } public static void main(String args[]) { new Thread(getRunnable()).start(); } }
清单 10
Lambda 表达式未声明任何参数,因为 Runnable
接口的 run()
方法没有声明任何形式参数。Lambda 表达式不返回值,因为 run()
方法的结果是 void
。清单 10 的输出如图 16 所示。
图 16
Lambda 表达式本身可以用作内部 lambda 表达式的目标类型。Callable
中的 call()
方法返回一个 Object
,但是 Runnable
中的 run()
方法没有返回类型。在 HelloCallable2
类中,内部 lambda 表达式的目标类型是 Runnable
,外部 lambda 表达式的目标类型是 Callable
。目标类型是从上下文(对 Callable<Runnable>
类型的引用变量进行赋值)推断出来的。
在清单 11 中,内部 lambda 表达式 () -> {System.out.println("Hello from Callable");}
的类型被推断为 Runnable
,因为参数列表为空,并且结果是 void
;匿名方法签名和结果与 Runnable
接口中的 run()
方法相同。外部 lambda 表达式 () -> Runnable
的类型被推断为 Callable<Runnable>
,因为 Callable<V>
中的 call()
方法没有声明任何形式参数,并且结果类型是类型参数 V
。
import java.util.concurrent.Callable; public class HelloCallable2 { public static void main(String[] args) { try { Callable<Runnable> c = () -> () -> { System.out.println("Hello from Callable"); }; c.call().run(); } catch (Exception e) { System.err.println(e.getMessage()); } } }
清单 11
HelloCallable2
的输出如图 17 所示。
图 17
Lambda 表达式可以用在数组初始值设定项中,但不能使用泛型数组初始值设定项。例如,以下泛型数组初始值设定项中的 lambda 表达式将产生编译器错误:
Callable<String>[] c=new Callable<String>[]{ ()->"a", ()->"b", ()->"c" };
将产生编译器错误 Cannot create a generic array of Callable<String>
,如图 18 所示。
图 18
要在数组初始值设定项中使用 lambda 表达式,请指定一个非泛型数组初始值设定项,如清单 12 所示的 CallableArray
类。
import java.util.concurrent.Callable; public class CallableArray { public static void main(String[] args) { try{ Callable<String>[] c=new Callable[]{ ()->"Hello from Callable a", ()->"Hello from Callable b", ()->"Hello from Callable c" }; System.out.println(c[1].call()); }catch(Exception e){System.err.println(e.getMessage());} } }
CallableArray
中的每个数组初始值设定项变量都是一个 Callable<String>
类型的 lambda 表达式。Lambda 表达式的参数列表为空,并且 lambda 表达式的结果是一个 String
类型的表达式。每个 lambda 表达式的目标类型从上下文推断为 Callable<String>
类型。CallableArray
的输出如图 19 所示。
图 19
Lambda 表达式的目标类型有时可能并不明确。例如,在下面的赋值语句中,lambda 表达式用作 AccessController.doPrivileged
方法的方法参数。Lambda 表达式的目标类型不明确,因为多个功能接口(PrivilegedAction
和 PrivilegedExceptionAction
)都可以是 lambda 表达式的目标类型。
String user = AccessController.doPrivileged(() -> System.getProperty("user.name"));
将产生编译器错误 The method doPrivileged(PrivilegedAction<String>) is ambiguous for the type AccessController
,如图 20 所示。
图 20
我们可以使用 lambda 表达式的转换将目标类型指定为 PrivilegedAction<String>
,如清单 13 所示的 UserPermissions
类。
import java.security.AccessController; import java.security.PrivilegedAction; public class UserPermissions { public static void main(String[] args) { String user = AccessController .doPrivileged((PrivilegedAction<String>) () -> System .getProperty("user.name")); System.out.println(user); } }
清单 13
Lambda 表达式中使用转换后的 UserPermissions
输出如图 21 所示。
图 21
Lambda 表达式可以用在三元条件表达式中,后者的值是这两个操作数中的任何一个,具体取决于 boolean
条件为 true
还是 false
。
在清单 14 所示的 HelloCallableConditional
类中,lambda 表达式 () -> "Hello from Callable:flag true")
和 () -> "Hello from Callable:flag false")
构成了用于赋值的这两个可供选择的表达式。Lambda 表达式的目标类型是从上下文(对 Callable<String>
引用变量进行赋值)推断出来的。随后,使用该引用变量调用 call()
方法。
import java.util.concurrent.Callable public class HelloCallableConditional { public static void main(String[] args) { try { boolean flag = true; Callable<String> c = flag ? (() -> "Hello from Callable: flag true") : (() -> "Hello from Callable: flag false"); System.out.println(c.call()); } catch (Exception e) { System.err.println(e.getMessage()); } } }
清单 14
HelloCallableConditional
的输出如图 22 所示。
图 22
调用重载方法时,将使用与 lambda 表达式最匹配的方法。我们将使用目标类型和方法参数类型选择最佳方法。
在清单 15 的 HelloRunnableOrCallable
类中,指定了两个返回类型为 String
的 hello()
方法(hello()
方法被重载):它们的参数类型分别是 Callable
和 Runnable
。
将调用 hello()
方法,其中 lambda 表达式作为方法参数。由于 lambda 表达式 () -> "Hello Lambda"
返回 String
,因此会调用 hello(Callable)
方法并输出 Hello from Callable
,因为 Callable
的 call()
方法具有返回类型,而 Runnable
的 run()
方法没有。
import java.util.concurrent.Callable; public class HelloRunnableOrCallable { static String hello(Runnable r) { return "Hello from Runnable"; } static String hello(Callable c) { return "Hello from Callable"; } public static void main(String[] args) { String hello = hello(() -> "Hello Lambda"); System.out.println(hello); } }
清单 15
HelloCallableConditional
的输出如图 23 所示。
图 23
this
在 lambda 表达式外面,this
引用当前对象。在 lambda 表达式里面,this
引用封闭的当前对象。不引用封闭实例成员的 lambda 表达式不会存储对该成员的强引用,这解决了内部类实例保留对封闭类的强引用时经常出现的内存泄漏问题。
在清单 16 所示的示例中,Runnable
是 lambda 表达式的目标类型。在 lambda 表达式主体中,指定了对 this
的引用。创建 Runnable r
实例并调用 run()
方法后,this
引用将调用封闭实例,并从 toString()
方法获得封闭实例的 String
值。将输出 Hello from Class HelloLambda
消息。
public class HelloLambda { Runnable r = () -> { System.out.println(this); }; public String toString() { return "Hello from Class HelloLambda"; } public static void main(String args[]) { new HelloLambda().r.run(); } }
清单 16
HelloLambda
的输出如图 24 所示。
图 24
为 lambda 表达式的形式参数创建新名称。如果用作 lambda 表达式参数名称的名称与封闭上下文中局部变量名称相同,将产生编译器错误。在以下示例中,lambda 表达式的参数名称被指定为 e1
和 e2
,它们同时还用于局部变量 Employee e1
和 Employee e2
。
Employee e1 = new Employee(1,"A", "C"); Employee e2 = new Employee(2,"B","D" ); List<Employee> list = new ArrayList<Employee>(); list.add(e1);list.add(e2); Collections.sort(list, (Employee e1, Employee e2) -> e1.getLastName().compareTo(e2.getLastName()));
Lambda 表达式参数 e1
将导致编译器错误,如图 25 所示。
图 25
Lambda 表达式参数 e2
也将导致编译器错误,如图 26 所示。
图 26
在提供匿名内部类的替代方案时,局部变量必须处于终态才能在 lambda 表达式中访问的要求被取消。JDK 8 也取消了局部变量必须处于终态才能从内部类访问的要求。在 JDK 7 中,必须将从内部类访问的局部变量声明为终态。
在内部类或 lambda 表达式中使用局部变量的要求已从“终态”修改为“终态或等效终态”。
Lambda 表达式定义了一个匿名方法,其中功能接口作为目标类型。可以使用方法引用来调用具有名称的现有方法,而不是定义匿名方法。在清单 9 所示的 EmployeeSort
示例中,以下方法调用将 lambda 表达式作为方法参数。
Collections.sort(list, (x, y) -> x.getLastName().compareTo(y.getLastName()));
可以按以下方式将 lambda 表达式替换为方法引用:
Collections.sort(list, Employee::compareByLastName);
分隔符 (::
) 可用于方法引用。compareByLastName
方法是 Employee
类中的静态方法。
public static int compareByLastName(Employee x, Employee y) { return x.getLastName().compareTo(y.getLastName());
对于非静态方法,方法引用可与特定对象的实例一起使用。通过将 compareByLastName
方法非静态化,可按如下方式将方法引用与与 Employee
实例结合使用:
Employee employee=new Employee(); Collections.sort(list, employee::compareByLastName);
方法引用甚至不必是所属对象的实例方法。方法引用可以是任何任意类对象的实例方法。例如,通过方法引用,可以使用 String
类的 compareTo
方法对 String List
进行排序。
String e1 = new String("A"); String e2 = new String("B"); List<String> list = new ArrayList<String>(); list.add(e1);list.add(e2); Collections.sort(list, String::compareTo);
方法引用是 lambda 表达式的进一步简化。
方法引用的作用是方法调用,而构造函数引用的作用是构造函数调用。方法引用和构造函数引用是 lambda 转换,方法引用和构造函数引用的目标类型必须是功能接口。
在本节中,我们将以 Multimap
为例讨论构造函数引用。Multimap 是一个 Google Collections 实用程序。图像类型的 Multimap
按如下方式创建:
Multimap<ImageTypeEnum, String> imageTypeMultiMap = Multimaps .newListMultimap( Maps.<ImageTypeEnum, Collection<String>> newHashMap(), new Supplier<List<String>>() { public List<String> get() { return new ArrayList<String>(); } });
在 Multimap
示例中,使用构造函数按如下方式创建 Supplier<List<String>>
:
new Supplier<List<String>>() { public List<String> get() { return new ArrayList<String>(); } }
构造函数返回 ArrayList<String>
。通过构造函数引用,可以使用简化的语法按如下方式创建 Multimap
:
Multimap<ImageTypeEnum, String> imageTypeMultiMap = Multimaps.newListMultimap(Maps.<ImageTypeEnum, Collection<String>> newHashMap(),ArrayList<String>::new);
清单 17 显示了使用构造函数引用的 Multimap
示例,即 ImageTypeMultiMap
类。
import java.util.ArrayList; import java.util.Collection; import java.util.List; import com.google.common.base.Supplier; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; public class ImageTypeMultiMap { enum ImageTypeEnum { tiff, tif, gif, jpeg, jpg, png, bmp } public static void main(String[] args) { Multimap<ImageTypeEnum, String> imageTypeMultiMap = Multimaps .newListMultimap( Maps.<ImageTypeEnum, Collection<String>> newHashMap(), ArrayList<String>::new); imageTypeMultiMap.put(ImageTypeEnum.tiff, "tiff"); imageTypeMultiMap.put(ImageTypeEnum.tif, "tif"); imageTypeMultiMap.put(ImageTypeEnum.gif, "gif"); imageTypeMultiMap.put(ImageTypeEnum.jpeg, "jpeg"); imageTypeMultiMap.put(ImageTypeEnum.jpg, "jpg"); imageTypeMultiMap.put(ImageTypeEnum.png, "png"); imageTypeMultiMap.put(ImageTypeEnum.bmp, "bmp"); System.out.println("Result: " + imageTypeMultiMap); } }
清单 17
要测试 ImageTypeMultiMap
,我们需要从 https://code.google.com/p/guava-libraries/ 下载 Guava 库 guava-14.0.1.jar
,并将 guava-14.0.1.jar
添加到 Java 构建路径。ImageTypeMultiMap
的输出如图 27 所示。
图 27
接口的封装和可重用性是接口的主要优点。但接口的缺点是实现接口的类必须实现所有接口方法。有时只需要接口的部分方法,但在实现接口时必须提供所有接口方法的方法实现。虚拟扩展方法解决了这个问题。
虚拟扩展方法是接口中具有默认实现的方法。如果实现类不提供方法的实现,则使用默认的实现。实现类可以重写默认实现,或提供新的默认实现。
虚拟扩展方法添加配置来扩展接口的功能,而不会破坏已实现接口较早版本的类的向后兼容性。虚拟扩展方法中的默认实现是用 default
关键字提供的。由于虚拟扩展方法提供默认实现,因此不能是抽象方法。
JDK 8 中的 java.util.Map<K,V>
类提供了几个具有默认实现的方法:
default V getOrDefault(Object key,V defaultValue)
default void forEach(BiConsumer<? super K,? super V> action)
default void replaceAll(BiFunction<? super K,? super V,? extends V> function)
default V putIfAbsent(K key,V value)
default boolean remove(Object key,Object value)
default boolean replace(K key,V oldValue,V newValue)
default V replace(K key,V value)
default V computeIfAbsent(K key,Function<? super K,? extends V> mappingFunction)
default V computeIfPresent(K key,BiFunction<? super K,? super V,? extends V> remappingFunction)
default V compute(K key,BiFunction<? super K,? super V,? extends V> remappingFunction)
default V merge(K key,V value,BiFunction<? super V,? super V,? extends V> remappingFunction)
要证明类在实现接口时无需实现具有默认实现的方法,请创建实现 Map<K,V>
接口的 MapImpl
类:
public class MapImpl<K,V> implements Map<K, V> {
清单 18 显示了完整的 MapImpl
类,实现了不提供默认实现的方法。
import java.util.Collection; import java.util.Map; import java.util.Set; public class MapImpl<K,V> implements Map<K, V> { public static void main(String[] args) { } @Override public int size() { return 0; } @Override public boolean isEmpty() { return false; } @Override public boolean containsKey(Object key) { return false; } @Override public boolean containsValue(Object value) { return false; } @Override public V get(Object key) { return null; } @Override public V put(K key, V value) { return null; } @Override public V remove(Object key) { return null; } @Override public void putAll(Map<? extends K, ? extends V> m) { } @Override public void clear() { } @Override public Set<K> keySet() { return null; } @Override public Collection<V> values() { return null; } @Override public Set<java.util.Map.Entry<K, V>> entrySet() { return null; } }
清单 18
虽然 Map<K,V>
接口是一个预定义的接口,但也可以使用虚拟扩展方法定义一个新接口。使用 default
关键字创建为所有方法提供默认实现的 EmployeeDefault
接口,如清单 19 所示。
public interface EmployeeDefault { String name = "John Smith"; String title = "PHP Developer"; String dept = "PHP"; default void setName(String name) { System.out.println(name); } default String getName() { return name; } default void setTitle(String title) { System.out.println(title); } default String getTitle() { return title; } default void setDept(String dept) { System.out.println(dept); } default String getDept() { return dept; } }
清单 19
如果使用 default
关键字声明接口方法,则该方法必须按编译器错误指出的方式提供实现,如图 28 所示。
图 28
默认情况下,接口的字段处于终态,不能在默认方法的默认实现中赋值,如图 29 中的编译器错误所示。
图 29
没有实现 EmployeeDefault
接口的类,也可以提供虚拟扩展方法的实现。EmployeeDefaultImpl
类实现了 EmployeeDefault
接口,没有为从 EmployeeDefault
继承的任何虚拟扩展方法提供实现。EmployeeDefaultImpl
类使用方法调用表达式调用虚拟扩展方法,如清单 20 所示。
public class EmployeeDefaultImpl implements EmployeeDefault { public static void main(String[] args) { EmployeeDefaultImpl employeeDefaultImpl=new EmployeeDefaultImpl(); System.out.println(employeeDefaultImpl.getName()); System.out.println(employeeDefaultImpl.getTitle()); System.out.println(employeeDefaultImpl.getDept()); } }
清单 20
本文介绍了 JDK 8 的新特性 — lambda 表达式,其语法简洁,是匿名类的简短形式。此外,还介绍了虚拟扩展方法,它非常有用,因为它提供了具有默认方法实现的接口;如果实现类不提供方法的实现,则会使用该默认的方法实现。
Deepak Vohra 是一名 NuBean 顾问、Web 开发人员、Sun 认证的 Java 1.4 程序员和 Sun 认证的 Java EE Web 组件开发人员。
请在 Facebook、Twitter 和 Oracle Java 博客上加入 Java 社区对话!