关于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());

Java语言设计者选择这样的语法,是因为C#和Scala等语言中的类似功能广受欢迎。 Lambda的基本语法是
(parameters) -> expression

Supplier<String> supplier = (String str) -> "hi";

或(请注意语句的花括号)
(parameters) -> { statements; }

Comparator<String> stringComparator = (String str1, String str2) -> {
	return str1.compareTo(str2);
};

异常、 Lambda,还有函数式接口又是怎么回事呢?


请注意,任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda 表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda 包在一个try/catch块中。
比如,在3.3节我们介绍了一个新的函数式接口BufferedReaderProcessor,它显式声 明了一个IOException:

@FunctionalInterface
public interface BufferedReaderProcessor {
	String process(BufferedReader b) throws IOException;
}
BufferedReaderProcessor p = (BufferedReader br) -> br.readLine();

但是你可能是在使用一个接受函数式接口的API,比如Function<T, R>,没有办法自己 创建一个(你会在下一章看到, Stream API中大量使用表3-2中的函数式接口)。这种情况下, 你可以显式捕捉受检异常:

Function<BufferedReader, String> f = (BufferedReader b) -> {
	try {
		return b.readLine();
	}
	catch(IOException e) {
		throw new RuntimeException(e);
	}
};

在Lambda表达式中使用局部变量


我们迄今为止所介绍的所有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);
portNumber = 31337;

对局部变量的限制
你可能会问自己,为什么局部变量有这些限制。

第一,实例变量和局部变量背后的实现有一 个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此, Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。

第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式。

复合 Lambda 表达式


Java 8的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递Lambda表达式的Comparator、Function和Predicate都提供了允许你进行复合的方法。 这是什么意思呢?在实践中,这意味着你可以把多个简单的Lambda复合成复杂的表达式。比如, 你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你还可以让一个函数的结 果成为另一个函数的输入。你可能会想,函数式接口中怎么可能有更多的方法呢?(毕竟,这违背了函数式接口的定义啊!)窍门在于,我们即将介绍的方法都是默认方法,也就是说它们不是抽象方法。
1.比较器复合
我们前面看到,你可以使用静态方法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));		//两个苹果一样重时,进一步按国家排序

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

Predicate<Apple> notRedApple = redApple.negate();

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

Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);

你可以进一步组合谓词,表达要么是重(150克以上)的红苹果,要么是绿苹果:

Predicate<Apple> redAndHeavyAppleOrGreen =
	redApple.and(a -> a.getWeight() > 150)
		.or(a -> "green".equals(a.getColor()));

and和or方法是按照在表达式链中的位置,从左向右确定优先级的。因此, a.or(b).and(c)可以看作(a || b) && c。

函数复合


最后,你还可以把Function接口所代表的Lambda表达式复合起来。 Function接口为此配了andThen和compose两个默认方法,它们都会返回Function的一个实例。
andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。比如,假设有一个函数f给数字加1 (x -> x + 1),另一个函数g给数字乘2,你可以将它们组合成一个函数h,先给数字加1,再给结果乘2:

//数学上会写作g(f(x))或(g o f)(x)
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
int result = h.apply(1);

你也可以类似地使用compose方法,先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果。比如在上一个例子里用compose的话,它将意味着f(g(x)),而andThen则意味着g(f(x)):

//数学上会写作f(g(x))或(f o g)(x)
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1);

转载于:https://my.oschina.net/depeng414/blog/3054184

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值