对于Java8其实相比之前的的版本增加的内容是相当多的,其中有相当一大块的内容是关于Lambda表达式与Stream API,而这两部分是紧密结合而不能将其拆开来对待的,但是是可以单独使用的,所以从学习的顺序来说首先得要学好Lambda表达式,然后再学习Stream API,最后再把这两者有机的结合起来,而这两部分涉及的知识体系又非常的多,很多东西都改变了以往对java这种面向对象语言的基本认识,所以下面一步步开始对Java8进行了解,先学好Lambda表达式。
何为Lambda表达式:
先看一个非常非常抽象的一个定义,如下:
上面看完~~可能还是一头雾水,不要着急,先对其定义有个大概的认识,重点是观注Lambda表达式是如何使用的。
为何需要Lambda表达式:
- 在Java中,我们无法将函数作为参数传递给一个方法,也无法声明返回一个函数的方法。
回想一下:对于java中方法的参数一定是数据类型【要么是基本数据、要么是引用数据】,同样的作为方法的返回值也只能是数据类型。但是在JDK8版本之后这些就变为可能。
- 在JavaScript中,函数参数是一个函数,返回值是另一上函数的情况是非常常见的;JavaScript是一个非常典型的函数式语言。
比如说举个例子:
这个在JavaScript中是非常常见的写法。
Java匿名内部类示例:
其实上图中的这种写法是非常违背直觉的,在点击按钮的时候其实就是执行某个方法既可,但是在传统的Java编程中还得要定义一个匿名的对象实现了View.OnclickListener,可见这种写法是比较繁琐啰嗦的,而在Java8之前也只能这么去实现。
Lambda表达式初步使用:
在上面理论过后下面编写代码来一步步引出Lambda表达式,这里依然是采用Intellij IDEA来写代码,这里采用gradle的方式来编写测试代码而不用之前学习java并发的那种方式了,具体如下:
接着就进行项目的初始化,初台化完成就可以正式编写代码了,下面开始从swing代码开始,因为它有按钮的事件,跟上面说的情况类似:
编译运行:
程序比较简单,重点不是看功能,而是在于代码分析,先来仔细观察一下代码:
把鼠标放到它上面,IDE就会有提示,提示信息如下:
说明IDE都已经检测到了这个匿名内部类的写法可以改用JDK8的Lambda表达式,为什么?这个在上面也已经谈到过了,因为对于这个匿名内部类,我们所要关心的只是其回调方法中的具体实现,如下:
那改用Lambda方式来替换目前这种匿名内部类的不人性的写法是怎么样呢?照IDE的提示来做:
顺间感觉代码精简了,但是也让人对新的写法产生了各种疑问,下面写的一些代码只要感受下就行,具体Lambda表达式之后会一步步深入:
这里可以变化一下写法就能知道实际是有类型的:
而之所以直接可以将类型省略掉是由于java编译系统借助于类型推断机制能推荐出来e一定是ActionEvent类型,所以说就没有必要再去定义类型了,当然加上也没毛病,只是多此一举而已,那是不是说java8的编译系统都能够都能推荐出这个类型是什么呢?不是的,有些时候根据上下文是推断不出来的,那此时就需要显示的来指定类型的。
根据上面的Lambda表达式的写法可以总结出它的大体样式:
(param1, param2, param3...) -> {
//执行体
}
当然关于Lambda表达式的东东不仅仅只是这点东西,之后再慢慢探究。
函数式接口(FunctionalInterface):
接下来继续举例来说明Lambda表达式,这里以集合遍历为例,从传统的方式一直演变成现在用Lambda表达式,其中会引出一个非常重要的概念---函数式接口,具体演变过程如下:
而接着在JDK1.5之后引入了增加的for循环,如下:
1
2
3
4
5
6
7
8
----------------------
1
2
3
4
5
6
7
8
接着到了JAVA8了,看在这个版本遍历集合有何特色:
输出:
1
2
3
4
5
6
7
8
----------------------
1
2
3
4
5
6
7
8
----------------------
1
2
3
4
5
6
7
8
其中在遍历中用到了Consumer这个接口,乍一看貌似表面上比之前的遍历方式还麻烦,不过这是暂时滴,先点进去对Consumer这个接口接口有个了解:
要了解这个注解当然是点进去进一步看一下它的源码喽:
package java.lang;
import java.lang.annotation.*;
/**
* An informative annotation type used to indicate that an interface
* type declaration is intended to be a functional interface as
* defined by the Java Language Specification.
*
* Conceptually, a functional interface has exactly one abstract
* method. Since {@linkplain java.lang.reflect.Method#isDefault()
* default methods} have an implementation, they are not abstract. If
* an interface declares an abstract method overriding one of the
* public methods of {@code java.lang.Object}, that also does
* not count toward the interface's abstract method count
* since any implementation of the interface will have an
* implementation from {@code java.lang.Object} or elsewhere.
*
* Note that instances of functional interfaces can be created with
* lambda expressions, method references, or constructor references.
*
*
If a type is annotated with this annotation type, compilers are
* required to generate an error message unless:
*
*
* - The type is an interface type and not an annotation type, enum, or class.
*
- The annotated type satisfies the requirements of a functional interface.
*
*
* However, the compiler will treat any interface meeting the
* definition of a functional interface as a functional interface
* regardless of whether or not a {@code FunctionalInterface}
* annotation is present on the interface declaration.
*
* @jls 4.3.2. The Class Object
* @jls 9.8 Functional Interfaces
* @jls 9.4.3 Interface Method Body
* @since 1.8
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}
关于什么是函数接口在之后还会学入学习,这里先读一读它的JavaDoc,对其有一个整体的认识,那下面就来读一读对它的描述喽:
按中文的意思来理解,也就是说明凡是一个接口上标有@FunctionalInterface注解,就可以将此接口称之为函数式接口。
光看这句话对于函数式接口还是一知半解的,那就接下来继续看下面的说明:
也就是说,如果一个接口有且只有一个抽象方法,则就可以将这个接口称之为函数式接口,这个概念在Java8的Lambda表达式出来之后是一个非常重要的概念,那看到这,一个新的疑问产生了:接口里面的方法不都是抽象的么?难道接口里面能有具体实现的方法,是的~~在java8出来之后这一切就成为可能了,当然在之前版本是不可能在接口中出现非抽象方法,所以在这句描述中就对函数式接口进行了一个基本定义,接下来继续往下看说明:
这处道出了函数式接口的非常重要的点,其中Lambda表达式我们之前已经使用过了,它可以用来创建函数式接口实例:
那其它两种创建方式具体又是什么呢?这个之后会学,先不用理会。继续往下读:
最后咱们对于这个JAVADOC的说明对其函数数接口做一个总结:
1、如果一个接口只有一个抽象方法,那么该接口就是一个函数式接口。
2、如果我们在某个接口上声明了FunctionalInterface注解,那么编译器就会按照函数式接口的定义来要求该接口。
3、如果某个接口只有一个抽象方法,但我们并没有给该接口声明FuncationalInterface注解,那么编译器依旧依旧会将该接口看作是函数式接口。(虽这样说,但是一般对于函数式接口建议还是加上这个注解的比较好。因为加了之后编译器会对接口增加一个强制性的保证,如果接口不满足某些条件的话是会报错提示的,就这好比子类重写父类的一个特定方法的时候,照理是应该在子类的这个方法上增加一个override方法,但是如果不加也没问题,加了的好处一是代码可读性比较好,二是如果覆写了父类中并不存在的方法那么编译器会第一时间提示出来,所以最好按照规则来:如果满足函数式接口一定要在接口上声明FuncationalInterface注解)
函数式接口:
函数式接口【FunctionalInterface】是整个Lambda表达式的一个根源,换句话来说java8中的Lambda表达式要想彻底掌握,前提是要彻底理解好函数式接口,所以这次继续对函数式接口进行巩固。
回顾一下上面通过读FunctionalInterface这个注解的javadoc之后的三点总结【参考:http://www.cnblogs.com/webor2006/p/8111585.html】:
关于FunctionalInterface的doc上有一个细节还需要注意,在上次中也已经提到过,这里再拧出来看一下:
那换成代码如何来理解上面这段话呢?新建一个接口,里面声明一个方法,当然它是抽象的【抽象的概念是只有声明木有具体实现的】:
那这个是不是FuncationalInterface呢?加上注解就可以论证了:
那如果再增加一个抽象方法:
看下报错提示:
那如果此时将这个新加的方法名称换一个是Object类中的呢?
那为什么呢?原因就如javadoc上面的这点所描述:toString()是一个抽象方法,但是Object中也有此方法,细心的可以发现其实开发工具比较智能的在该方法的左侧已经显示出来一个箭头,点击则可以查看它父类的方法:
那点开看一下呗:
很显然该方法是复写的Object类的中方法,所以java编译器不认为该方法是一个抽象方法,所以当然整个接口还是满足只有一个抽象方法的条件,当然认为此时的接口还是一个函数式接口啦。这是表现上的理论,那为啥要有这样的一个规定呢?其实也比较好理解:如果一个类实现该接口,那很明显该类一定有这两个方法的实现,然而java.lang.Object是所有类的父类,也就是说明具体类都会直接或间接的继承Object类中的方法,而toString()并非是子类特有的方法,所以说如果一个方法中声明的刚好是Object类中的方法,那它不算抽象方法。
接下来继续用代码来进行延深:
由于MyInterface是函数式接口,所以可以改用Lambda表达式,如下:
其实上面标红的Lamdba表达式的写法就是MyInterface的匿名实现类,所以程序可以这样写:
那咱们可以打印一下这个类和它父类名字,如下:
那这个myInterface类的具体实现的接口是哪些呢?接着可以打印一下:
那这接口是谁呢?继续打印:
通过上面的例子对于函数式接口应该有一个比较好的认识了,所以对于它的探讨先暂时到这,接下来对于之前的例子进行一个进一步的探讨,回顾下当时的代码:
通过三种方式来对一个集合进行遍历,这里将重点观注在最后一种用函数式接口的方式,那这个forEach方法是来自于List类中么?点击查看下源码:
来自于Iterable接口当中,可以看到该方法是从Java1.8才开始引入的,但是Iterable是从1.5就开始引入的:
而我们知道List最终是实现了Iterable这个接口,所以当然也就继承有forEach这个方法啦,这就解释了为啥可以通过List去直接调用forEach来达到遍历的目的。
这里需要注意一下细节,这个forEach方法的具体实现实际上就是写在Iterable接口当中的,但是在接口的声明前面有个default关键字,这个也在之前说了,在Java8以后在接口中可以有具体实现了,但凡在接口中有具体实现方法,前面必须加default的关键字,这称之为默认方法(Default Method),而对于实现这个接口的类也自然而然继承有这个默认方法了,有点像抽象类的概念:类中既可有抽象方法,也可以有具体方法,而继承类也会继承抽象类的具体方法。
接着来查看一下forEach javadoc的注释:
接下来再来看一下这个指定的动作Consumer,从字面意思来理解当然就是消费者的意思啦,点击看一下它的源码:
读一下接口的doc:
再回到咱们的程序来说,很显然可以换成Lambda表达式来改造,所有函数式接口都可以采用Lambda表达式来编写,如下:
下面再对Lambda表达式进行一个总结。
Lambda表达式作用:
- Lambda表达式为Java添加了缺失的函数式编程特性,使得我们能将函数当做一等公民对待。
因为Java在以前方法永远都是依附于类而存在的,不可以独立存在的, 现在我们可以将方法当作参数进行传递了, 所以函数在Java8里面就成了一等公民。
- 在将函数作为一等公民的语言中,Lambda表达式的类型是函数,但在Java中,Lambda表达式是对象,他们必须依咐于一类特别的对象类型---函数式接口(Functional Interface)
标红的说Lambda表达式是对象,为什么呢?
迭代方式:
外部迭代:
什么是外部迭代呢?看程序:
下面用图来更形象的理解:
然后一个个元素进行迭代,最后指向一个空的元素既迭代完成了,如下:
内部迭代:
何为内部迭代,直接看代码:
之所以叫内部迭代,相对于外部迭代,当然是没有了外部迭代的迭代器啦,不借助于外部力量既完成元素的迭代。
方法引用:
对于上面元素迭待的方式已经改用Lambda表达式去写,代码已经很精简了,但是!!还可以更加精简,如下:
对于上面这种写法就叫做方法引用(method references),而这个forEach方法参数是函数式接口的实例,那意思是这个方法引用能创建函数式接口的实例?是的,在查看函数式注解的javadoc上就已经清楚的说明了,这里再来回顾一下:
这里看一个IDE比较智能的地方,就是在方法引用语句中的"::"处点击ctrl键之后会看到:
自动就跳到了Consumer这个函数式接口了,说明编译器识别到了这种定法就是对Consumer接口的实现,关于方法引用在之后还会仔细学习,这里有个感性的认识就行。
Lambda表达式深入:
在上面介绍Lambda表达式的作用时,其中说到这点:
如标红处所说,既然Lambda表达式是一个对象,而且必须依附于一类特别的对象类型叫函数式接口,那么如果咱们给出了一个Lambda表达式,那这个表达式对应一个什么样的函数式接口呢?答案是:这个类型的判定必须依附于上下文,如果没有给出上下文,那么仅凭这个Lambda表达式是无法得知其具体是什么类型的,下面用代码来阐述一下:
接下来声明这两个函数式接口对应的Lambda表达式,因为Lambda表达式是可以生成函数式接口的实例方式之一,这里再将函数式接口实例的三种方法再贴出来,加深印象,因为确实非常重要:
接着继续编写代码:
发现这两处的Lambda表达式是一模一样的,如果单独写这样的一个Lambda表达式会怎么样呢?
其中上面说的上下文既为:
而如果木有上下文的Lambda表达式它到底是啥类型的,编译器是无法断定出来的,所以就会报错了。上下文这也是Java编译器对于Lambda表达式类型推断的一个非常重要的依据,实际上它就是去找目标函数式接口特定唯一的抽象方法,然后再找到抽象方法的参数、抽象方法的返回类型,而关于抽象方法到底是什么名字,对于Lambda表达式是毫无意义的:
当然啦,这个名称对于方法本身是意义的。
接下来用Lambda表达式来写一下线程的代码,因为在Java8中的Runnable接口已经声明为函数式接口了:
运行:
流初步:
先说一个小需求:将集合中的String变成大写,然后再输出出来,这里不用传统的方式去实现,而是采用Lambda表达式,这里用上次学过的forEach方法来进行元素遍历,具体如下:
编译运行:
这时需求发生了变化:不只是将元素以大写的形式打印出来,这里需要构造一个新的集合,然后里面存放的是转换成大写之后的元素,那首先new出来一个新的集合,这里先插播一个小插曲:
看一下IDE对这个变灰的提示:
所以按照提示来修正下代码:
回到正题,接下来则是遍历之后将转换后的大写字符串一个个添加到新集合中,如下:
乍一看这种实现貌似跟咱们传统的处理方式木有精简多少呀,不差不多嘛,下面再看一种新的方式,也就是采用Java8的流的方式,需要提醒的是这里只是对流进行一个初步引入,这是Java8中的一大专题涉及到的东东还不少,所以之后还会不断深入系统的学习它的,对于下面的代码有个初步认识既可,领略一下采用流的方式给咱们带来的便利性,那用流的方式倒底是怎么弄呢?
集合中有两个跟stream相关的方法,那这两者有啥区别呢?简单说:stream()方法是串行的,而parallelStream()是并行的,当然并行的效率要比串行的要高,这里先来看一下stream()方法是在哪里定义的:
再来看下该stream是一个默认方法,当然符合在接口中如果是具体方法一定得是默认方法的规定,下面读一下该方法的定义的javadoc,如下:
可以它确实是串行的方式,那继续看下下面的说明,不是很重要,做了解:
然后粗略的看一下具体实现:
上面仅做了解~~下面来看下如何利用stream来达到我们的要求:
来看一下Function接口的定义:
关于该函数式接口在之后会进行详细学习,这里先有个认识既可,所以代码可以这样来写:
这时已经将集合中的元素都转换成大写了,接着再对集合中的元素进行打印输出:
而具体Consumer的操作则是打印输出,如下:
接下来进一步改造一下,因为函数式接口还可以由方法引用来创建,so,
而点击"::"就能智能的跳到Function接口:
另外再看一下细节,对于map方法是需要接收一个Function函数式接口的实例的,而它里面apply接口的要求是要有一个输入参数和一个返回值的,那对于String的toUpperCase方法我们可以瞅一眼:
当然符合,不符合那编译器肯定直接报错了,其实是这样理解的,输入参数是调用了toUpperCase这个方法的那个对象,这个需要注意一下,输出返回值就是转换成大写的那个字串。
上面涌现出了很多新的知识点,不要着急~之后会一点点进行深入学习的~~
转载地址列表:
java8学习之Lambda表达式初步与函数式接口
Java8学习之深入函数式接口与引用
java8学习之Lambda表达式深入与流初步