利用行为参数化来传递代码有助于应对不断变化的需求。它允许你定义一个代码块来表示一个行为,然后传递它。你可以决定在某一事件发生时(例如单击一个按钮)或在算法中的某个特定时刻(例如筛选算法中类似于“重量超过150
克的苹果”的谓词,或排序中的自定义比较操作)运行该代码块。一般来说,利用这个概念,你就可以编写更为灵活且可重复使用的代码了。
但使用匿名类来表示不同的行为并不令人满意:代码十分啰嗦,这会影响程序员在实践中使用行为参数化的积极性。接下来会看到Java 8
中解决这个问题的新工具——Lambda
表达式。它可以让你很简洁地表示一个行为或传递代码。现在你可以把Lambda
表达式看作匿名功能,它基本上就是没有声明名称的方法,但和匿名类一样,它也可以作为参数传递给一个方法。
可以把Lambda
表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
Lambda
函数不像方法那样属于某个特定的类。但和方法一样,Lambda
有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。Lambda
表达式可以作为参数传递给方法或存储在变量中。Lambda
这个词是从哪儿来的?其实它来自于学术界开发出来的一套用来描述计算的λ
演算法。
Lambda
表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常的列表。
在Java
中传递代码十分繁琐和冗长,而现在,Lambda
解决了这个问题:它可以让你十分简明地传递代码。理论上来说,你在Java 8
之前做不了的事情,Lambda
也做不了。但是,现在你用不着再用匿名类写一堆笨重的代码,来体验行为参数化的好处了!Lambda
表达式鼓励你采用行为参数化风格。最终结果就是你的代码变得更清晰、更灵活。比如,利用Lambda
表达式(由参数、箭头和主体组成),你可以更为简洁地自定义一个Comparator
对象:
Comparator<Apple> byWeight=new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
};
现在(用了Lambda
表达式):
Comparator<Apple> byWeight=(Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
解析如下:
Comparator
中compare
方法的参数,两个Apple
->
把参数列表与Lambda
主体分隔开。Apple
的重量。表达式就是Lambda
的返回值了。下面是Java 8
中五个有效的Lambda
表达式的例子:
//1. 具有一个String类型的参数并返回一个int,Lambda没有return语句,因为已经隐含了return
(String s) -> s.length()
//2. 有一个Apple类型的参数并返回一个boolean(苹果的重量是否超过150克)
(Apple a) -> a.getWeight() > 150
//3. 具有两个int类型的参数而没有返回值(void返回),注意Lambda表达式可以包含多行语句,这里是两行
(int x,int y) -> (
System.out.println("Result:");
System.out.println(x+y);
)
//4. 没有参数,返回一个int
() -> 42
//5. 具有两个Apple类型的参数,返回一个int;比较两个Apple的重量
(Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
Lambda
的基本语法如下:
(parameters) -> expression
或(注意语句的花括号)
(parameters) -> { statements; }
下表是一些Lambda
的例子和使用案例:
使用案例 | Lambda示例 |
---|---|
布尔表达式 | (List |
创建对象 | () -> new Apple(10) |
消费一个对象 | (Apple a) -> { System.out.println(a.getWeight(); } |
从一个对象中选择/抽取 | (String s) -> s.length() |
组合两个值 | (int a,int b) -> a*b |
比较两个对象 | (Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) |
只有在接受函数式接口的地方才可以使用Lambda
表达式。
函数式接口就是只定义一个抽象方法的接口。注意,接口现在还可以拥有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。
函数式接口只包含一个抽象方法,默认方法是种非抽象方法。
一个类只能继承一个抽象类,但是一个类可以实现多个接口。一个抽象类可以通过实例变量(字段)保存一个通用状态,而接口是不能有实例变量的。
用函数式接口可以干什么呢?Lambda
表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。
下面是一个在函数式接口(Runnable
)使用Lambda
表达式的例子:
//java.lang.Runnable
public interface Runnable{
public void run();
}
Runnable r1=() -> System.out.println("Hello World 1"); //使用Lambda
Runnable r2=new Runnable(){ //使用匿名类
public void run(){
System.out.println("Hello World 2");
}
};
public static void process(Runnable r){
r.run();
}
process(r1); //打印“Hello World 1”
process(r2); //打印“Hello World 2”
process(() -> System.out.println("Hello World 3")); //利用直接传递的Lambda打印“Hello World 3”
函数式接口的抽象方法的签名基本上就是Lambda
表达式的签名。我们将这种抽象方法叫作函数描述符。例如,Runnable
接口可以看作一个什么也不接受什么也不返回(void
)的函数的签名,因为它只有一个叫作run
的抽象方法,这个方法什么也不接受,什么也不返回(void
)。
我们可以使用一个特殊表示法来描述Lambda
和函数式接口的签名。() -> void
代表了参数列表为空,且返回void的函数。这正是Runnable
接口所代表的。举另一个例子,(Apple,Apple) -> int
代表接受两个Apple
作为参数且返回int
的函数。
Lambda
表达式可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法,当然这个Lambda
表达式的签名要和函数式接口的抽象方法一样。比如,在我们之前的例子里,你可以像下面这样直接把一个Lambda
传给process
方法:
public void process(Runnable r){
r.run();
}
process(() -> System.out.println("This is awesome!!"));
此代码执行时将打印“This is awesome!!
”。Lambda
表达式() -> System.out.println("This is awesome!!")
不接受参数且返回void
。这恰恰是Runnable
接口中run
方法的签名。
如果你去看看新的Java API
,会发现函数式接口带有@FunctionalInterface
的标注。这个标注用于表示该接口会设计成一个函数式接口。
如果你用@FunctionalInterface
定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。例如,错误消息可能是“Multiple non-overriding abstract methods found in interface Foo
”,表明存在多个抽象方法。请注意,@FunctionalInterface
不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。它就像是@Override
标注表示方法被重写了。
环绕执行模式(即在方法所必需的代码中间,你需要执行点儿什么操作,比如资源分配和清理)可以配合Lambda提高灵活性和可重用性。
资源处理(例如处理文件或数据库)时一个常见的模式就是打开一个资源,做一些处理,然后关闭资源。这个设置和清理阶段总是很类似,并且会围绕着执行处理的那些重要代码。这就是所谓的环绕执行(execute around
)模式,如下图所示,任务A
和任务B
周围都环绕着进行准备/清理的同一段冗余代码。
例如,在以下代码中,从一个文件中读取一行所需的模板代码(注意你使用了Java 7
中的带资源的try
语句,它已经简化了代码,因为你不需要显式地关闭资源了):
public static String processFile() throws IOException{
try(BufferedReader br=new BufferedReader(new FileReader("data.txt"))){
returnbr.readLine(); //这就是做有用工作的那行代码
}
}
现在上面这段代码是有局限的。你只能读文件的第一行。如果你想要返回头两行,甚至是返回使用最频繁的词,该怎么办呢?在理想的情况下,你要重用执行设置和清理的代码,并告诉processFile
方法对文件执行不同的操作。那么,你需要把processFile
的行为参数化。你需要一种方法把行为传递给processFile
,以便它可以利用BufferedReader
执行不同的行为。
传递行为正是Lambda
的拿手好戏。如果想一次读两行,在这个新的processFile
方法中,基本上,你需要一个接收BufferedReader
并返回String
的Lambda
。例如,下面就是从BufferedReader
中打印两行的写法:
String result=processFile((BufferedReader br) -> br.readLine()+br.readLine());
Lambda
仅可用于上下文是函数式接口的情况。你需要创建一个能匹配BufferedReader->String
,还可以抛出IOException
异常的接口:
@FunctionalInterface
public interface BufferedReaderProcessor{
String process(BufferedReader b) throws IOException;
}
现在你就可以把这个接口作为新的processFile
方法的参数了:
public static String processFile(BufferedReaderProcessor p) throws IOException{
…
}
任何BufferedReader->String
形式的Lambda
都可以作为参数来传递,因为它们符合BufferedReaderProcessor
接口中定义的process
方法的签名。现在你只需要一种方法在processFile
主体内执行Lambda
所代表的代码。请记住,Lambda
表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。因此,你可以在processFile
主体内,对得到的BufferedReaderProcessor
对象调用process
方法执行处理:
public static String processFile(BufferedReaderProcessor p) throws IOException{
try(BufferedReader br=new BufferedReader(new FileReader("data.txt"))){
return p.process(br); //处理BufferedReader对象
}
}
现在你就可以通过传递不同的Lambda
重用processFile
方法,并以不同的方式处理文件了。
处理一行:
String oneLine=processFile((BufferedReader br) -> br.readLine());
处理两行:
String twoLines=processFile((BufferedReader br) -> br.readLine()+br.readLine());
四个步骤总结如下:
上面说明了如何利用函数式接口来传递Lambda
,但你还是得定义你自己的接口。
函数式接口定义且只定义了一个抽象方法。函数式接口很有用,因为抽象方法的签名可以描述Lambda
表达式的签名。函数式接口的抽象方法的签名称为函数描述符。
为了应用不同的Lambda
表达式,你需要一套能够描述常见函数描述符的函数式接口。Java API
中已经有了几个函数式接口,比如Comparable
、Runnable
和Callable
。
Java 8
的库设计师帮你在java.util.function
包中引入了几个新的函数式接口。我们接下来会介绍Predicate
、Consumer
和Function
。
java.util.function.Predicate
接口定义了一个名叫test
的抽象方法,它接受泛型T
对象,并返回一个boolean
。这恰恰和你先前创建的一样,现在就可以直接使用了。在你需要表示一个涉及类型T
的布尔表达式时,就可以使用这个接口。比如,你可以定义一个接受String
对象的Lambda
表达式,如下所示。
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list,Predicate<T> p){
List<T> results=new ArrayList<>();
for(T s:list){
if(p.test(s)){
results.add(s);
}
}
return results;
}
Predicate<String> nonEmptyStringPredicate=(String s) -> !s.isEmpty();
List<String> nonEmpty=filter(listOfStrings,nonEmptyStringPredicate);
java.util.function.Consumer
定义了一个名叫accept
的抽象方法,它接受泛型T
的对象,没有返回(void
)。你如果需要访问类型T
的对象,并对其执行某些操作,就可以使用这个接口。比如,你可以用它来创建一个forEach
方法,接受一个Integers
的列表,并对其中每个元素执行操作。在下面的代码中,你就可以使用这个forEach
方法,并配合Lambda
来打印列表中的所有元素。
@FunctionalInterface
public interface Consumer<T>{
void accept(T t);
}
public static <T> void forEach(List<T> list,Consumer<T> c){
for(T i:list){
c.accept(i);
}
}
forEach(
Arrays.asList(1,2,3,4,5),
(Integer i) -> System.out.println(i) //Lambda是Consumer中accept方法的实现
);
java.util.function.Function
接口定义了一个叫作apply
的方法,它接受一个泛型T
的对象,并返回一个泛型R
的对象。如果你需要定义一个Lambda
,将输入对象的信息映射到输出,就可以使用这个接口(比如提取苹果的重量,或把字符串映射为它的长度)。在下面的代码中,会展示如何利用它来创建一个map
方法,以将一个String
列表映射到包含每个String
长度的Integer
列表。
@FunctionalInterface
public interface Function<T,R>{
R apply(T t);
}
public static <T,R> List<R> map(List<T> list,Function<T,R> f){
List<R> result=new ArrayList<>();
for(T s:list){
result.add(f.apply(s));
}
return result;
}
//[7,2,6]
List<Integer> l=map(Arrays.asList("lambdas","in","action"),(String s)->s.length()); //Lambda是Function接口的apply方法的实现
上面是三个泛型函数式接口:Predicate
、Consumer
和Function
。还有些函数式接口专为某些类型而设计。
Java
类型要么是引用类型(比如Byte
、Integer
、Object
、List
),要么是原始类型(比如int
、double
、byte
、char
)。但是泛型(比如Consumer
中的T
)只能绑定到引用类型。这是由泛型内部的实现方式造成的。
因此,在Java
里有一个将原始类型转换为对应的引用类型的机制。这个机制叫作装箱(boxing
)。相反的操作,也就是将引用类型转换为对应的原始类型,叫作拆箱(unboxing
)。Java
还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操作是自动完成的。
但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。
Java 8
为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。比如,在下面的代码中,使用IntPredicate
就避免了对值1000
进行装箱操作,但要是用Predicate
就会把参数1000
装箱到一个Integer
对象中:
public interface IntPredicate{
boolean test(int t);
}
IntPredicate evenNumbers=(int i) -> i % 2 == 0;
evenNumbers.test(1000); //true(无装箱)
Predicate<Integer> oddNumbers=(Integer i) -> i % 2 == 1;
oddNumbers.test(1000); //false(装箱)
Java 8
自带一些常用的函数式接口,放在java.util.function
包里,包括Predicate
、Function
、Supplier
、Consumer
和BinaryOperator
。
为了避免装箱操作,对Predicate
和Function
等通用函数式接口的原始类型特化:IntPredicate
、IntToLongFunction
等。
一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如DoublePredicate
、IntConsumer
、LongBinaryOperator
、IntFunction
等。Function
接口还有针对输出参数类型的变种:ToIntFunction
、IntToDoubleFunction
等。
下表总结了Java API
中提供的最常用的函数式接口及其函数描述符。这只是一个起点。如果有需要,你可以自己设计一个。请记住,(T,U) -> R
的表达方式展示了应当如何思考一个函数描述符。这里它代表一个函数,具有两个参数,分别为泛型T
和U
,返回类型为R
。
函数式接口 | 函数描述符 | 原始类型特化 |
---|---|---|
Predicate< T> | T -> boolean | IntPredicate,LongPredicate,DoublePredicate |
Consumer< T> | T -> void | IntConsumer,LongConsumer,DoubleConsumer |
Function< T,R> | T -> R | IntFunction< R>,IntToDoubleFunction,IntToLongFunction,LongFunction< R>,LongToDoubleFunction,LongToIntFunction,DoubleFunction< R>,ToIntFunction< T>,ToDoubleFunction< T>,ToLongFunction< T> |
Supplier< T> | () -> T | BooleanSupplier,IntSupplier,LongSupplier,DoubleSupplier |
UnaryOperator< T> | T -> T | IntUnaryOperator,LongUnaryOperator,DoubleUnaryOperator |
BinaryOperator< T> | (T,T) -> T | IntBinaryOperator,LongBinaryOperator,DoubleBinaryOperator |
BiPredicate< L,R> | (L,R) -> boolean | |
BiConsumer< T,U> | (T,U) -> void | ObjIntConsumer< T>,ObjLongConsumer< T>,ObjDoubleConsumer< T> |
BiFunction |
(T,U) -> R | ToIntBiFunction< T,U>,ToLongBiFunction< T,U>,ToDoubleBiFunction< T,U> |
下表总结了一些使用案例、Lambda的例子,以及可使用的函数式接口。
使用案例 | Lambda的例子 | 对应的函数式接口 |
---|---|---|
布尔表达式 | (List< String> list) -> list.isEmpty() | Predicate< List< String>> |
创建对象 | () -> new Apple(10) | Supplier< Apple> |
消费一个对象 | (Apple a) -> System.out.println(a.getWeight()) | Consumer< Apple> |
从一个对象中选择/提取 | (String s) -> s.length() | Function< String, Integer> 或 ToIntFunction< String> |
合并两个值 | (int a,int b) -> a*b | IntBinaryOperator |
比较两个对象 | (Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) | Comparator< Apple> 或 BiFunction< Apple,Apple,Integer> 或 ToIntBiFunction< Apple,Apple> |
请注意,任何函数式接口都不允许抛出受检异常(checked exception
)。如果你需要Lambda
表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda
包在一个try/catch
块中。比如,前面介绍了一个新的函数式接口BufferedReaderProcessor
,它显式声明了一个IOException
:
@FunctionalInterface
public interface BufferedReaderProcessor{
String process(BufferedReader b) throws IOException;
}
BufferedReaderProcessor p=(BufferedReader br) -> br.readLine();
但是你可能是在使用一个接受函数式接口的API
,比如Function
,没有办法自己创建一个。这种情况下,你可以显式捕捉受检异常:
Function<BufferedReader,String> f = (BufferedReader b) -> {
try{
return b.readLine();
}
catch(IOException e){
throw new RuntimeException(e);
}
};
Lambda
的类型是从使用Lambda
的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda
表达式需要的类型称为目标类型。让我们通过一个例子,看看当你使用Lambda
表达式时背后发生了什么。
List<Apple> heavierThan150g = filter(inventory,(Apple a) -> a.getWeight() > 150);
下图概述了上述代码的类型检查过程。
类型检查过程可以分解为如下所示。
filter
方法的声明。Predicate
(目标类型)对象的第二个正式参数。Predicate
是一个函数式接口,定义了一个叫作test
的抽象方法。test
方法描述了一个函数描述符,它可以接受一个Apple
,并返回一个boolean
。filter
的任何实际参数都必须匹配这个要求。这段代码是有效的,因为我们所传递的Lambda
表达式也同样接受Apple
为参数,并返回一个boolean
。请注意,如果Lambda
表达式抛出一个异常,那么抽象方法所声明的throws
语句也必须与之匹配。
有了目标类型的概念,同一个Lambda
表达式就可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。比如,Callable
和PrivilegedAction
,这两个接口都代表着什么也不接受且返回一个泛型T的函数。因此,下面两个赋值是有效的:
Callable<Integer> c = () -> 42;
PrivilegedAction<Integer> p = () -> 42;
这里,第一个赋值的目标类型是Callable
,第二个赋值的目标类型是PrivilegedAction
。
同一个Lambda
可用于多个不同的函数式接口:
Comparator<Apple> c1 = (Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple,Apple> c2 = (Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple,Apple,Integer> c3 = (Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
那些熟悉Java
的演变的人会记得,Java 7
中已经引入了菱形运算符(<>
),利用泛型推断从上下文推断类型的思想(这一思想甚至可以追溯到更早的泛型方法)。一个类实例表达式可以出现在两个或更多不同的上下文中,并会像下面这样推断出适当的类型参数:
List<String> listOfStrings=new ArrayList<>();
List<Integer> listOfIntegers=new ArrayList<>();
特殊的void
兼容规则:如果一个Lambda
的主体是一个语句表达式,它就和一个返回void
的函数描述符兼容(当然需要参数列表也兼容)。例如,以下两行都是合法的,尽管List
的add
方法返回了一个boolean
,而不是Consumer
上下文(T->void
)所要求的void
:
//Predicate返回了一个boolean
Predicate<String> p = s -> list.add(s);
//Consumer返回了一个void
Consumer<String> b = s -> list.add(s);
Lambda
表达式可以从赋值的上下文、方法调用的上下文(参数和返回值),以及类型转换的上下文中获得目标类型。利用目标类型可以检查一个Lambda
是否可以用于某个特定的上下文。
Java
编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda
表达式,这意味着它也可以推断出适合Lambda
的签名,因为函数描述符可以通过目标类型来得到。
这样做的好处在于,编译器可以了解Lambda
表达式的参数类型,这样就可以在Lambda
语法中省去标注参数类型。换句话说,Java
编译器会像下面这样推断Lambda
的参数类型。当Lambda
仅有一个类型需要推断的参数时,参数名称两边的括号也可以省略。
List<Apple> greenApples=filter(inventory,a -> "green".equals(a.getColor())); //参数a没有显式类型
Lambda
表达式有多个参数,代码可读性的好处就更为明显。例如,你可以这样来创建一个Comparator
对象:
Comparator<Apple> c=(Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); //没有类型推断
Comparator<Apple> c=(a1,a2) -> a1.getWeight().compareTo(a2.getWeight()); //有类型推断
请注意,有时候显式写出类型更易读,有时候去掉它们更易读。没有什么法则说哪种更好;对于如何让代码更易读,程序员必须做出自己的选择。
我们迄今为止接触的所有Lambda
表达式都只用到了其主体里面的参数。但Lambda
表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。它们被称作捕获Lambda
。例如,下面的Lambda
捕获了portNumber
变量:
int portNumber=1337;
Runnable r=() -> System.out.println(portNumber);
尽管如此,还有一点点小麻烦:关于能对这些变量做什么有一些限制。Lambda
可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为final
,或事实上是final
。换句话说,Lambda
表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量this
。)例如,下面的代码无法编译,因为portNumber
变量被赋值两次:
int portNumber=1337;
Runnable r=() -> System.out.println(portNumber); //错误:Lambda表达式引用的局部变量必须是最终的(final)或事实上最终的
portNumber=31337;
为什么局部变量有这些限制?
第一,实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda
可以直接访问局部变量,而且Lambda
是在一个线程中使用的,则使用Lambda
的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java
在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。
第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(这种模式会阻碍很容易做到的并行处理)。
闭包(closure
)就是一个函数的实例,且它可以无限制地访问那个函数的非本地变量。例如,闭包可以作为参数传递给另一个函数。它也可以访问和修改其作用域之外的变量。
现在,Java 8
的Lambda
和匿名类可以做类似于闭包的事情:它们可以作为参数传递给方法,并且可以访问其作用域之外的变量。但有一个限制:它们不能修改定义Lambda
的方法的局部变量的内容。这些变量必须是隐式最终的。可以认为Lambda
是对值封闭,而不是对变量封闭。
如前所述,这种限制存在的原因在于局部变量保存在栈上,并且隐式表示它们仅限于其所在线程。如果允许捕获可改变的局部变量,就会引发造成线程不安全的新的可能性,而这是我们不想看到的(实例变量可以,因为它们保存在堆中,而堆是在线程之间共享的)。
可以把方法引用视为某些Lambda
的快捷写法。
方法引用让你可以重复使用现有的方法定义,并像Lambda
一样传递它们。在一些情况下,比起使用Lambda
表达式,它们似乎更易读,感觉也更自然。下面就是我们借助更新的Java 8 API
,用方法引用写的一个排序的例子:
之前:
inventory.sort((Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
现在(使用方法引用和java.util.Comparator.comparing
):
inventory.sort(comparing(Apple::getWeight)); //你的第一个方法引用
方法引用可以被看作仅仅调用特定方法的Lambda
的一种快捷写法。它的基本思想是,如果一个Lambda
代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。
事实上,方法引用就是让你根据已有的方法实现来创建Lambda
表达式。但是,显式地指明方法的名称,你的代码的可读性会更好。
它是如何工作的呢?当你需要使用方法引用时,目标引用放在分隔符::
前,方法的名称放在后面。例如,Apple:: getWeight
就是引用了Apple
类中定义的方法getWeight
。请记住,不需要括号,因为你没有实际调用这个方法。该方法引用就是Lambda
表达式(Apple a) -> a.getWeight()
的快捷写法。
下表给出了Java 8
中方法引用的其他一些例子。
Lambda | 等效的方法引用 |
---|---|
(Apple a) -> a.getWeight() | Apple: : getWeight |
() -> Thread.currentThread().dumpStack() | Thread.currentThread(): : dumpStack |
(str,i) -> str.substring(i) | String: : substring |
(String s) -> System.out.println(s) | System.out: : println |
你可以把方法引用看作针对仅仅涉及单一方法的Lambda
的语法糖,因为你表达同样的事情时要写的代码更少了。
方法引用主要有三类。
Integer
的parseInt
方法,写作Integer:: parseInt
)String
的length
方法,写作String:: length
)expensiveTransaction
用于存放Transaction
类型的对象,它支持实例方法getValue
,那么你就可以写expensiveTransaction:: getValue
)类似于String:: length
的第二种方法引用的思想就是你在引用一个对象的方法,而这个对象本身是Lambda
的一个参数。例如,Lambda
表达式(String s) -> s.toUppeCase()
可以写作String:: toUpperCase
。
但第三种方法引用指的是,你在Lambda
中调用一个已经存在的外部对象中的方法。例如,Lambda
表达式() -> expensiveTransaction.getValue()
可以写作expensiveTransaction:: getValue
。
依照一些简单的方子,我们就可以将Lambda
表达式重构为等价的方法引用,如下图所示。
请注意,还有针对构造函数、数组构造函数和父类调用(super-call
)的一些特殊形式的方法引用。
比如你想要对一个字符串的List
排序,忽略大小写。List
的sort
方法需要一个Comparator
作为参数。Comparator
描述了一个具有(T,T) -> int
签名的函数描述符。你可以利用String
类中的compareToIgnoreCase
方法来定义一个Lambda
表达式(注意compareToIgnoreCase
是String
类中预先定义的)。
List<String> str=Arrays.asList("a","b","A","B");
str.sort((s1,s2) -> s1.compareToIgnoreCase(s2));
Lambda
表达式的签名与Comparator
的函数描述符兼容。利用前面所述的方法,这个例子可以用方法引用改写成下面的样子:
List<String> str=Arrays.asList("a","b","A","B");
str.sort(String:: compareToIgnoreCase);
请注意,编译器会进行一种与Lambda
表达式类似的类型检查过程,来确定对于给定的函数式接口,这个方法引用是否有效:方法引用的签名必须和上下文类型匹配。
到目前为止,只展示了如何利用现有的方法实现和如何创建方法引用。但是你也可以对类的构造函数做类似的事情。
对于一个现有构造函数,你可以利用它的名称和关键字new
来创建它的一个引用:ClassName:: new
。它的功能与指向静态方法的引用类似。例如,假设有一个构造函数没有参数。它适合Supplier
的签名() -> Apple
。你可以这样做:
Supplier<Apple> c1=Apple:: new; //构造函数引用指向默认的Apple()构造函数
Apple a1=c1.get(); //调用Supplier的get方法将产生一个新的Apple
这就等价于:
Supplier<Apple> c1=() -> new Apple(); //利用默认构造函数创建Apple的Lambda表达式
Apple a1=c1.get(); //调用Supplier的get方法将产生一个新的Apple
如果你的构造函数的签名是Apple(Integer weight)
,那么它就适合Function
接口的签名,于是你可以这样写:
Function<Integer,Apple> c2=Apple::new; //指向Apple(Integerweight)的构造函数引用
Apple a2=c2.apply(110); //调用该Function函数的apply方法,并给出要求的重量,将产生一个Apple
这就等价于:
Function<Integer,Apple> c2=(weight) -> new Apple(weight); //用要求的重量创建一个Apple的Lambda表达式
Apple a2=c2.apply(110); //调用该Function函数的apply方法,并给出要求的重量,将产生一个新的Apple对象
在下面的代码中,一个由Integer
构成的List
中的每个元素都通过我们前面定义的类似的map
方法传递给了Apple
的构造函数,得到了一个具有不同重量苹果的List
:
List<Integer> weights=Arrays.asList(7,3,4,10);
List<Apple> apples=map(weights,Apple:: new); //将构造函数引用传递给map方法
public static List<Apple> map(List<Integer> list,Function<Integer,Apple> f){
List<Apple> result=new ArrayList<>();
for(Integer e:list){
result.add(f.apply(e));
}
return result;
}
如果你有一个具有两个参数的构造函数Apple(String color,Integer weight)
,那么它就适合BiFunction
接口的签名,于是你可以这样写:
BiFunction<String,Integer,Apple> c3=Apple:: new; //指向Apple(String color,Integer weight)的构造函数引用
Apple c3=c3.apply("green",110); //调用该BiFunction函数的apply方法,并给出要求的颜色和重量,将产生一个新的Apple对象
这就等价于:
BiFunction<String,Integer,Apple> c3=(color,weight) -> new Apple(color,weight); //用要求的颜色和重量创建一个Apple的Lambda表达式
Apple c3=c3.apply("green",110); //调用该BiFunction函数的apply方法,并给出要求的颜色和重量,将产生一个新的Apple对象
不将构造函数实例化却能够引用它,这个功能有一些有趣的应用。例如,你可以使用Map
来将构造函数映射到字符串值。你可以创建一个giveMeFruit
方法,给它一个String
和一个Integer
,它就可以创建出不同重量的各种水果:
static Map<String,Function<Integer,Fruit>> map=new HashMap<>();
static{
map.put("apple",Apple:: new);
map.put("orange",Orange:: new);
//etc...
}
public static Fruit giveMeFruit(String fruit,Integer weight){
return map.get(fruit.toLowerCase()) //你用map得到了一个Function
.apply(weight); //用Integer类型的weight参数调用Function的apply()方法将提供所要求的Fruit
}
下面将运用行为参数化、匿名类、Lambda
表达式和方法引用,实现用不同的排序策略给一个Apple
列表排序。我们想要实现的最终解决方案是这样的:
inventory.sort(comparing(Apple:: getWeight));
Java 8
的API
已经提供了一个List
可用的sort
方法,不用自己去实现它。那么最困难的部分已经搞定了!但是,如何把排序策略传递给sort
方法呢?sort
方法的签名是这样的:void sort(Comparator super E> c)
它需要一个Comparator
对象来比较两个Apple
!这就是在Java
中传递策略的方式:它们必须包裹在一个对象里。我们说sort
的行为被参数化了:传递给它的排序策略不同,其行为也会不同。你的第一个解决方案看上去是这样的:
public class AppleComparator implements Comparator<Apple>{
public int compare(Apple a1,Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());
你可以使用匿名类来改进解决方案,而不是实现一个Comparator
却只实例化一次:
inventory.sort(new Comparator<Apple>(){
public int compare(Apple a1,Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
现在代码还是太啰嗦了,Java 8
引入了Lambda
表达式,它提供了一种轻量级语法来实现相同的目标:传递代码。在需要函数式接口的地方可以使用Lambda
表达式。
函数式接口就是仅仅定义一个抽象方法的接口。抽象方法的签名(称为函数描述符)描述了Lambda
表达式的签名。
在这个例子里,Comparator
代表了函数描述符(T,T) -> int
。因为这里用的是苹果,所以它具体代表的就是(Apple,Apple) -> int
。改进后的新解决方案看上去就是这样的了:
inventory.sort((Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
Java
编译器可以根据Lambda
出现的上下文来推断Lambda
表达式参数的类型。那么解决方案就可以重写成这样:
inventory.sort((a1,a2) -> a1.getWeight().compareTo(a2.getWeight()));
这段代码还能变得更易读一点吗?Comparator
具有一个叫作comparing
的静态辅助方法,它可以接受一个Function
来提取Comparable
键值,并生成一个Comparator
对象。它可以像下面这样用(注意你现在传递的Lambda
只有一个参数:Lambda
说明了如何从苹果中提取需要比较的键值):
Comparator<Apple> c=Comparator.comparing((Apple a) -> a.getWeight());
现在可以把代码再改得紧凑一点了:
import static java.util.Comparator.comparing;
inventory.sort(comparing((a) -> a.getWeight()));
方法引用就是替代那些转发参数的Lambda
表达式的语法糖。你可以用方法引用让你的代码更简洁(假设你静态导入了java.util.Comparator.comparing
):
inventory.sort(comparing(Apple:: getWeight));
这就是你的最终解决方案!这比Java 8
之前的代码好在哪儿呢?它比较短;它的意思也很明显,并且代码读起来和问题描述差不多:“对库存进行排序,比较苹果的重量。”
Java 8
的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递Lambda
表达式的Comparator
、Function
和Predicate
都提供了允许你进行复合的方法。
在实践中,这意味着你可以把多个简单的Lambda
复合成复杂的表达式。比如,你可以让两个谓词之间做一个or
操作,组合成一个更大的谓词。而且,你还可以让一个函数的结果成为另一个函数的输入。你可能会想,函数式接口中怎么可能有更多的方法呢?(毕竟,这违背了函数式接口的定义啊!)窍门在于,我们即将介绍的方法都是默认方法,也就是说它们不是抽象方法。
可以使用静态方法Comparator.comparing
,根据提取用于比较的键值的Function
来返回一个Comparator
,如下所示:
Comparator<Apple> c=Comparator.comparing(Apple:: getWeight);
如果你想要对苹果按重量递减排序怎么办?用不着去建立另一个Comparator
的实例。接口有一个默认方法reversed
可以使给定的比较器逆序。因此仍然用开始的那个比较器,只要修改一下前一个例子就可以对苹果按重量递减排序:
inventory.sort(comparing(Apple:: getWeight).reversed());←─按重量递减排序
但如果发现有两个苹果一样重怎么办?哪个苹果应该排在前面呢?你可能需要再提供一个Comparator
来进一步定义这个比较。比如,在按重量比较两个苹果之后,你可能想要按原产国排序。thenComparing
方法就是做这个用的。它接受一个函数作为参数(就像comparing
方法一样),如果两个对象用第一个Comparator
比较之后是一样的,就提供第二个Comparator
。你又可以优雅地解决这个问题了:
inventory.sort(comparing(Apple:: getWeight).reversed() //按重量递减排序
.thenComparing(Apple:: getCountry)); //两个苹果一样重时,进一步按国家排序
谓词接口包括三个方法:negate
、and
和or
,让你可以重用已有的Predicate
来创建更复杂的谓词。比如,你可以使用negate
方法来返回一个Predicate
的非,比如苹果不是红的:
Predicate<Apple> notRedApple=redApple.negate(); //产生现有Predicate对象redApple的非
你可能想要把两个Lambda
用and
方法组合起来,比如一个苹果既是红色又比较重:
Predicate<Apple> redAndHeavyApple=redApple.and(a -> a.getWeight() > 150); //链接两个谓词来生成另一个Predicate对象
你可以进一步组合谓词,表达要么是重(150
克以上)的红苹果,要么是绿苹果:
Predicate<Apple> redAndHeavyAppleOrGreen=
redApple.and(a -> a.getWeight() > 150)
.or(a -> "green".equals(a.getColor())); //链接Predicate的方法来构造更复杂Predicate对象
这一点为什么很好呢?从简单Lambda
表达式出发,你可以构建更复杂的表达式,但读起来仍然和问题的陈述差不多!请注意,and
和or
方法是按照在表达式链中的位置,从左向右确定优先级的。因此,a.or(b).and(c)
可以看作(a||b)&&c
。
最后,还可以把Function
接口所代表的Lambda
表达式复合起来。Function
接口为此配了andThen
和compose
两个默认方法,它们都会返回Function
的一个实例。
Comparator
、Predicate
和Function
等函数式接口都有几个可以用来结合Lambda
表达式的默认方法。
andThen
方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。比如,假设有一个函数f
给数字加1
(x -> x+1
),另一个函数g
给数字乘2
,你可以将它们组合成一个函数h
,先给数字加1
,再给结果乘2
:
Function<Integer,Integer> f = x -> x+1;
Function<Integer,Integer> g = x -> x*2;
Function<Integer,Integer> h = f.andThen(g); //数学上会写作g(f(x))或(g o f)(x)
int result=h.apply(1); //这将返回4
你也可以类似地使用compose
方法,先把给定的函数用作compose
的参数里面给的那个函数,然后再把函数本身用于结果。比如在上一个例子里用compose
的话,它将意味着f(g(x))
,而andThen
则意味着g(f(x))
:
Function<Integer,Integer> f = x -> x+1;
Function<Integer,Integer> g = x -> x*2;
Function<Integer,Integer> h = f.compose(g); //数学上会写作f(g(x))或(f o g)(x)
int result = h.apply(1); //这将返回3
下图说明了andThen
和compose
之间的区别。
那么这些在实际中这有什么用呢?比如你有一系列工具方法,对用String
表示的一封信做文本转换:
public class Letter{
public static String addHeader(String text){
return "From Raoul,Mario and Alan:"+text;
}
public static String addFooter(String text){
return text+"Kind regards";
}
public static String checkSpelling(String text){
return text.replaceAll("labda","lambda");
}
}
现在你可以通过复合这些工具方法来创建各种转型流水线了,比如创建一个流水线:先加上抬头,然后进行拼写检查,最后加上一个落款,如下图所示。
Function<String,String> addHeader=Letter:: addHeader;
Function<String,String> transformationPipeline
=addHeader.andThen(Letter:: checkSpelling)
.andThen(Letter:: addFooter);
Function<String,String> addHeader = Letter:: addHeader;
Function<String,String> transformationPipeline = addHeader.andThen(Letter:: addFooter);