java函数式编程入口_Java中的函数式编程

前言

JDK8引入的Lambda表达式和Stream为Java平台提供了函数式编程的支持,极大地提高了开发效率.本文结合网络资源和自身使用经验,介绍下Java中的函数式编程

Java中的函数式编程

出现的原因

语言面临着要么改变,要么衰亡的压力. Java是传统的命令式编程,而函数式编程.是一种更"高级"的编程范式,Java为了支持它,推出了Lambda表达式和Stream.

函数式编程 VS 命令式编程

一言以蔽之:

函数式编程是:

"我现在想要这样东西(怎么办到我不管,你来处理)"

命令式编程是:

"你要先...,再...,最后...,就能拿到这样东西了"

事实上,函数式编程的底层实现还是命令式编程,就像面向对象语言核心部分(如JVM)是由面向过程语言(如C)实现的.毕竟脏活累活总是要有人去做的.

举个栗子

以一个比较苹果重量的Comparator为例,类Apple定义如下

public class Apple{

private int weight;

private int type;

public int getWeight(){

return this.weight;

}

public int getType(){

return this.type;

}

}

如果按照匿名类实现,代码会是这样,总体来说比较繁琐.

Comparator byWeight=new Comparator<>(){

@Override

public int compareTo(Apple a1,Apple a2) {

return a1.getWeight().compareTo(a2.getWeight());

}

}

如果使用Lambda表达式,最繁琐的形式会是这样

Comparator byWeight=

(Apple a1,Apple a2) -> {return a1.getWeight().compareTo(a2.getWeight());}

要搞清楚Lambda表达式的工作原理,首先要了解它的语法以及函数式接口

Lambda表示式 VS 方法

Lambda的语法结构如下

// 参数列表 箭头 方法体

( ParameterType1 param1,ParameterType2 param2... ) -> { ... }

方法的语法结构如下(暂不考虑throws)

访问权限 ReturnType methodName(ParameterType1 param1,ParameterType2 param2...){

...

}

可以看出,Lambda表达式可以看做方法的简化形式: 没有访问权限,返回类型以及方法名.并且它还可以进一步简化.

函数式接口

有且只有一个抽象方法的接口

首先澄清一下这里抽象方法的定义(java doc)

接口中的default方法不是抽象方法,因为它有默认实现

如果接口中的方法覆盖了java.lang.Object中的方法,也不做计数

下面以Comparator为例(适当精简)

@FunctionalInterface

public interface Comparator {

int compare(T o1, T o2);//1

boolean equals(Object obj);//2

default Comparator reversed() {//3

return Collections.reverseOrder(this);

}

}

@FunctionalInterface用来标识一个接口是函数式接口,它和@Override注解类似,只是编译时起检查作用,如果这个接口定义不符合的话,编译时就会报错,如果一个接口符合函数式接口的定义,即使没有这个注解依然是有效的

再来看下Comparator中有几个抽象方法

是抽象方法

覆盖了Object.equal()方法,所以不是

是default方法,也不是抽象方法

只有一个抽象方法,因此Comparator接口是一个函数式接口.

说了这么多,函数式接口到底有什么作用呢?

Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例

当我们把一个Lambda表达式赋给一个函数式接口时,这个表达式对应的必定是接口中唯一的抽象方法,因此就不需要以匿名类那么繁琐的形式去实现这个接口.可以说在语法简化上,Lambda表达式完成了方法层面的简化,函数式接口完成了类层面的简化.

Lambda表达式的进一步简化

在Lambda中,除了参数列表的大括号()和箭头→不能省略,其他部分如果编译器可以自动推断,都能省略.

简化规则1: 如果编译器可以推断出参数类型,参数列表中就可以省略参数类型

Comparator byWeight=

(a1,a2) -> {return a1.getWeight().compareTo(a2.getWeight());}

简化规则2: 如果方法体只有一条语句,花括号{}和return(如果有的话)都可以省略

Comparator byWeight=

(a1,a2) -> a1.getWeight().compareTo(a2.getWeight())

简化规则3: 可以通过方法引用来调用方法

首先要介绍一个新概念:方法引用,它的基本思想是:如果一个Lambda代表的只是直接调用这个方法,那最好还是用名称来调用它,而不是去描述如何调用它. 这样可读性更好.

方法引用的一般形式如下

//可以表示对静态/实例方法的调用

类名::方法名

//只能表示实例方法

this::方法名

针对上面的例子,首先利用JDK提供的工具做一些简化

Comparator byWeight=

Comparator.comparingInt((a)->a.getWeight())

然后利用方法引用可以简化为如下形式,是不是简单明了?

Comparator byWeight= Comparator.comparingInt(Apple::getWeight)

然而Lambda并不是万金油,它也有自己的限制.

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.

Lambda表达式的匹配

鸭子类型: “当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”

还是以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{

int someMethod(T a1,T a2);

}

那么上面这个Lambda同样适用于这个方法,因为它也符合鸭子的特征

SomeClass someMethod=

(Apple a1,Apple a2) -> {return ...;}

Lambda表达式的匹配规则相当的宽松简单,这也让它的使用更加方便.那么如何有效的利用它呢?

Stream

Java中的Stream是对函数式编程中pipeline的实现,日常业务开发中用的特别特别多,很值得学习.

又一个例子

需求: 有一堆苹果List apples,以重量从小到大,获取他们的品种.

以命令式编程来做会是:

Comparator byWeight=new Comparator<>(){

@Override

public int compareTo(Apple a1,Apple a2) {

return a1.getWeight().compareTo(a2.getWeight());

}

}

apples.sort(byWeight);

List types=new ArrayList();

for(Apple apple:apples){

types.add(apple.getType());

}

有了Stream,会是这样,语义清晰了很多,个人非常喜欢这种链式调用(链式调用一时爽,一直链式一直爽)再次展示出命令式编程和函数式编程的不同

List 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()的,这就是参数化

parallelStream—并行化任务的最简单方式

假设现在有一个包含100w元素的List,要对它进行一系列操作,元素很多,会消耗很多时间.

elements.stream().filter(...).map(...).collect(...);

很明显,多线程执行能够加速执行,只需要一点点修改就能使它以多线程模式执行,wonderful!

elements.parallelStream().filter(...).map(...).collect(...);

parallelStream的底层是fork/join框架.可以把它理解一个智能的线程池,它能将任务拆分并分发给不同的线程执行,最终汇总.然而 parallelStream并不是银弹,以下几点需要注意

parallelStream()的后续操作中进行排序(调用sorted())得出的结果是无效的.原因很简单,它是多线程执行的.这个问题的解决办法就是在parallelStream结束后再进行排序.

执行的任务不能依赖于线程私有数据(比如ThreadLocal),由于是多线程执行,其他线程并没有当前线程栈上的数据,一个最常见的例子就是在spring中执行数据库操作,session是绑定在线程上的,这时候以parallelStream执行就会报错 can't obtain session

任务数量必须足够多/单个任务耗时很长(io/网络操作)才有必要使用parallelStream,不然运行反而会更慢. 因为要fork/join也是要付出很大代价的: 划分子任务,分配任务给线程.具体的计算规则可以参考这篇文章

更好的使用Lambda

预定义的函数式接口

java.util.function下有很多JDK预定义的函数式接口.以常用的为例

使用JDK中新增的Lambda相关方法

JDK8的底层机制也添加了很多和函数式编程相关的改进,作为开发者,如何更好的享受这免费的午餐呢?

又又又是一个例子: 对Map 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 {

/**

* 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相关方法,可以大概遵循下面这个步骤

查找符合需求的api,如replaceAll

查看该方法要求的行为(参数)的定义,如BiFucntion.apply()

编写符合方法签名的Lambda表达式

用Lambda改造设计模式

又是一个例子,使用Lambda来实现模板方法模式,需求如下

根据不同行为对Apple进行不同的处理

public void templateMethod(Supplier supplier,Consumer consumer){

...

Apple apple=supplier.get();

...

consumer.accept(apple);

...

}

//eg.

Supplier normalSupplier=()->new Apple(10,1);

Consumer weightConsumer=(a)->System.out.println(a.getWeight());

templateMethod(normalSupplier,weightConsumer);

//eg.

Supplier bigSupplier=()->new Apple(100,2);

Consumer typeConsumer=(a)->System.out.println(a.getType());

templateMethod(bigSupplier,typeConsumer);

上面这个例子就是行为参数化的直观体现,相比传统的模板方法设计模式,免去了抽象出类的麻烦,更加易用.

总结

Lambda表达式和Stream使Java用起来不再那么繁琐,即使不探究其底层原理,也能用的很舒服.但是只有真正的理解函数式编程的思想,才能真正发挥Lambda表达式的威力.

以下是对提供的参考资料的总结,按照要求结构化多个要点分条输出: 4G/5G无线网络优化与网规案例分析: NSA站点下终端掉4G问题:部分用户反馈NSA终端频繁掉4G,主要因终端主动发起SCGfail导致。分析显示,在信号较好的环境下,终端可能因节能、过热保护等原因主动释放连接。解决方案建议终端侧进行分析处理,尝试关闭节电开关等。 RSSI算法识别天馈遮挡:通过计算RSSI平均值及差值识别天馈遮挡,差值大于3dB则认定有遮挡。不同设备分组规则不同,如64T和32T。此方法可有效帮助现场人员识别因环境变化引起的网络问题。 5G 160M组网小区CA不生效:某5G站点开启100M+60M CA功能后,测试发现UE无法正常使用CA功能。问题原因在于CA频点集标识配置错误,修正后测试正常。 5G网络优化与策略: CCE映射方式优化:针对诺基亚站点覆盖农村区域,通过优化CCE资源映射方式(交织、非交织),提升RRC连接建立成功率和无线接通率。非交织方式相比交织方式有显著提升。 5G AAU两扇区组网:与三扇区组网相比,AAU两扇区组网在RSRP、SINR、下载速率和上传速率上表现不同,需根据具体场景选择适合的组网方式。 5G语音解决方案:包括沿用4G语音解决方案、EPS Fallback方案和VoNR方案。不同方案适用于不同的5G组网策略,如NSA和SA,并影响语音连续性和网络覆盖。 4G网络优化与资源利用: 4G室分设备利旧:面对4G网络投资压减与资源需求矛盾,提出利旧多维度调优策略,包括资源整合、统筹调配既有资源,以满足新增需求和提质增效。 宏站RRU设备1托N射灯:针对5G深度覆盖需求,研究使用宏站AAU结合1托N射灯方案,快速便捷地开通5G站点,提升深度覆盖能力。 基站与流程管理: 爱立信LTE基站邻区添加流程:未提供具体内容,但通常涉及邻区规划、参数配置、测试验证等步骤,以确保基站间顺畅切换和覆盖连续性。 网络规划与策略: 新高铁跨海大桥覆盖方案试点:虽未提供详细内容,但可推测涉及高铁跨海大桥区域的4G/5G网络覆盖规划,需考虑信号穿透、移动性管理、网络容量等因素。 总结: 提供的参考资料涵盖了4G/5G无线网络优化、网规案例分析、网络优化策略、资源利用、基站管理等多个方面。 通过具体案例分析,展示了无线网络优化的常见问题及解决方案,如NSA终端掉4G、RSSI识别天馈遮挡、CA不生效等。 强调了5G网络优化与策略的重要性,包括CCE映射方式优化、5G语音解决方案、AAU扇区组网选择等。 提出了4G网络优化与资源利用的策略,如室分设备利旧、宏站RRU设备1托N射灯等。 基站与流程管理方面,提到了爱立信LTE基站邻区添加流程,但未给出具体细节。 新高铁跨海大桥覆盖方案试点展示了特殊场景下的网络规划需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值