1-06-2 Lambda表达式
last modified:2020/10/31
1-06-3-Lambda表达式
6.3.1 为什么引入lambda表达式
- lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。
- 将一个代码块传递到某个对象,这个代码块会在将来某个时间调用。
6.3.2 lambda表达式的语法
带参数变量的表达式被称为lambda表达式。
你已经见过Java中的一种lambda表达式形式:参数,箭头(->)以及一个表达式。
- 如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在中,并包含显式的return语句。例如:
(String first,String second)->
{
if (first.1ength() < second.length()) return -1;
else if (first.length() > second.length()) return 1;
else return 0;
}
- 即使lambda表达式没有参数,仍然要提供空括号,就像无参数方法一样:
() -> { for (int i = 100; i >= 0; i--) System.out.prinln(i); }
- 如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型。例如:
Comparator<String> comp
(first,second) // Same as (String first,String second)
-> first.length() - second.length();
- 在这里,编译器可以推导出first和second必然是字符串,因为这个lambda表达式将赋给一个字符串比较器。(下一节会更详细地分析这个赋值。)
- 如果方法只有一个参数,而且这个参数的类型可以推导得出,那么甚至还可以省略小括号:
ActionListener listener = event ->
System.out.println("The time is " + new Date()");
// Instead of (event) -> .. . or (ActionEvent event) -> ..·
- 无需指定lambda表达式的返回类型。lambda表达式的返回类型总是会由上下文推导得出。例如,下面的表达式
(String first, String second)-> first.length() - second.length();
- 可以在需要int类型结果的上下文中使用。
- 如果一个lambda表达式只在某些分支返回一个值,而在另外一些分支不返回值,这是不合法的。例如,(int x)->{ if(x>= 0) return1; }就不合法。
//程序显示了如何在一个比较器和一个动作监听器中使用lambda表达式
public class LambdaTest
{
public static void main(String[]args)
{
String[] planets = new String[]{ "Mercury","Venus","Earth”,
"Mars","Jupiter","Saturn","Uranus","Neptune"};
System.out.println(Arrays.toString(planets));
system.out.println("Sorted in dictionary order:");
Arrays.sort(planets);
System.out.println(Arrays.toString(planets));
system.out.println("Sorted by length:");
Arrays.sort(planets,(first,second)->first.length()-second.length());
System.out.print1n(Arrays.toString(planets));
Timer t = new Timer(1000, event->
System.out.println("The time is" +new Date());
t.start();
// keep program running until user selects "ok"
optionPane.showMessageDialog(null,"Quit program?");
System.exit(O);
}
}
6.3.3 函数式接口
Java中已经又很多封装代码块的接口,lambda表达式与这些接口是兼容的。
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口(functional interface)。
注释:你可能想知道为什么函数式接口必须有一个抽象方法。不是接口中的所有方法都是抽象的吗?实际上,接口完全有可能重新声明Object类的方法,如toString 或clone,这些声明有可能会让方法不再是抽象的。(Java API中的一些接口会重新声明Object方法来附加javadoc注释。Comparator API就是这样一个例子。)更重要的是,正如6.1.5节所述,在Java SE 8中,接口可以声明非抽象方法。
为了展示如何转换为函数式接口,下面考虑Arrays.sort方法。
它的第二个参数需要一个Comparator实例,Comparator就是只有一个方法的接口,所以可以提供一个lambda表达式:
Arrays.sort(words,
(first, second)->first.length()- second.length();
在底层,Arrays.sort方法会接收实现了Comparator<String>的某个类的对象。在这个对象上调用compare方法会执行这个lambda表达式的体。这些对象和类的管理完全取决于具体实现,与使用传统的内联类相比,这样可能要高效得多。最好把lambda表达式看作是一个函数,而不是一个对象,另外要接受lambda表达式可以传递到函数式接口。
实际上,在java中,对lambda表达式所能做的也只是能转换为函数式接口。
Java API在java.util.function包中定义了很多非常通用的函数式接口。
其中一个接口BiFunction<T,U,R>描述了参数类型为T和U而且返回类型为R的函数。可以把我们的字符串比较lambda表达式保存在这个类型的变量中:
BiFunction<String,String,Integer> comp
= (first,second)-> first.length() - second.length();
不过,这对于排序并没有帮助。没有哪个Arrays.sort方法想要接收一个BiFunction。如果你之前用过某种函数式程序设计语言,可能会发现这很奇怪。不过,对于Java程序员而言,这非常自然。类似Comparator的接口往往有一个特定的用途,而不只是提供一个有指定参数和返回类型的方法。Java SE8沿袭了这种思路。想要用lambda表达式做某些处理,还是要谨记表达式的用途,为它建立一个特定的函数式接口。
java.util.function包中有一个尤其有用的接口Predicate:
public interface Predicate<T>
{
boolean test(T t);
//Additional default and static methods
}
ArrayList类有一个removelf方法,它的参数就是一个Predicate。这个接口专门用来传递lambda表达式。例如,下面的语句将从一个数组列表删除所有null值:
list.removeIf(e -> e == null);
6.3.4 方法引用
有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。
例如,假设你希望只要出现一个定时器事件就打印这个事件对象。当然,为此也可以调用:
Timer t = new Timer(1000,event -> System.out.println(event));
但是,如果直接把printIn方法传递到Timer构造器就更好了。具体做法如下:
Timer t = new Timer(1000,System.out::println);
表达式System.out::printIn是一个方法引用(method reference),它等价于lambda表达式
x->System.out.println(x)
再来看一个例子,假设你想对字符串排序,而不考虑字母的大小写。可以传递以下方法表达式:
Arrays.sort(strings,String::compareToIgnoreCase)
从这些例子可以看出,要用::操作符分隔方法名与对象或类名。主要有3种情况:
- object::instanceMethod
- Class::staticMethod
- Class::instanceMethod
在前2种情况中,方法引用等价于提供方法参数的lambda表达式。前面已经提到,
System.out::println等价于x->System.out.printIn(x)。类似地,Math:pow等价于(x,y)->Math.pow(x,y)。
对于第3种情况,第1个参数会成为方法的目标。例如String::compareTolgnoreCase等同于(x,y)-> x.compareTolgnoreCase(y)。
如果有多个同名的重载方法,编译器就会尝试从上下文中找出你指的那一个方法。
- 例如,Math.max方法有两个版本,一个用于整数,另一个用于double值。选择哪一个版本取决于Math::max转换为哪个函数式接口的方法参数。类似于lambda表达式,方法引用不能独立存在,总是会转换为函数式接口的实例。
可以在方法引用中使用this参数。
- 例如,this::equals 等同于x -> this.equals(x)。使用super也是合法的。下面的方法表达式super::instanceMethod使用this 作为目标,会调用给定方法的超类版本。
6.3.5 构造器引用
构造器引用与方法引用很类似,只不过方法名为new。
例如,Person::new 是Person构造器的一个引用。哪个构造器呢?这取决于上下文
假设你有一个字符串列表。可以把它转换为一个Person对象数组,为此要在各个字符串上调用构造器,调用如下:
Arraylist<String> names= ... ;
// map方法会为各个列表元素调用Person(String)构造器
Strean<Person> strean = names.stream().map(Person::new) ;
List<Person> people = strean.collect(Collectors . tolist());
如果有多个Person构造器,编译器会选择有一个String参数的构造器,因为它从上下文推导出这是在对一个字符串调用构造器。
可以用数组类型建立构造器引用。
- 例如,int[]::new 是一个构造器引用,它有一个参数:即数组的长度。这等价于lambda表达式x -> new int[x]。
Java有一个限制,无法构造泛型类型T的数组。
数组构造器引用对于克服这个限制很有用。表达式new T[n]会产生错误,因为这会改为new Object[n]。对于开发类库的人来说,这是一个问题。例如,假设我们需要一个Person对象数组。Stream接口有一个toArray方法可以返回Object数组:
0bject[] people = stream.toArray();
不过,这并不让人满意。用户希望得到一个Person引用数组,而不是Object引用数组。流库利用构造器引用解决了这个问题。可以把Person[]:new传入toArray方法:
Person[] people = stream.toArray(Person[]::new);
toArray方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。
6.3.6 变量作用域
lambda 表达式有3个部分:
- 1)一个代码块;
- 2)参数;
- 3)自由变量的值,这是指非参数而且不在代码中定义的变量。
public static void repeatMessage(String text, int delay)
{
Actionlistener listener = event ->
{
System.out.println(text);
Toolkit.getDefaultToolkit().beep();
};
new Timer(delay,listener).start();
}
//来看这样一个调用:
repeatMessage("Hel1o",1000); // Prints Hel1o every 1,000 milliseconds
在我们的例子中,这个lambda表达式有1个自由变量text。表示lambda表达式的数据结构必须存储自由变量的值,在这里就是字符串"Hello"。我们说它被lambda表达式捕获
( captured)。(下面来看具体的实现细节。例如,可以把一个lambda表达式转换为包含一个方法的对象,这样自由变量的值就会复制到这个对象的实例变量中。)注释:关于代码块以及自由变量值有一个术语: 闭包( closure)。 如果有人吹噓他们的语言有闭包,现在你也可以自信地说Java也有闭包。在Java中,lambda表达式就是闭包。
可以看到,lambda表达式可以捕获外围作用域中变量的值。
在Java中,要确保所捕获的值是明确定义的,这里有一个重要的限制。
在lambda表达式中,只能引用值不会改变的变量。例如,下面的做法是不合法的:
public static void countDown(int start,int delay)
{
ActionListener listener = event ->{
start--; // Error: Can't mutate captured variable
System.out.println(start);
};
new Timer(delay,listener) .start();
}
另外如果在lambda表达式中引用变量,而这个变量可能在外部改变,这也是不合法的。
例如,下面就是不合法的:public static void repeat(String text,int count)
{
for (int i = 1; i <= count; i++)
{
ActionListener listener = event -> {
System.out.println(i + "; " + text);
// Error: Cannot refer to changing i
};
new Timer(1000 ,listener).start();
}
}
这里有一条规则: lambda 表达式中捕获的变量必须实际上是最终变量( effectively final)。实际上的最终变量是指,这个变量初始化之后就不会再为它赋新值。在这里,text 总是指示同一个String 对象,所以捕获这个变量是合法的。不过, i的值会改变,因此不能捕获i。
lambda表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则。
- 在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的。
Path first = Paths.get("/usr/bin");
Comparator<String> comp =
(first, second) -> first.length() - second.length();
// Error: Variable first already defined
在方法中,不能有两个同名的局部变量,因此,lambda表达式中同样也不能有同名的局部变量。
在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。例如,考虑下面的代码:
public class Application()
{
public void init()
{
ActionListener listener = event ->
{
//表达式this.toString()会调用Application对象的 //toString方法,而不是ActionListener实例的方法。
System.out.print1n(this. toString());
...
}
...
}
}
在lambda表达式中,this 的使用并没有任何特殊之处。lambda 表达式的作用域嵌套在init方法中,与出现在这个方法中的其他位置一样,lambda表达式中this的含义并没有变化。
6.3.7 处理lambda表达式
使用lambda表达式的重点是延迟执行( deferred execution)。
之所以希望以后再执行代码,这有很多原因,如:
- 在一个单独的线程中运行代码;
- 多次运行代码;
- 在算法的适当位置运行代码(例如,排序中的比较操作);
- 发生某种情况时执行代码(如,点击了一个按钮,数据到达,等等);
- 只在必要时才运行代码。
下面来看一个简单的例子。假设你想要重复一个动作n次。将这个动作和重复次数传递
到一个repeat方法:repeat(10, () -> System.out.println("Hello, World!"));
要接受这个lambda表达式,需要选择(偶尔可能需要提供)一个函数式接口。
表6-1列出了Java API中提供的最重要的函数式接口。
在这里,我们可以使用Runnable接口:
public static void repeat(int n, Runnable action)
{
for (int i =0; i < n; i++) action.run();
}
需要说明,调用action.run()时会执行这个lambda表达式的主体。
现在让这个例子更复杂一些。我们希望告诉这个动作它出现在哪一次迭代中。 为此,需
要选择一个合适的函数式接口,其中要包含一个方法,这个方法有一个int参数而且返回类
型为void。处理int值的标准接口如下:public interface IntConsumer{
void accept(int value);
}
下面给出repeat方法的改进版本:
public static void repeat(int n, IntConsumer action){
for (int i =0; i < n; i++) action.accept(i);
}
可以如下调用它:
repeat(10, i -> Sysem.out.pritnln("Countdown: "+ (9 - i)));
表6-2列出了基本类型int、long和double的34个可能的规范。最好使用这些特殊
化规范来减少自动装箱。出于这个原因,上一个例子中使用了IntConsumer 而不是
Consumer<Integer>。- 最好使用6-1或6-2中的接口
- 大多数标准函数式接口都提供了非抽象方法来生成或合并函数。
- 如,Predicate.isEqual(a)等同于a::equal,不过如果a为null也能正常工作。
- 如果设计你自己的接口,其中只有一个抽象方法,可以用@FunctionalInterface注
**解来标记这个接口。这样做有两个优点。- 如果你无意中增加了另一个非抽象方法,编译器会产生一个错误消息。
- 另外javadoc页里会指出你的接口是一个函数式接口。
- 并不是必须使用注解。根据定义,任何有一个抽象方法的接口都是函数式接口。不
过使用@FunctionalInterface 注解确实是一个很好的做法。
6.3.8 再谈Comparator(!!!)
Comparator接口包含很多方便的静态方法来创建比较器。这些方法可以用于lambda表达式或方法引用。
静态comparing方法取一个“键提取器”函数,它将类型T映射为一个可比较的类型
(如String)。对要比较的对象应用这个函数,然后对返回的键完成比较。例如,假设有一个Person对象数组,可以如下按名字对这些对象排序:
Arrays.sort(people,Comparator.comparing(Person:getName));
与手动实现一个Comparator相比,这当然要容易得多。另外,代码也更为清晰,因为显然我们都希望按人名来进行比较。
可以把比较器与thenComparing方法串起来。例如,Arrays.sort(people,Comparator.comparing(Person::getLastName)
.thenComparing(Person::getFirstNane));
如果两个人的姓相同,就会使用第二个比较器。
这些方法有很多变体形式。可以为comparing和thenComparing方法提取的键指定一个比较器。例如,可以如下根据人名长度完成排序:Arrays.sort(people, Comparator.comparing(Person:getName,
(s, t) -> Integer.compare(s.length(), t.length())));
另外,comparing 和thenComparing方法都有变体形式,可以避免int、long或double值的装箱。要完成前一个操作,还有一种更容易的做法:
Arrays. sort(people, Comparator . comparingInt(p -> p.getName(). length));
如果键函数可以返回null,可能就要用到nullFirst和nullsLast适配器。这些静态方
法会修改现有的比较器,从而在遇到null值时不会抛出异常,而是将这个值标记为小于或
大于正常值。例如,假设一个人没有中名时getMiddleName会返回一个nul,就可以使用Comparator.comparing(Person:.getMiddleName(), Comparator.nullsFirst(...))
nullsFirst方法需要一个比较器, 在这里就是比较两个字符串的比较器。
naturalOrder 方法可以为任何实现了Comparable的类建立一个比较器。在这里,Comparator.<String> naturalOrder()正是我们需要的。下面是一个完整的调用,可以按可能为null 的中名进行排序。这里使用了一个静态导入java.util.Comparator.*,以便理解这个表达式。注意naturalOrder的类型可以推导得出。
Arrays.sort(people,comparing(Person::getMiddleName,
nullsFirst(naturalOrder())));
静态reverseOrder方法会提供自然顺序的逆序。要让比较器逆序比较,可以使用reversed实例方法。例如naturalOrder.(reversed)等同于reverseOrder()。
1-06-2 Lambda表达式的更多相关文章
- 委托、Lambda表达式、事件系列06,使用Action实现观察者模式,体验委托和事件的区别
在"实现观察者模式(Observer Pattern)的2种方式"中,曾经通过接口的方式.委托与事件的方式实现过观察者模式.本篇体验使用Action实现此模式,并从中体验委托与事件 ...
- 背后的故事之 - 快乐的Lambda表达式(一)
快乐的Lambda表达式(二) 自从Lambda随.NET Framework3.5出现在.NET开发者眼前以来,它已经给我们带来了太多的欣喜.它优雅,对开发者更友好,能提高开发效率,天啊!它还有可能 ...
- JDK8 的 Lambda 表达式原理
JDK8 使用一行 Lambda 表达式可以代替先前用匿名类五六行代码所做的事情,那么它是怎么实现的呢?从所周知,匿名类会在编译的时候生成与宿主类带上 $1, $2 的类文件,如写在 TestLamb ...
- Lambda表达式动态拼接(备忘)
EntityFramework动态组合Lambda表达式作为数据筛选条件,代替拼接SQL语句 分类: C# Lambda/Linq Entity Framework 2013-05-24 06:58 ...
- 十分钟学会Java8的lambda表达式和Stream API
01:前言一直在用JDK8 ,却从未用过Stream,为了对数组或集合进行一些排序.过滤或数据处理,只会写for循环或者foreach,这就是我曾经的一个写照. 刚开始写写是打基础,但写的多了,各种乏 ...
- 【转】背后的故事之 - 快乐的Lambda表达式(一)
快乐的Lambda表达式(二) 自从Lambda随.NET Framework3.5出现在.NET开发者眼前以来,它已经给我们带来了太多的欣喜.它优雅,对开发者更友好,能提高开发效率,天啊!它还有可能 ...
- 快乐的Lambda表达式(一)
转载:http://www.cnblogs.com/jesse2013/p/happylambda.html 原文出处: Florian Rappl 译文出处:Jesse Liu 自从Lambda ...
- 委托、Lambda表达式、事件系列07,使用EventHandler委托
谈到事件注册,EventHandler是最常用的. EventHandler是一个委托,接收2个形参.sender是指事件的发起者,e代表事件参数. □ 使用EventHandler实现猜拳游戏 使用 ...
- 委托、Lambda表达式、事件系列05,Action委托与闭包
来看使用Action委托的一个实例: static void Main(string[] args) { int i = 0; Action a = () => i++; a(); a(); C ...
- 委托、Lambda表达式、事件系列04,委托链是怎样形成的, 多播委托, 调用委托链方法,委托链异常处理
委托是多播委托,我们可以通过"+="把多个方法赋给委托变量,这样就形成了一个委托链.本篇的话题包括:委托链是怎样形成的,如何调用委托链方法,以及委托链异常处理. □ 调用返回类型为 ...
随机推荐
- Nginx常见错误解决办法
报错: nginx: [error] CreateFile() "C:\mytools\nginx-1.8.1/logs/nginx.pid" failed (2: The sys ...
- C# 微信access_token缓存和过期刷新
摘自:http://blog.csdn.net/hechurui/article/details/22398849 首先建立一个Access_token类 /// <summary> // ...
- CC2530定时器模模式最大值计算
首先假设 频率: f 分频系数: n 间隔定时: s 周期: T 模模式最大值: N 因为 T = 1 / f 所以 s = ( n / f ) * N = n * N / f 由此可得 计算模模 ...
- Java8新特性--Base64转换
1.简介 在Java8中,Base64编码已经成为Java类库的标准.Java 8 内置了 Base64 编码的编码器和解码器. Base64工具类提供了一套静态方法获取下面三种BASE64编解码器: ...
- java默认值
注意:此处默认值是在类成员时才可以被初始化有默认值 如果时在局部变量中,必须先自己初始化才可以使用,否则编译失败
- mysql 改变表结构 alter
总结:alter添加栏位时,只需记住添加新栏位为第一列,用first;添加其他用,after 前一个栏位字段,如下例 1.需求:将新的栏位添加为第二列 添加前: 添加后: 参考:http://www. ...
- docker容器学习资料
现在说起docker容器,你应该不会太陌生了吧?如果你是真的不懂或者是太久没有用到已经忘记的差不多了,那么你需要这一波的干货了,这波的干货刚刚好可以满足你的需要! 话不多说,直接上干货
- 给大家分享一下java数据库操作步骤
获取驱动程序Jar文件,并放置到项目的类路径中: 注册驱动器类: 获取数据库连接: 获取Statement对象来执行相关SQL操作: 关闭各种资源;
- 21个写SQL的好习惯,你值得拥有
前言 每一个好习惯都是一笔财富,本文分SQL后悔药, SQL性能优化,SQL规范优雅三个方向,分享写SQL的21个好习惯,谢谢阅读,加油哈~ 公众号:捡田螺的小男孩 1. 写完SQL先explain查 ...
- STM32入门系列-创建寄存器模板
介绍如何使用 KEIL5 软件创建寄存器模板, 方便之后使用寄存器方式来操作STM32开发板上的LED,让大家创建属于自己的寄存器工程模板. 获取工程模板的基础文件 首先我们在电脑任意位置创建一个文件 ...