函数式应用:前奏,聊聊Java8

Posted 品质出行技术

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了函数式应用:前奏,聊聊Java8相关的知识,希望对你有一定的参考价值。

    肯定有人会问,马上都Java11了,还在聊8,是不是太low了,要知道业内经常流传“线上用的JDK7甚至JDK6,JDK8还没用熟,JDK9都不知道做了啥改进,JDK10也才发布不久,JDK11又要来了”。其实Oracle用了近3年时间才发布了Java8,从7到8,也是在Java史上最有代表意义的一次改进,其中的很多特性都让Java在和其他语言争夺“最好的语言”问题上增加了谈话的资本,下面我们一一道来。

1. Lambda Expressions

    首先我们说说Lambda表达式,这里首先提及一个概念叫函数接口,其实就是只包含一个抽象方法的接口,例如我们的Runnable接口,只有run抽象方法。开发者可以用Lambda来传递行为,大家都知道在Java开发中,类和对象是我们首先要考虑的,我们没法直接传递行为,我们只有通过对象包含一个行为进行传递,用Lambda就可以让开发人员更专注于业务的开发上,不用每次都写接口和类,而且他还有很多特异功能,例如类型推断、闭包等等,可以让你的代码更优雅,但是根据Java语言的要求,类才是第一公民,这一点是融入骨子里的,解决不了,那怎么办?Oracle那帮人很聪明,定义了一些类型来持有Lambda表达式,Java里提供了Predicate/Consumer/Function/Supplier/BinaryOperator等类型,他们都在java.util.function下,这些类是不是看起来有些熟悉在Guava里也是存在的,Google那帮大牛觉得Java的发布太慢了,他们借助匿名类和泛型实现了一套近似Lambda的组件,但是使用过的同学肯定深有感触,你会发现用这种方式实现的代码冗长、混乱、可读性差而且非常低效,因为他仅仅是对Java的一层包装而已。
“talk is cheap,show me the code”,让我们来看一个例子:

在这个实现中,infoData和param一定要是声明成final的,不过这种要求也是应该的,对业务逻辑也没啥影响,但是我们要写大量的样板式代码,很繁琐,用Lambda写起来是这样的:

AsyncTaskManager.submit(() -> checkSuddenInfoWithZip(infoData, param));  

    在这里,你关注的将只是run里面的逻辑,而不用去关心这个对象是什么,lambda里不需要你一定使用final变量,只要变量在事实上是final的就可以使用!

    再举个例子,你有个计算器程序,可以支持加减乘除,你一定会这么做:

函数式应用:(一)前奏,聊聊Java8

为了实现这个功能,你要建一个Operate接口来定义操作行为,然后在执行的地方new个对象出来,这就是前面说的没法传递行为,你得用对象包裹着,换了Lambda就这样写了:

函数式应用:(一)前奏,聊聊Java8

    我们不用定义接口来持有行为,我们的JVM也不用加载Operate接口,这里只有一个操作类型,可能看上去一样,都要加载一个Class,但是如果还有类似的行为,你还要写更多的接口,加载更多的类,而Lambda里只需要加载BinaryOperator这一个类就可以了,后面我们说到Stream再看看Lambda的好处。

    对了,这里还有个利器叫方法引用,方法引用其实就是用来简写Lambda表达式中的方法,例如我们的Person类有个静态的compare方法:

public static int compare(Person a, Person b) {  

    return a.age.compareTo(b.age);  

执行排序的时候可以:

Arrays.sort(personList, Person::compare);  

同样,System.out.println可以用在Stream遍历的时候打印字符串:

list.forEach(System.out::println);  

2. 类型推断的增强

    泛型可以做到参数化类型,使代码的可扩展性大大增强,但是在Java8之前,方法内套用泛型方法,是无法识别的,你需要显示的赋值给一个具体类型变量,才能推断出来真实类型,而在Java8里不再需要这样做:

函数式应用:(一)前奏,聊聊Java8

3. Stream

    终于说到Stream了,该引入可以说是Java史上最厉害的特性,没有之一!她可以让开发者快速写出更加有效、简洁和紧凑的代码。

    流的主要思想是将外部迭代改为内部迭代,开发者只需关注迭代的动作而不用关心如何迭代。我们每次迭代Collection时,都要写样板式的for循环,甚至几层嵌套,让代码的可读性差到令人发指,稍有不慎就会出错。

    举个例子来冲击一下你的思维:我们的客户端长连接系统会打包司机心跳数据并通过MQ发送给各业务方,假设现在是100个心跳一个包,业务上现在需要判断,筛选出杭州司机的心跳点,我们会这么做:

函数式应用:(一)前奏,聊聊Java8

现在需求改了,我们还要加限制,是要杭州男司机的心跳:

函数式应用:(一)前奏,聊聊Java8

    突然,长连接系统说我们现在逻辑变了,每个包里有1000个心跳点了,此时,你怎么办?还这样顺序处理1000个么?假设长连接系统包里有10000了呢?将这份代码改成并发处理,相信很多人都能做到,但是很麻烦。

现在看看Stream会怎么做:

函数式应用:(一)前奏,聊聊Java8

   代码看起来是不是简洁很多?一眼看去就知道是过滤(filter)出来杭州的男性司机!有没有感受到Lambda+Stream带来的好处?把迭代的过程交给JVM吧,你要做的只是选择迭代规则。

    假设我们又来新业务了,需要收集心跳里的各城市心跳数量统计,形如List -> Map<city,total>这样的结构,老的方式应该要这样:函数式应用:(一)前奏,聊聊Java8    这里穿插一个新功能,Java8里的HashMap增加了例如getOrDefault/putIfAbsent/computeIfAbsent/computeIfPresent等方法来简化我们的代码,上面的get就可以用getOrDefault(hb.cityId, 0)来替代。

而Lambda里就没这么麻烦了:

Map<City, Integer> count = heartbeats.stream().collect(Collectors.groupingBy(hb -> hb.getCityId(), Collectors.counting()));  

   有没有觉得很棒?能用一行代码解决,就绝不用两行!我们从循环里跳出来了,只需要关注循环内的逻辑,关注怎么处理我的每个对象就可以了。

Filter、Transfer、Convert是函数式编程里比较重要的三个利器!映射到Java8里就是filter、map、collect。

    Stream里有个非常重要的概念——惰性求值,这个概念就类似于Builder模式,只有到最后一步遇到调用build方法时,才会去组装对象,Stream里也一样,只要是返回Stream对象的方法,都是惰性的,就不会真正执行运算,直到遇到返回非Stream对象的方法。这种方法有自己的名字,有的地方叫“及早求值”,有的叫“严格求值”,都是一个意思,这样做的好处是JVM会给你优化执行过程,不会做多余的遍历运算,例如以下两个语句的执行过程是一样的:

函数式应用:(一)前奏,聊聊Java8

   Stream提供了filter、sort、map、match、count、reduce等等很多功能,可以满足大家日常所有的开发需要,用Stream的好处显而易见,所以不要再自己写循环了,用优雅、可读性高的Lambda+Stream来替代吧!
    刚刚一直在说List接口的Stream方法,按照惯例,接口里增加了方法,实现类里应该都要增加实现的,如果我们在业务里实现了我们自己的一种AbcdList,如果直接升级Java8,岂不是编译就报错了?Oracle那帮大牛怎么解决你自己实现的List能兼容新加的Stream方法呢?Java从一开始就声称要“永远向下兼容”,就是这句话,把自己带坑里了,哈哈!开玩笑的。。。回归正题,怎么解决呢?

4. 默认方法

    默认方法可以做到在不破坏二进制兼容性的前提下,给接口增加方法,就是不强制那些实现了该接口的类也同时实现这个新加的方法。

   这个特性在多态里用起来非常爽,当需要增加某个特性的时候,再也不用给每个实现类增加样板式的方法了,特别是命令模式,增加了一个命令,你要给每个实现类增加抛NotSupport异常的样板代码,现在你只要在接口里做默认实现抛NotSupport异常就可以了,所有的实现类将自动继承这种实现!现在脑海中是不是回想起当初一个类一个类的加NotSupport的痛苦日子了。

    接口的默认方法还有什么好处呢?它间接的解决了多重继承问题,但是Java8里的这种多重继承更倾向于解决的是代码块的继承,而不是对象状态的继承,这一点也完美避开了多重继承导致的二义性等一系列诟病。

5. Optional

    Optional主要是用来替换null值,使用他的目的是提示开发者使用之前要检查是否为空,避免NPE的发生!其实这个概念早在Guava中就出现了,Google那帮大牛又一次教会我们:觉得不爽,咱们就自己干!

    其实现在应用更多的还是在用Guava的Optional,特别是在对外的API接口上,因为你没法要求你的调用方升级到1.8,但是你可以让他们去依赖Guava,当然,系统内部随便你用哪个,开心就好!

6. 结束

    Java8的主要功能就先介绍到这里,其他的,例如HashMap和ConcurrentHashMap的改进、新引入的Date-Time API、注解的扩展、Nashorn javascript引擎等特性也很不错,不过有的应用的场景比较特殊,就不一一介绍了。

    如果深究Java8算不算函数式,众说纷纭,纠结来纠结去也没啥意义,我个人觉得:如果我可以用函数式的思维来做逻辑实现,而且用起来爽、结构清晰,这就足够了,管他是什么语言,下一篇我们将一起探讨函数式编程,以及如何改变我们的编程思维,从命令式到函数式的蜕变!

    Java8跑起来吧,你的升级总不能比Java版本的升级节奏还慢吧~~~

后续推荐:

函数式应用:(二)如何转变为函数式思维

函数式应用:(三)浅析响应式编程


打赏,是种生活方式!!


以上是关于函数式应用:前奏,聊聊Java8的主要内容,如果未能解决你的问题,请参考以下文章

Java8 之 lambda 表达式方法引用函数式接口默认方式静态方法

简单聊聊:函数式编程

聊聊函数式编程

Java8中的函数式接口SupplierConsumerBiConsumer详解

Java8中的函数式接口SupplierConsumerBiConsumer详解

Java8中的函数式接口SupplierConsumerBiConsumer详解