java8函数式编程-上

2021/11/05

前言

jdk8是14年发布的正式版,因为是LTS目前也一直在维护更新,所以大部分公司锁java版本也是在锁8。但是最近长期支持版jdk17也出来了嘛,后面可能会有激进的团队跟进,我有朋友公司也把升级jdk17提上评估纲程了,慕了慕了。回到jdk8,我觉得最大的亮点就是让java一只脚踏进了多范式编程语言中。

当然你肯定会说这才哪跟哪呀,都是语法糖而已。但是配上一些优秀的库,这就是我想要要介绍的java函数式库,我上次无意在Github上看到后,去看文档学习然后引入到自己项目里,真的可以让你甜到闭口不言,写起来和scala一样。函数库的名字是vavr,github的地址

所以我就想介绍一下vavr,一是自己也可以总结一下,二是顺便布道一下,给也喜欢甜甜的你。本篇是上篇,为了使得更好地介绍vavr,这篇主要介绍一下java8的函数式编程,然后vavr的详细介绍会在下篇。

函数式编程

函数式首先要有函数,在java8以前,其实java只有方法概念,压根就没有函数这个概念。其次函数要作为“第一等公民”,所谓第一等公民就是函数和其他的基本类型/引用类型一样,处于同一地位,可以赋值,可以传参。

java其实是通过一个接口和lambda特殊语法来实现的,下面是lambda的语法

// 前面是入参,然后中间使用一个 “->” 的符号连接, 后面是表达式
Type var -> 表达式;
/*
两个入参前面需要括号括起来(一个也可以括号,但是通常省略)
如果表达式很复杂,一句写不完,则需要使用{}括起来,如果有返回值还需要并且手动return
*/
(Type1 var1, Type2 var2) -> {
  表达式1;
  表达式2;
}
// 如果函数没有入参,则必须要括号,但是括号里面什么都不xei
() -> 表达式;

// 另外类型java会自动推断,其实入参的类型可以不写,只需要取个名字就好了
var -> 表达式;
(var1, var2) -> 表达式

下面写一些实际例子

// 只有一个参数的语法(括号省略,类型也省略)
str -> System.out.print(str);
str -> System.out.print(str);

// 两个参数的例子(两个参数必须要括号)
(str1, str2) -> System.out.print(str + str2);

// 没有入参,复杂表达式,还带返回值
() -> {
      String someget = new String("someget");
      String result = someget.concat(".cn");
      return result;
    };
  

你把lambda写的函数可以赋值给一个接口,然后通过这个接口进行入参和出参传输。java内部已经很庞大了,现在一年是发两个大版本,但是再怎么改对于语法都不可能是巨变。有人说lambda是匿名内部类的语法糖,有那么点味道,但是说语法糖不准确,使用lambda编译之后,没有单独的.calss字节码文件,对应的字节码文件会在运行的时候生成。

p.s. 有一点要注意,Java8中的lambda表达式,并不是完全闭包,因为其只对值封闭,不对变量封闭。也就是lambda中使用的变量,必须是声明final类型或者是隐式的final(就是虽然更不是final但是变量在代码中没有被修改),。这个一点也会在下一篇的不变性数据结构中体现,因为这个限制会让你的代码变得无比安全。

函数式接口

上面介绍了java实现的函数式编程。java是通过接口来定义函数的,定义了一个lambda以后可以赋值给一个接口,那么你的lambda其实是实现了接口里面的一个方法。

这个接口你可以自己定义,然后定义一个未实现(抽象)的方法就好了。注意这个是硬性条件,接口里面必须要有一个未实现(抽象)的方法,java8在接口中引入了default方法,有一个default方法不能变成函数式接口。为了在编译前让你检查出是否符合规范,java8新加一个@FunctionalInterface注解。加上这个注解编译器就知道你是想要写函数式接口,因此如果你接口中没有未实现的方法则会通过不了编译。函数式接口也称为SAM接口,翻译是Single Abstract Method interfaces。

那么既然自己写一个未实现方法的接口就可以接受lambda,反正大家方法都未实现,你的接口和我的接口不都是一样的?对但是又不对,方法确实大家都没有实现,可是入参和出参每个方法确实都不一样。所以其实函数式接口的区别就是通过这个未实现方法的入参和出参来定义的。

入参和出参相同的函数式接口理论来说是一样的,java8有内置很多函数式接口,下面就来介绍最常见的四种,它们的应用领域你肯定也经常使用。

Function<T, R>

官方注释翻译是接受一个参数并且产生一个结果。这个函数式接口的名字稍稍有点误导,因为他的名字就叫函数。其实它的作用是接受一个对象(泛型T), 然后把它转换成另外一个对象(泛型R).Function<T, R>的菱形括号前一个指的是入参的泛型, 后一个指的是出参的泛型。

image-20211115221406511

源码

可以看到, 它里面未实现的方法就是apply。如果你用过javaStream或者scala里面的map算子那么你肯定对它很熟悉,它就是把流里面的元素挨个映射成你想要的其他类型

image-20211115221604580

Consumer

官方翻译是接收单个参数不产生任何结果。也就是说它只接受一个泛型,然后不返回任何东西(void),你可以通过副作用伴随着一些期望状态的改变或者事件的发生。

image-20211115222127716

consumer的源码

consumer未实现的方法是accept,它需要传入一个对象,然后返回一个void。最常用的foreach就是使用这个接口。

image-20211115222339830

Predicate

。这个函数式接口是需要传入一个类型,但是它会返回一个布尔类型的值(true或者false)。顺便吐槽一下部分书籍把predicate翻译成谓词,真的是反人类

image-20211115222654674

源码截取

它的未实现方法是test,聪明的你肯定想到了java的Stream里面filter里面传入的就是这个接口

image-20211115223230651

Supplier

最后一个比较常用的和consumer有点类似,它反过来,它没有入参,但是有一个返回值

image-20211115223401921

源码截取

它的未实现方法是get,如果你写过() -> xxxx你应该能马上反应过来。比如CompletableFuture里面的supplyAsync传入的就是一个supllier

image-20211115223511662

最后来总结一下这四种最常见的函数式接口,上面分来开写有点乱,这里汇总一起背一下

接口名 中文 作用 未实现方法
consumer 消费者 T是入参,接受一个类型然后产生副作用 void accept(T t)
function <T, R> 函数 T是入参,R是出参,接受一个T, 然后返回一个R对象 R apply(T t)
predicate 断言 T是入参, 接受一个T, 然后返回一个布尔类型的值 boolean test(T t)
supplier 提供者 T是出参, 没有入参(写成() -> xxx), 返回一个T对象 T get()

image-20211115233015255

引用一张圣书java8实战的截图

后言

这里没讲半毛钱的vavr,主要总结一下java的函数编程接口,当然我只列举四种,其实还有很多比如前面加一个Binary就成了另外的函数式接口(大家翻下源码看看)。如果种类不够完全可以自定义,这些都是只是java自带的,比如Function的出参变成boolean就是Predicate.我们完全可以创建一个Function的出参变成Integer然后叫IntFunction之类的(其实jdk也给你提供了😂).

函数式是基础,那么在下一篇我会介绍最近让我为之着迷的vavr函数库。可以让java写起来像scala一样丝滑。

引用

  1. Side Effects in Functional Programming

  2. java8实战

(转载本站文章请注明作者和出处 没有气的汽水



┌┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┐
├ 文章已经完啦, 想要第一时间收到文章更新可以关注↓ ┤
└┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┘

Post Directory






下面是评论区,欢迎大家留言探讨或者指出错误哈