lambda表达式详解
一.问题
1.什么是lambda表达式?
2.lambda表达式用来干什么的?
3.lambda表达式的优缺点?
4.lambda表达式的使用场景?
5.lambda只是一个语法糖吗?
二.概念
lambda表达式是JAVA8中提供的一种新的特性,它支持Java也能进行简单的“函数式编程”。
它是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。
三.先看看效果
先看几个例子:
1.使用lambda表达式实现Runnable
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
2.使用lambda表达式实现Comparator
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
3.使用lambda表达式实现ActionListener
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
四.来由
好了,通过上述的几个例子,大家差不多也能明白了lambda是用来干什么以及好处了。
显而易见的,好处就是代码量大大减少了!程序逻辑也很清晰明了。
它的用处浅显来说就是替代“内部匿名类”、可以对集合或者数组进行循环操作。
以前:
面向对象式编程就应该纯粹的面向对象,于是经常看到这样的写法:
如果你想写一个方法,那么就必须把它放到一个类里面,然后new出来对象,对象调用这个方法。
匿名类型最大的问题就在于其冗余的语法。
有人戏称匿名类型导致了“高度问题”(height problem):
比如大多匿名内部类的多行代码中仅有一行在做实际工作。
因此JAVA8中就提供了这种“函数式编程”的方法 —— lambda表达式,供我们来更加简明扼要的实现内部匿名类的功能。
五.什么时候可以使用它?
先说一个名词的概念
函数式接口:Functional Interface.
定义的一个接口,接口里面必须 有且只有一个抽象方法 ,这样的接口就成为函数式接口。
在可以使用lambda表达式的地方,方法声明时必须包含一个函数式的接口。
(JAVA8的接口可以有多个default方法)
任何函数式接口都可以使用lambda表达式替换。
例如:ActionListener、Comparator、Runnable
lambda表达式只能出现在目标类型为函数式接口的上下文中。
注意:
此处是只能!!!
意味着如果我们提供的这个接口包含一个以上的Abstract Method,那么使用lambda表达式则会报错。
这点已经验证过了。
场景:
这种场景其实很常见:
你在某处就真的只需要一个能做一件事情的函数而已,连它叫什么名字都无关紧要。
Lambda 表达式就可以用来做这件事。
六.写法、规则
基本语法:
(parameters) -> expression 或 (parameters) ->{ statements; }
即: 参数 -> 带返回值的表达式/无返回值的陈述
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
七.几个特性
1. 类型推导
编译器负责推导lambda表达式的类型。它利用lambda表达式所在上下文所期待的类型进行推导,
这个被期待的类型被称为目标类型。就是说我们传入的参数可以无需写类型了!
2.变量捕获
在Java SE 7中,编译器对内部类中引用的外部变量(即捕获的变量)要求非常严格:
如果捕获的变量没有被声明为final就会产生一个编译错误。
我们现在放宽了这个限制——对于lambda表达式和内部类,
我们允许在其中捕获那些符合有效只读(Effectively final)的局部变量。
简单的说,如果一个局部变量在初始化后从未被修改过,那么它就符合有效只读的要求,
换句话说,加上final后也不会导致编译错误的局部变量就是有效只读变量。
注意:此处和final关键字一样,指的是引用不可改!(感觉没多大意义,还不是用的final)
3.方法引用
如果我们想要调用的方法拥有一个名字,我们就可以通过它的名字直接调用它。
Comparator byName = Comparator.comparing(Person::getName);
此处无需再传入参数,lambda会自动装配成Person类型进来然后执行getName()方法,而后返回getName()的String
方法引用有很多种,它们的语法如下:
静态方法引用:ClassName::methodName
实例上的实例方法引用:instanceReference::methodName
超类上的实例方法引用:super::methodName
类型上的实例方法引用:ClassName::methodName
构造方法引用:Class::new
数组构造方法引用:TypeName[]::new
4.JAVA提供给我们的SAM接口
java SE 8中增加了一个新的包:java.util.function,它里面包含了常用的函数式接口,例如:
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
那么在参数为这些接口的地方,我们就可以直接使用lambda表达式了!
八.更多的例子
1.自定义SAM接口,从而使用lambda表达式
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
2.使用方法引用( ClassName::Method,无括号)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
3.使用lambda表达式完成for-each循环操作
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
list.forEach()是JAVA8的新方法,支持函数式编程,此处使用的参数就是JAVA提供给我们的函数式接口:Consumer< T>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
4.一个完整的例子
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
化简流程:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
九.优缺点
优点:
1.极大的简化代码。去除了很多无用的Java代码,使得代码更为简洁明了。
2.比匿名内部类更加高效(不确定)。编译器会生成专门的lambda方法,可以使用javap -p查看编译过后的代码
缺点:
1.可读性差。在代码简洁的情况下,另一方面又让大多程序员很难读懂。因为很少程序员接触使用它。
(不过这个缺点不是本身缺点,而且源于程序员较少使用)
十.它是一个语法糖吗?
答:
就我自身的理解来说,lambda表达式不算是一个语法糖。
语法糖就是说只是帮助我们程序员轻松的少写一些代码,之后编译器帮我们把那部分代码生成出来。
但是从编译过后的结果来说,并不是自动帮我们生成一个内部匿名类,而是生成了一个lambda$X方法。
第二个就是lambda其实表达的是目前流行的“函数式编程”这种思维。区别于我们面向对象的思维方法。
这点我认为很有意义,即我们要从各种思维来对待事情。而不是说,面向对象的这种方法就是最NB的。
但是论坛基本都认为这是一个语法糖,也没错。毕竟它提倡的只是一种思想,而且jdk底层为lambda生成了新的高效的代码这个事儿并不确定。
接下来介绍 lambda的 好哥们:stream.
stream的方法里面大多都使用了lambda表达式
stream概要
一.什么是stream?
官方解释:
- 1
- 1
简单来讲,stream就是JAVA8提供给我们的对于元素集合统一、快速、并行操作的一种方式。
它能充分运用多核的优势,以及配合lambda表达式、链式结构对集合等进行许多有用的操作。
概念:
stream:可以支持顺序和并行对元素操作的元素集合。
作用:
提供了一种操作大数据接口,让数据操作更容易和更快
使用stream,我们能够对collection的元素进行过滤、映射、排序、去重等许多操作。
中间方法和终点方法:
它具有过滤、映射以及减少遍历数等方法,这些方法分两种:中间方法和终端方法,
“流”抽象天生就该是持续的,中间方法永远返回的是Stream,因此如果我们要获取最终结果的话,
必须使用终点操作才能收集流产生的最终结果。区分这两个方法是看他的返回值,
如果是Stream则是中间方法,否则是终点方法
二.如何使用stream?
1.通过Stream接口的静态工厂方法(注意:Java8里接口可以带静态方法);
2.通过Collection接口的默认方法(默认方法:Default method,也是Java8中的一个新特性,就是接口中的一个带有实现的方法)–stream(),把一个Collection对象转换成Stream
一般情况下,我们都使用Collection接口的 .stream()方法得到stream.
三.常见的几个中间方法
中间方法即是一些列对元素进行的操作。譬如过滤、去重、截断等。
1.Filter(过滤)
- 1
- 2
- 3
- 1
- 2
- 3
2.Map(对元素进行操作)
- 1
- 2
- 1
- 2
3.limit(截断)
对一个Stream进行截断操作,获取其前N个元素,如果原Stream中包含的元素个数小于N,那就获取其所有的元素
4.distinct(去重)
对于Stream中包含的元素进行去重操作(去重逻辑依赖元素的equals方法),新生成的Stream中没有重复的元素
四.常用的终点方法
通过中间方法,我们对stream的元素进行了统一的操作,但是中间方法得到还是一个stream。要想把它转换为新的集合、或者是统计等。我们需要使用终点方法。
1.count(统计)
count方法是一个流的终点方法,可使流的结果最终统计,返回int,比如我们计算一下满足18岁的总人数
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
2.Collect(收集流的结果)
collect方法也是一个流的终点方法,可收集最终的结果
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
五.顺序流和并行流
每个Stream都有两种模式:顺序执行和并行执行。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
顾名思义,当使用顺序方式去遍历时,每个item读完后再读下一个item。
而使用并行去遍历时,数组会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。
并行流原理:
List originalList = someData;
split1 = originalList(0, mid);//将数据分小部分
split2 = originalList(mid,end);
new Runnable(split1.process());//小部分执行操作
new Runnable(split2.process());
List revisedList = split1 + split2;//将结果合并
性能:如果是多核机器,理论上并行流则会比顺序流快上一倍。
以下是借用他人的一个测试两者性能的Demo.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
运行结果:
- 1
- 2
- 1
- 2
可以看出,并行流的效率确实提高了3.5倍(我本机是4核,电脑较差.)
进阶学习:
1.Predicate和Consumer接口– Java 8中java.util.function包下的接口:
http://ifeve.com/predicate-and-consumer-interface-in-java-util-function-package-in-java-8/
2.深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)
http://zh.lucida.me/blog/java-8-lambdas-insideout-language-features/
参考资料:
1.Java8初体验(二)Stream语法详解:
http://ifeve.com/stream/
2.Java8初体验(一)lambda表达式语法
http://ifeve.com/lambda/