JDK8引入的Lambda表达式和Stream为Java平台提供了函数式编程的支持,极大地提升了开发效率.本文结合网络资源和自身使用经验,介绍下Java中的函数式编程html
语言面临着要么改变,要么衰亡的压力. Java是传统的命令式编程,而函数式编程.是一种更"高级"的编程范式,Java为了支持它,推出了Lambda表达式和Stream.java
一言以蔽之:spring
函数式编程是:shell
"我如今想要这样东西(怎么办到我无论,你来处理)"数据库
命令式编程是:编程
"你要先...,再...,最后...,就能拿到这样东西了"设计模式
事实上,函数式编程的底层实现仍是命令式编程,就像面向对象语言核心部分(如JVM)是由面向过程语言(如C)实现的.毕竟脏活累活老是要有人去作的.api
以一个比较苹果重量的Comparator为例,类Apple定义以下网络
public class Apple{
private int weight;
private int type;
public int getWeight(){
return this.weight;
}
public int getType(){
return this.type;
}
}
复制代码
若是按照匿名类实现,代码会是这样,整体来讲比较繁琐.session
Comparator<Apple> byWeight=new Comparator<>(){
@Override
public int compareTo(Apple a1,Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
}
复制代码
若是使用Lambda表达式,最繁琐的形式会是这样
Comparator<Apple> byWeight=
(Apple a1,Apple a2) -> {return a1.getWeight().compareTo(a2.getWeight());}
复制代码
要搞清楚Lambda表达式的工做原理,首先要了解它的语法以及函数式接口
Lambda的语法结构以下
// 参数列表 箭头 方法体
( ParameterType1 param1,ParameterType2 param2... ) -> { ... }
复制代码
方法的语法结构以下(暂不考虑throws)
访问权限 ReturnType methodName(ParameterType1 param1,ParameterType2 param2...){
...
}
复制代码
能够看出,Lambda表达式能够看作方法的简化形式: 没有访问权限,返回类型以及方法名.而且它还能够进一步简化.
有且只有一个抽象方法的接口
首先澄清一下这里抽象方法的定义(java doc)
下面以Comparator为例(适当精简)
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);//1
boolean equals(Object obj);//2
default Comparator<T> reversed() {//3
return Collections.reverseOrder(this);
}
}
复制代码
@FunctionalInterface用来标识一个接口是函数式接口,它和@Override注解相似,只是编译时起检查做用,若是这个接口定义不符合的话,编译时就会报错,若是一个接口符合函数式接口的定义,即便没有这个注解依然是有效的
再来看下Comparator中有几个抽象方法
只有一个抽象方法,所以Comparator接口是一个函数式接口.
说了这么多,函数式接口到底有什么做用呢?
Lambda表达式容许你直接之内联的形式为函数式接口的抽象方法提供实现,并把整个表达式做为函数式接口的实例
当咱们把一个Lambda表达式赋给一个函数式接口时,这个表达式对应的一定是接口中惟一的抽象方法,所以就不须要以匿名类那么繁琐的形式去实现这个接口.能够说在语法简化上,Lambda表达式完成了方法层面的简化,函数式接口完成了类层面的简化.
在Lambda中,除了参数列表的大括号()
和箭头→
不能省略,其余部分若是编译器能够自动推断,都能省略.
简化规则1: 若是编译器能够推断出参数类型,参数列表中就能够省略参数类型
Comparator<Apple> byWeight=
(a1,a2) -> {return a1.getWeight().compareTo(a2.getWeight());}
复制代码
简化规则2: 若是方法体只有一条语句,花括号*{}和return*(若是有的话)均可以省略
Comparator<Apple> byWeight=
(a1,a2) -> a1.getWeight().compareTo(a2.getWeight())
复制代码
简化规则3: 能够经过
方法引用
来调用方法
首先要介绍一个新概念:方法引用,它的基本思想是:若是一个Lambda表明的只是直接调用这个方法,那最好仍是用名称来调用它,而不是去描述如何调用它. 这样可读性更好.
方法引用的通常形式以下
//能够表示对静态/实例方法的调用
类名::方法名
//只能表示实例方法
this::方法名
复制代码
针对上面的例子,首先利用JDK提供的工具作一些简化
Comparator<Apple> byWeight=
Comparator.comparingInt((a)->a.getWeight())
复制代码
而后利用方法引用能够简化为以下形式,是否是简单明了?
Comparator<Apple> byWeight= Comparator.comparingInt(Apple::getWeight)
复制代码
然而Lambda并非万金油,它也有本身的限制.
Lambda引用局部变量时,要求局部变量时final或effective final(即仅被赋值一次,以后不被修改).实例变量则能够随意使用.这个限制有以下几个缘由
堆和栈的差别
局部变量是存储在栈上的,即局部变量是线程私有的,而Lambda表达式不是线程私有的,它可能在其余线程上执行,而其余线程上是没有对应的局部变量的(实例变量是在堆上分配的,任何线程都能访问到),为了解决这个问题,Java会将局部变量的拷贝一份保存到在Lambda表达式中.所以Java在访问局部变量时,实际是在访问它的副本,而不是访问原始变量. 若是局部变量不是effective final的(好比在Lambda表达式以后对原始变量进行了修改),拷贝就可能和原始变量不一致,会引起不少语义上的问题(匿名内部类中局部变量也是相同缘由)
避免函数式编程的不正确使用
局部变量必须是effective final刚好符合函数式编程的特征之一—immutable data 数据不可变.数据不可变便没有了数据竞争问题,这样最有利于并行
假设非effective final局部变量是被容许的,那么下面这句代码其实是串行执行的,由于每一个任务都在竞争sum这个变量
int sum=0;
//parallelStream()会以多线程形式执行任务
ints.parallelStream().forEach(i->sum+=i);
复制代码
以函数式编程的思想来写,应该是这样,没有数据竞争问题,可以充分利用并行.
int sum=ints.parallelStream().reduce(0, (e1, e2) -> e1 + e2);
复制代码
并发问题
引用JLS的说明 ,什么状况下会致使并发问题笔者还没搞清楚.
The restriction to effectively final variables prohibits access to dynamically-changing local variables, whose capture would likely introduce concurrency problems.
鸭子类型: “当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就能够被称为鸭子。”
仍是以Comparator为例,首先看下Comparator.compare方法签名
int compare(T o1, T o2);
复制代码
而上面咱们提供的Lambda表达式正好符合这个形式: 两个同类型参数,返回int值
(Apple a1,Apple a2) -> {return a1.getWeight().compareTo(a2.getWeight());}
复制代码
在这里 Lambda表达式就表明"鸭子"这个类型,而Comparator的行为彻底符合鸭子的特征("走起来像鸭子,游泳起来像鸭子,叫起来也想鸭子"),就能够认为它"是"一只"鸭子"
假设如今咱们有以下接口
public interface SomeClass<T>{
int someMethod(T a1,T a2);
}
复制代码
那么上面这个Lambda一样适用于这个方法,由于它也符合鸭子的特征
SomeClass<Apple> someMethod=
(Apple a1,Apple a2) -> {return ...;}
复制代码
Lambda表达式的匹配规则至关的宽松简单,这也让它的使用更加方便.那么如何有效的利用它呢?
Java中的Stream是对函数式编程中pipeline的实现,平常业务开发中用的特别特别多
,很值得学习.
需求: 有一堆苹果List apples,以重量从小到大,获取他们的品种.
以命令式编程来作会是:
Comparator<Apple> byWeight=new Comparator<>(){
@Override
public int compareTo(Apple a1,Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
}
apples.sort(byWeight);
List<Integer> types=new ArrayList();
for(Apple apple:apples){
types.add(apple.getType());
}
复制代码
有了Stream,会是这样,语义清晰了不少,我的很是喜欢这种链式调用(链式调用一时爽,一直链式一直爽)再次展现出命令式编程和函数式编程的不一样
List<Integer> types=apples.stream()
.sorted(Comparator.comparingInt(Apple::getWeight))
.map(Apple::getType)
.collect(Collectors.toList());
复制代码
在平常开发中,将一个列表进行排序过滤转化最后收集这个套路十分常见,这个过程当中变化的只是咱们传递过去的Lambda表达式
,这也被称为行为参数化
行为参数化
一个方法接受多个不一样的行为做为参数,并在内部使用它们,完成不一样行为.
何为行为
实际一点来讲,获取苹果的类型(这个方法)就是一个行为,在代码中就是就是 map(Apple::getType)中的Apple::getType,假设Apple增长了一个属性尺寸Size,获取苹果的尺寸这个新的行为就是Apple::getSize.
参数化
Apple::getType这个行为 是做为一个参数传递给map()的,这就是参数化
假设如今有一个包含100w元素的List,要对它进行一系列操做,元素不少,会消耗不少时间.
elements.stream().filter(...).map(...).collect(...);
复制代码
很明显,多线程执行可以加速执行,只须要一点点修改就能使它以多线程模式执行,wonderful!
elements.parallelStream().filter(...).map(...).collect(...);
复制代码
parallelStream的底层是fork/join框架.能够把它理解一个智能的线程池,它能将任务拆分并分发给不一样的线程执行,最终汇总.然而 parallelStream并非银弹,如下几点须要注意
java.util.function下有不少JDK预约义的函数式接口.以经常使用的为例
JDK8的底层机制也添加了不少和函数式编程相关的改进,做为开发者,如何更好的享受这免费的午饭呢?
又又又是一个例子: 对Map<String,Integer> map中的全部值进行+1操做,在JDK8中,最好的作法以下
map.replaceAll((key,oldVal)->oldVal+1);
复制代码
首先看replaceAll方法的签名,replaceAll接收一个BiFunction做为参数,很明显 这个BiFunction就是咱们要传递的行为
/**
* Replaces each entry's value with the result of invoking the given
* function on that entry until all entries have been processed or the
* function throws an exception. Exceptions thrown by the function are
* relayed to the caller.
*
**/
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function){
...
}
复制代码
再来看BiFunction的定义,它是一个函数式接口,apply方法接收两个参数,有返回值
@FunctionalInterface
public interface BiFunction<T, U, R> {
/**
* Applies this function to the given arguments.
*
* @param t the first function argument
* @param u the second function argument
* @return the function result
*/
R apply(T t, U u);
}
复制代码
再来看咱们提供的Lambda表达式,符合Map.replaceAll中对BiFunction.apply的方法签名要求.完美!!!
(key,oldVal)->oldVal+1
复制代码
使用JDK8新增的Lambda相关方法,能够大概遵循下面这个步骤
又是一个例子,使用Lambda来实现模板方法模式,需求以下
根据不一样行为对Apple进行不一样的处理
public void templateMethod(Supplier<Apple> supplier,Consumer<Apple> consumer){
...
Apple apple=supplier.get();
...
consumer.accept(apple);
...
}
//eg.
Supplier normalSupplier=()->new Apple(10,1);
Consumer<Apple> weightConsumer=(a)->System.out.println(a.getWeight());
templateMethod(normalSupplier,weightConsumer);
//eg.
Supplier bigSupplier=()->new Apple(100,2);
Consumer<Apple> typeConsumer=(a)->System.out.println(a.getType());
templateMethod(bigSupplier,typeConsumer);
复制代码
上面这个例子就是行为参数化
的直观体现,相比传统的模板方法设计模式,免去了抽象出类的麻烦,更加易用.
Lambda表达式和Stream使Java用起来再也不那么繁琐,即便不探究其底层原理,也能用的很舒服.可是只有真正的理解函数式编程的思想,才能真正发挥Lambda表达式的威力.