Java 8 学习笔记3——Lambda 表达式

Lambda 表达式简介

利用行为参数化来传递代码有助于应对不断变化的需求。它允许你定义一个代码块来表示一个行为,然后传递它。你可以决定在某一事件发生时(例如单击一个按钮)或在算法中的某个特定时刻(例如筛选算法中类似于“重量超过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());

解析如下:

  • 参数列表——这里它采用了Comparatorcompare方法的参数,两个Apple
  • 箭头——箭头->把参数列表与Lambda主体分隔开。
  • 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 表达式基本语法和例子

Lambda基本语法如下:

(parameters) -> expression

或(注意语句的花括号)

(parameters) -> { statements; }

下表是一些Lambda的例子和使用案例:

使用案例 Lambda示例
布尔表达式 (List<String> list) -> list.isEmpty()
创建对象 () -> 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表达式。

函数式接口

函数式接口就是只定义一个抽象方法的接口。注意,接口现在还可以拥有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。

用函数式接口可以干什么呢?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付诸实践:环绕执行模式

环绕执行模式(即在方法所必需的代码中间,你需要执行点儿什么操作,比如资源分配和清理)可以配合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();		//这就是做有用工作的那行代码
	}
}

第1步,行为参数化

现在上面这段代码是有局限的。你只能读文件的第一行。如果你想要返回头两行,甚至是返回使用最频繁的词,该怎么办呢?在理想的情况下,你要重用执行设置和清理的代码,并告诉processFile方法对文件执行不同的操作。那么,你需要把processFile的行为参数化。你需要一种方法把行为传递给processFile,以便它可以利用BufferedReader执行不同的行为。

传递行为正是Lambda的拿手好戏。如果想一次读两行,在这个新的processFile方法中,基本上,你需要一个接收BufferedReader并返回StringLambda。例如,下面就是从BufferedReader中打印两行的写法:

String result=processFile((BufferedReader br) -> br.readLine()+br.readLine());

第2步,使用函数式接口来传递行为

Lambda仅可用于上下文是函数式接口的情况。你需要创建一个能匹配BufferedReader->String,还可以抛出IOException异常的接口:

@FunctionalInterface
public interface BufferedReaderProcessor{
	String process(BufferedReader b) throws IOException;
}

现在你就可以把这个接口作为新的processFile方法的参数了:

public static String processFile(BufferedReaderProcessor p) throws IOException{}

第3步,执行一个行为

任何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对象
	}
}

第4步,传递Lambda

现在你就可以通过传递不同的Lambda重用processFile方法,并以不同的方式处理文件了。
处理一行:

String oneLine=processFile((BufferedReader br) -> br.readLine());

处理两行:

String twoLines=processFile((BufferedReader br) -> br.readLine()+br.readLine());

四个步骤总结如下:
在这里插入图片描述
上面说明了如何利用函数式接口来传递Lambda,但你还是得定义你自己的接口。

使用函数式接口

函数式接口定义且只定义了一个抽象方法。函数式接口很有用,因为抽象方法的签名可以描述Lambda表达式的签名。函数式接口的抽象方法的签名称为函数描述符

为了应用不同的Lambda表达式,你需要一套能够描述常见函数描述符的函数式接口。Java API中已经有了几个函数式接口,比如ComparableRunnableCallable

Java 8的库设计师帮你在java.util.function包中引入了几个新的函数式接口。我们接下来会介绍PredicateConsumerFunction

Predicate

java.util.function.Predicate<T>接口定义了一个名叫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);

Consumer

java.util.function.Consumer<T>定义了一个名叫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方法的实现
);

Function

java.util.function.Function<T,R>接口定义了一个叫作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<T>Consumer<T>Function<T,R>。还有些函数式接口专为某些类型而设计。

Java类型要么是引用类型(比如ByteIntegerObjectList),要么是原始类型(比如intdoublebytechar)。但是泛型(比如Consumer<T>中的T)只能绑定到引用类型。这是由泛型内部的实现方式造成的。

因此,在Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫作装箱boxing)。相反的操作,也就是将引用类型转换为对应的原始类型,叫作拆箱unboxing)。Java还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操作是自动完成的。

但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值

Java 8为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。比如,在下面的代码中,使用IntPredicate就避免了对值1000进行装箱操作,但要是用Predicate<Integer>就会把参数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<T>Function<T,R>Supplier<T>Consumer<T>BinaryOperator<T>
为了避免装箱操作,对Predicate<T>Function<T,R>等通用函数式接口的原始类型特化:IntPredicateIntToLongFunction等。

一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如DoublePredicateIntConsumerLongBinaryOperatorIntFunction等。Function接口还有针对输出参数类型的变种ToIntFunction<T>IntToDoubleFunction等。

下表总结了Java API中提供的最常用的函数式接口及其函数描述符。这只是一个起点。如果有需要,你可以自己设计一个。请记住,(T,U) -> R的表达方式展示了应当如何思考一个函数描述符。这里它代表一个函数,具有两个参数,分别为泛型TU,返回类型为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> (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<T,R>,没有办法自己创建一个。这种情况下,你可以显式捕捉受检异常:

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<Apple>(目标类型)对象的第二个正式参数。
  • 第三,Predicate<Apple>是一个函数式接口,定义了一个叫作test的抽象方法。
  • 第四,test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean
  • 最后,filter的任何实际参数都必须匹配这个要求。

这段代码是有效的,因为我们所传递的Lambda表达式也同样接受Apple为参数,并返回一个boolean。请注意,如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必须与之匹配。

同样的Lambda,不同的函数式接口

有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。比如,CallablePrivilegedAction,这两个接口都代表着什么也不接受且返回一个泛型T的函数。因此,下面两个赋值是有效的:

Callable<Integer> c = () -> 42; 
PrivilegedAction<Integer> p = () -> 42;

这里,第一个赋值的目标类型是Callable<Integer>,第二个赋值的目标类型是PrivilegedAction<Integer>

同一个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的函数描述符兼容(当然需要参数列表也兼容)。例如,以下两行都是合法的,尽管Listadd方法返回了一个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 8Lambda和匿名类可以做类似于闭包的事情:它们可以作为参数传递给方法,并且可以访问其作用域之外的变量。但有一个限制:它们不能修改定义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的语法糖,因为你表达同样的事情时要写的代码更少了。

方法引用主要有三类。

  • 指向静态方法的方法引用(例如IntegerparseInt方法,写作Integer:: parseInt
  • 指向任意类型实例方法的方法引用(例如Stringlength方法,写作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排序,忽略大小写。Listsort方法需要一个Comparator作为参数。Comparator描述了一个具有(T,T) -> int签名的函数描述符。你可以利用String类中的compareToIgnoreCase方法来定义一个Lambda表达式(注意compareToIgnoreCaseString类中预先定义的)。

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<Integer,Fruit>
				.apply(weight);	//用Integer类型的weight参数调用Function的apply()方法将提供所要求的Fruit
}

Lambda和方法引用实战

下面将运用行为参数化、匿名类、Lambda表达式和方法引用,实现用不同的排序策略给一个Apple列表排序。我们想要实现的最终解决方案是这样的:

inventory.sort(comparing(Apple:: getWeight));

第1步,传递代码

Java 8API已经提供了一个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());

第2步,使用匿名类

你可以使用匿名类来改进解决方案,而不是实现一个Comparator却只实例化一次:

inventory.sort(new Comparator<Apple>(){
	public int compare(Apple a1,Apple a2){
		return a1.getWeight().compareTo(a2.getWeight());
	}
});

第3步,使用Lambda表达式

现在代码还是太啰嗦了,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()));

第4步,使用方法引用

方法引用就是替代那些转发参数的Lambda表达式的语法糖。你可以用方法引用让你的代码更简洁(假设你静态导入了java.util.Comparator.comparing):

inventory.sort(comparing(Apple:: getWeight));

这就是你的最终解决方案!这比Java 8之前的代码好在哪儿呢?它比较短;它的意思也很明显,并且代码读起来和问题描述差不多:“对库存进行排序,比较苹果的重量。”

复合Lambda表达式的有用方法

Java 8的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递Lambda表达式的ComparatorFunctionPredicate都提供了允许你进行复合的方法。

在实践中,这意味着你可以把多个简单的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));	//两个苹果一样重时,进一步按国家排序

谓词复合

谓词接口包括三个方法:negateandor,让你可以重用已有的Predicate来创建更复杂的谓词。比如,你可以使用negate方法来返回一个Predicate的非,比如苹果不是红的:

Predicate<Apple> notRedApple=redApple.negate();	//产生现有Predicate对象redApple的非

你可能想要把两个Lambdaand方法组合起来,比如一个苹果既是红色又比较重:

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表达式出发,你可以构建更复杂的表达式,但读起来仍然和问题的陈述差不多!请注意,andor方法是按照在表达式链中的位置,从左向右确定优先级的。因此,a.or(b).and(c)可以看作(a||b)&&c

函数复合

最后,还可以把Function接口所代表的Lambda表达式复合起来。Function接口为此配了andThencompose两个默认方法,它们都会返回Function的一个实例。

ComparatorPredicateFunction等函数式接口都有几个可以用来结合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

下图说明了andThencompose之间的区别。
在这里插入图片描述
那么这些在实际中这有什么用呢?比如你有一系列工具方法,对用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);

猜你喜欢

转载自blog.csdn.net/qq_43415405/article/details/84958702
今日推荐