Java 8新特性:lambda表达式

 

lambda 表达式是 java 8th 给咱们带来的几个重量级新特性之一,借用 lambda 表达式可让咱们的程序设计更加简洁。java

一. 行为参数化express

行为参数化简单的说就是将方法的逻辑以参数的形式传递到方法中,方法主体仅包含模板类通用代码,而一些会随着业务场景而变化的逻辑则以参数的形式传递到方法之中,采用行为参数化可让程序更加的通用,以应对频繁变动的需求。编程

这里咱们以 java 8 in action 中的例子进行说明。考虑一个业务场景,假设咱们须要经过程序对苹果按照必定的条件进行筛选,咱们先定义一个苹果实体:app

用户最开始的需求可能只是简单的但愿可以经过程序筛选出绿色的苹果,因而咱们能够很快的经过程序实现:函数式编程

若是过了一段时间用户提出了新的需求,但愿可以经过程序筛选出红色的苹果,因而咱们又须要针对性的添加了筛选红色苹果的功能:函数

更通用的实现是把颜色做为一个参数传递到方法中,这样就能够应对之后用户提出的各类颜色筛选需求:学习

这样的设计不再用担忧用户的颜色筛选需求变化了,可是不幸的是某一天用户提了一个需求但愿可以筛选重量达到某一标准的苹果,有了前面的教训咱们也把重量的标准做为参数传递给筛选函数:编码

这样经过传递参数的方式真的好吗?若是筛选条件愈来愈多,组合模式愈来愈复杂,咱们是否是须要考虑到全部的状况,并针对每一种状况都实现相应的策略呢?而且这些函数仅仅是筛选条件的部分不同,其他部分都是相同的模板代码(遍历集合),这个时候咱们就能够将行为进行 参数化处理,让函数仅保留模板代码,而把筛选条件抽离出来当作参数传递进来,在 java 8th 以前,咱们经过定义一个过滤器接口来实现:spa

经过上面行为抽象化以后,咱们能够在具体调用的地方设置筛选条件,并将条件做为参数传递到方法中:线程

上面的行为参数化方式采用匿名类实现,这样的设计在 jdk 内部也常常采用,好比java.util.Comparator,java.util.concurrent.Callable等,使用这类接口的时候,咱们均可以在具体调用的地方用匿名类指定函数的具体执行逻辑,不过从上面的代码块来看,虽然很极客,可是不够简洁,在 java 8th 中咱们能够经过 lambda 表达式进行简化:

如上述所示,经过 lambda 表达式极大精简了代码,同时行为参数让咱们的程序极大的加强了可扩展性。

二. Lambda 表达式

2.1 Lambda 表达式的定义与形式

咱们能够将 lambda 表达式定义为一种 简洁、可传递的匿名函数,首先咱们须要明确 lambda 表达式本质上是一个函数,虽然它不属于某个特定的类,但具有参数列表、函数主体、返回类型,甚至可以抛出异常;其次它是匿名的,lambda 表达式没有具体的函数名称;lambda 表达式能够像参数同样进行传递,从而简化代码的编写,其格式定义以下:

参数列表 -> 表达式参数列表 -> {表达式集合}

须要注意 lambda 表达式隐含了 return 关键字,因此在单个的表达式中,咱们无需显式的写 return 关键字,可是当表达式是一个语句集合的时候则须要显式添加 return 关键字,并用花括号{ } 将多个表达式包围起来,下面看几个例子:

 

 

2.2 基于函数式接口使用 lambda 表达式

lambda 表达式的使用须要借助于 函数式接口,也就是说只有函数式接口出现地方,咱们才能够将其用 lambda 表达式进行简化。那么什么是函数接口?函数接口的定义以下:

函数式接口定义为仅含有一个抽象方法的接口。

按照这个定义,咱们能够肯定一个接口若是声明了两个或两个以上的方法就不叫函数式接口,须要注意一点的是 java 8th 为接口的定义引入了默认的方法,咱们能够用

default

关键字在接口中定义具有方法体的方法,这个在后面的文章中专门讲解,若是一个接口存在多个默认方法,可是仍然仅含有一个抽象方法,那么这个接口也符合函数式接口的定义。

2.2.1 自定义函数式接口

咱们在前面例子中实现的苹果筛选接口就是一个函数式接口(定义以下),正由于如此咱们能够将筛选逻辑参数化,并应用 lambda 表达式:

AppleFilter 仅包含一个抽象方法accept(Apple apple),依照定义能够将其视为一个函数式接口。在定义时咱们为该接口添加了@FunctionalInterface注解,用于标记该接口是一个函数式接口,不过该注解是可选的,当添加了该注解以后,编译器会限制了该接口只容许有一个抽象方法,不然报错,因此推荐为函数式接口添加该注解。

2.2.2 jdk 自带的函数式接口

jdk 为 lambda 表达式已经内置了丰富的函数式接口,以下表所示(仅列出部分):

其中最典型的三个接口是Predicate<T>、Consumer<T>,以及Function<T, R>,其他接口几乎都是对这三个接口的定制化,下面就这三个接口举例说明其用处,针对接口中提供的逻辑操做默认方法,留到后面介绍接口的 default 方法时再进行说明。

Predicate<T>

Predicate 的功能相似于上面的 AppleFilter,利用咱们在外部设定的条件对于传入的参数进行校验并返回验证经过与否,下面利用 Predicate 对 List 集合的元素进行过滤:

上述方法的逻辑是遍历集合中的元素,经过 Predicate 对集合元素进行验证,并将验证不过的元素从集合中移除。咱们能够利用上面的函数式接口筛选整数集合中的偶数:

Consumer<T>

Consumer 提供了一个 accept 抽象函数,该函数接收参数并依据传递的行为应用传递的参数值,下面利用 Consumer 遍历字符串集合并转换成小写进行打印:

利用上面的函数式接口,遍历字符串集合并以小写形式打印输出:

Function<T, R>

Funcation 执行转换操做,输入类型 T 的数据,返回 R 类型的结果,下面利用 Function 对字符串集合转换成整型集合,并忽略掉不是数值型的字符:

下面利用上面的函数式接口,将一个封装字符串的集合转换成整型集合,忽略不是数值形式的字符串:

2.2.3 一些须要注意的事情

类型推断

在编码过程当中,有时候可能会疑惑咱们的调用代码会具体匹配哪一个函数式接口,实际上编译器会根据参数、返回类型、异常类型(若是存在)等因素作正确的断定。在具体调用时,一些时候能够省略参数的类型以进一步简化代码:

局部变量

上面全部例子中使用的变量都是 lambda 表达式的主体参数,咱们也能够在 lambda 中使用实例变量、静态变量,以及局部变量,以下代码为在 lambda 表达式中使用局部变量:

上述示例咱们在 lambda 中使用了局部变量 weight,不过在 lambda 中使用局部变量仍是有不少限制,学习初期 IDE 可能常常会提示咱们

Variable used in lambda expression should be final or effectively final

的错误,即要求在 lambda 表达式中使用的变量必须 显式声明为 final 或事实上的 final 类型

为何要限制咱们直接使用外部的局部变量呢?主要缘由在于内存模型,咱们都知道实例变量在堆上分配的,而局部变量在栈上进行分配,lambda 表达式运行在一个独立的线程中,了解 JVM 的同窗应该都知道栈内存是线程私有的,因此局部变量也属于线程私有,若是肆意的容许 lambda 表达式引用局部变量,可能会存在局部变量以及所属的线程被回收,而 lambda 表达式所在的线程却无从知晓,这个时候去访问就会出现错误,之因此容许引用事实上的 final(没有被声明为 final,可是实际中不存在更改变量值的逻辑),是由于对于该变量操做的是变量副本,由于变量值不会被更改,因此这份副本始终有效。这一限制可能会让刚刚开始接触函数式编程的同窗不太适应,须要慢慢的转变思惟方式。

实际上在 java 8th 以前,咱们在方法中使用内部类时就已经遇到了这样的限制,由于生命周期的限制 JVM 采用复制的策略将局部变量复制一份到内部类中,可是这样会带来多个线程中数据不一致的问题,因而衍生了禁止修改内部类引用的外部局部变量这一简单、粗暴的策略,只不过在 8th 以前必需要求这部分变量采用 final 修饰,可是 8th 开始放宽了这一限制,只要求所引用变量是 “事实上” 的 final 类型便可。

三. 方法引用

方法引用能够更近一步的简化代码,有时候这种简化让代码看上去更加直观,先看一个例子:

 

 

方法引用经过 ::将方法隶属和方法自身链接起来,主要分为三类:

静态方法

参数的实例方法

外部的实例方法

相关文章
相关标签/搜索