摘要:引入了与此前完全不同的函数式编程方法,通过表达式和来为下的函数式编程提供动力。命令式编程语言把对象变量和流转当作一等公民,而函数式编程在此基础上加入了策略变量这一新的一等公民。
到底什么是函数式编程呢?Java8引入了与此前完全不同的函数式编程方法,通过Lambda表达式和StreamAPI来为Java下的函数式编程提供动力。本文是Java8新特性的第一篇,旨在阐释函数式编程的本义,更在展示Java是如何通过新特性实现函数式编程的。
最近在读这本图灵的新书:Java 8 in Action ,本书作者的目的在于向Java程序员介绍Java8带来的新特性,鼓励使用新特性来完成更简洁有力的Java编程。本系列的文章的主要思路也来源于本书。
函数式编程并不是一个新概念,诸如Haskell这样的学院派编程语言就是以函数式编程为根基的,JVM平台上更彻底的采用函数式编程思维的更是以Scala为代表,因此函数式编程确实不是什么新概念。
下面来谈谈命令式编程和函数式编程。
什么是命令式编程呢?很容易理解,就是一条条的命令明明白白地告诉计算机,计算机依照这些这些明确的命令一步步地执行下去就好了,从汇编到C,这样的命令式编程语言无非都是在模仿计算机的机器指令的下达,明确地在每一句命令里面告诉计算机每一步需要怎么申请内存(对象变量)、怎么跳转到下一句命令(流转),即便后来的为面向对象编程思维而生的编程语言,比如Java,也仍然未走出这个范式,在每个类的对象执行具体的方法时也是按照这种“对象变量-流转”的模式在运行的。在这个模式下,我们会经常发现程序编写可能会经常限于冗长的“非关键”语句,大量的无用命令只是为了照顾语言本身的规则:比如所谓的面向接口编程最终变成了定义了一组一组的interface、interfaceImpl。
函数式编程则试图从编程范式的高度提高代码的抽象表达能力。命令式编程语言把“对象变量”和“流转”当作一等公民,而函数式编程在此基础上加入了“策略变量”这一新的一等公民。策略是什么呢?策略就是函数,函数本身是可以作为变量进行传递的。在以往的编程范式里,策略要被使用时通常是被调用,所以策略的使用必须通过承载策略的类或对象这样的对象变量,而函数式编程里面,我们可以直接使用策略对象来随意传递,省去了这些不必要的无用命令。
Java8作为一个新特性版本,在保留原有的Java纯面向对象特性之外,在容易理解的范围内引入了函数式编程方式。
我们有这样的一个引入的例子:我们有一堆颜色和重量不定的苹果,这些苹果需要经过我们的一道程序,这道程序可以把这堆苹果中的红苹果取出来。怎样编写程序来选出红苹果呢?
首先我们定义苹果Apple类:
public class Apple{ private String color; private Integer weight; public String getColor() { return color; } public void setColor(String color) { this.color = color; } public Integer getWeight() { return weight; } public void setWeight(Integer weight) { this.weight = weight; } public Apple(String color, Integer weight) { this.color = color; this.weight = weight; } }
添加我们的一堆颜色和重量随机的苹果:
public static void main(String[] args){ ArrayList纯命令式的思路:apples = new ArrayList<>(); Random weightRandom = new Random(); Random colorRandom = new Random(); String[] colors = {"red","green","yellow"}; for (int i = 0; i < 100; i++) { apples.add(new Apple(colors[colorRandom.nextInt(3)],weightRandom.nextInt(200))); } }
如果我们使用传统的命令式的编程方法,这个从苹果堆中筛选红苹果的方法会这样:
public static ListredAppleFilter(List apples){ List redApples = new ArrayList<>(); for (Apple apple: apples) { if("red".equals(apple.getColor())){ redApples.add(apple); } } return redApples; }
ListredApples = redAppleFilter(apples);
如果这个时候我们变更需求了,比如我们不筛选红苹果了,要绿苹果了,怎么办呢?就得再定义一个从苹果堆中筛选绿苹果的方法:
public static ListgreenAppleFilter(List apples){ List greenApples = new ArrayList<>(); for (Apple apple: apples) { if("green".equals(apple.getColor())){ greenApples.add(apple); } } return greenApples; }
List面向对象的编程方法:greenApples = greenAppleFilter(apples);
使用为抽象操作而生的接口:接口只定义抽象的方法,具体的方法实现可以有不同的类来实现。如果把这些操作放到继承了一般筛选器的不同筛选方法的筛选器中去就会有一个典型的面向对象式的解决方案了:
interface AppleFilter { public List面向对象的抽象层级问题filterByRules(List apples); } class RedAppleFilter implements AppleFilter{ @Override public List filterByRules(List apples) { List redApples = new ArrayList<>(); for (Apple apple: apples) { if("red".equals(apple.getColor())){ redApples.add(apple); } } return redApples; } } class GreenAppleFilter implements AppleFilter{ @Override public List filterByRules(List apples) { List greenApples = new ArrayList<>(); for (Apple apple: apples) { if("green".equals(apple.getColor())){ greenApples.add(apple); } } return greenApples; } }
我们发现虽然使用了面向对象的编程方法虽然可以使得逻辑结构更为清晰:子类苹果筛选器实现了一般苹果筛选器的抽象方法,但仍然会有大量的代码是出现多次的。
这就是典型的坏代码的味道,重复编写了两个基本一样的代码,所以我们要怎么修改才能使得代码应对变化的需求呢,比如可以应对筛选其他颜色的苹果,不要某些颜色的苹果,可以筛选某些重量范围的苹果等等,而不是每个确定的筛选都需要编写独立且基本逻辑相同的代码呢?
我们来看一下重复的代码究竟是哪些:
ListgreenApples = new ArrayList<>(); for (Apple apple: apples) { ... ... } return greenApples;
不重复的代码有哪些:
if("green".equals(apple.getColor())){ }
其实对于循环列表这部分是对筛选这一逻辑的公用代码,而真正不同的是筛选的具体逻辑:根据红色筛选、绿色筛选等等。
而造成现在局面的原因就在于仅仅对大的筛选方法的实现的抽象层级太低了,所以就会编写太多的代码,如果筛选的抽象层级定位到筛选策略这一级就会大大提升代码的抽象能力。
传递策略对象所谓策略的范围就是我们上面找到的这个“不重复的代码”:在这个问题里面就是什么样的苹果是可以经过筛选的。所以我们需要的这个策略就是用于确定什么样的苹果是可以被选出来的。我们定义一个这样的接口:给一个苹果用于判断,在test方法里对这个苹果进行检测,然后给出是否被选出的结果。
interface AppleTester{ public Boolean test(Apple apple); }
比如我们可以通过实现上述接口,重写这个test方法使之成为选择红苹果的方法,然后我们就可以得到一个红苹果选择器:
class RedAppleTester implements AppleTester{ @Override public Boolean test(Apple apple) { return "red".equals(apple.getColor()); } }
再比如我们可以通过实现上述接口,重写这个test方法使之成为选择大苹果的方法,然后我们就可以得到一个大苹果选择器:
class BigAppleTester implements AppleTester{ @Override public Boolean test(Apple apple) { return apple.getWeight()>150; } }
有了这个选择器,我们就可以把这个选择器,亦即我们上面提到的筛选策略,传给我们的筛选器,以此进行相应需求的筛选,只要改变选择器,就可以更换筛选策略:
public static ListfilterSomeApple(List apples,AppleTester tester){ ArrayList resList = new ArrayList<>(); for (Apple apple : apples) { if(tester.test(apple)) resList.add(apple); } return resList; }
ListredApples = filterSomeApple(apples,new RedAppleTester());
ListbigApples = filterSomeApple(apples,new BigAppleTester());
通过使用Java的匿名类来实现选择器接口,我们可以不显式地定义RedAppleTester,BigAppleTester,而进一步简洁代码:
ListredApples = filterSomeApple(apples, new AppleTester() { @Override public Boolean test(Apple apple) { return "red".equals(apple.getColor()); } });
ListbigApples = filterSomeApple(apples, new AppleTester() { @Override public Boolean test(Apple apple) { return apple.getWeight()>150; } });
所以我们已经从上面的说明中看到,我们定义的策略是:一个实现了一般苹果选择器接口的抽象方法的特殊苹果选择器类的对象,因为是对象,所以当然是可以在代码里作为参数来传递的。这也就是我们反复提到的在函数式编程里的策略传递,在原书中叫做「行为参数化的目的是传递代码」。
说到这里,其实这种函数式编程的解决思路并未出现什么Java8的新特性,在低版本的Java上即可实现这个过程,因为思路虽然很绕,但是说到底使用的就是简单的接口实现和方法重写。实际上呢,借助Java 8新的特性,我们可以更方便地使用语法糖来编写更简洁、更易懂的代码。
Java 8 Lambda简洁化函数式编程我们上面定义的这种单方法接口叫做函数式接口:
interface AppleTester{ public Boolean test(Apple apple); }
函数式接口的这个方法就是这个函数式接口的函数,这个函数的「参数-返回值」类型描述叫做函数描述符,test函数的描述符是 Apple->Boolean。
而lambda表达式其实是一种语法糖现象,它是对函数实现的简单表述,比如我们上文的一个函数实现,即实现了AppleTester接口的RedAppleTester:
class RedAppleTester implements AppleTester{ @Override public Boolean test(Apple apple) { return "red".equals(apple.getColor()); } }
这个实现类可以用lambda表达式
(Apple a) -> "red".equals(a.getColor())
或者
(Apple a) -> {return "red".equals(a.getColor());}
来代替。->前是参数列表,后面是表达式或命令。
在有上下文的情况下,甚至有更简洁的写法:
AppleTester tester = a -> "red".equals(a.getColor());
可以这样写的原因在于编译器可以根据上下文来推断参数类型:AppleTester作为函数式接口只定义了单一抽象方法:public Boolean test(Apple apple),所以可以很容易地推断出其抽象方法实现的参数类型。
如果AppleUtils工具类直接定义了判定红苹果的方法:
class AppleUtils { public static Boolean isRedApple(Apple apple) { return "red".equals(apple.getColor()); } }
我们会发现isRedApple方法的方法描述符和函数式接口AppleTester定义的单一抽象方法的函数描述符是一样的:Apple->Boolean,因此我们可以采用一种叫做方法引用的语法糖来进一步化简这个lambda表达式,不需要在lambda表达式中重复写已经定义过的方法:
AppleTester tester = AppleUtils::isRedApple
方法引用之所以可以起作用,就是因为这个被引用的方法具有和引用它的函数式接口的函数描述符相同的方法描述符。在实际创建那个实现了抽象方法的匿名类对象时会将被引用的方法体嵌入到这个实现方法中去:
虽然写起来简洁了,但是在本质上编译器会将lambda表达式编译成一个这样的实现了接口抽象方法的匿名类的对象。
基于lambda表达式简洁而强大的表达能力,可以很容易把上面的这段代码:
ListredApples = filterSomeApple(apples, new AppleTester() { @Override public Boolean test(Apple apple) { return "red".equals(apple.getColor()); } });
改写为Java8版本的:
ListredApples = filterSomeApple(apples, AppleUtils::isRedApple);
如你所见,这样的写法瞬间将代码改到Java8前无法企及的简洁程度。
Java 8泛型函数式接口我们在上文介绍的这个函数式接口:
interface AppleTester{ public Boolean test(Apple apple); }
它的作用仅仅是对苹果进行选择,通过实现test抽象方法来作出具体的选择器。
但是其实在我们的应用环境中,很多需求是泛化的,比如上文中的给一个对象(文中是苹果)以判断其是否能满足某些需求,这个场景一经泛化即可被许多场景所使用,可以使用泛型来对接口进行泛化:
interface ChooseStrategy{ public Boolean test(T t); }
public Boolean test(T t)的函数描述符是T->Boolean,所以只要说满足这个描述符的方法都可以作为方法引用。
同时我们需要一个泛化的filter方法:
public staticList filter(List ts, ChooseStrategy strategy){ ArrayList resList = new ArrayList<>(); for (T t : ts) { if(strategy.test(t)) resList.add(t); } return resList; }
ListredApples = filter(apples,AppleUtils::isRedApple);
除了这种在类型上的泛型来泛化使用定义的函数式接口外,甚至有一些公用的场景Java8 为我们定义了一整套的函数式接口API来涵盖这些使用场景中需要的函数式接口。我们的编程中甚至不需要自己定义这些函数式接口:
java.util.function.Predicate
函数描述符:T->boolean
java.util.function.Consumer
函数描述符:T->void
java.util.function.Function
函数描述符:T->R
java.util.function.Supplier
函数描述符:()->T
java.util.function.UnaryOperator
函数描述符:T->T
java.util.function.BinaryOperator
函数描述符:(T,T)->T
java.util.function.BiPredicate
函数描述符:(L,R)->boolean
java.util.function.BiConsumer
函数描述符:(T,U)->void
java.util.function.BiFunction
函数描述符:(T,U)->R
Java8通过接口抽象方法实现、lambda表达式来实现了策略对象的传递,使得函数成为了第一公民,并以此来将函数式编程带入了Java世界中。
有了策略传递后,使用具体的策略来完成任务,比如本文中筛选苹果的filter过程,Java8则依靠StreamAPI来实现,一系列泛化的任务过程定义在这些API中,这也将是本系列文章的后续的关注。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/69800.html
摘要:初体验下面进入本文的正题表达式。接下来展示表达式和其好基友的配合。吐槽一下方法引用表面上看起来方法引用和构造器引用进一步简化了表达式的书写,但是个人觉得这方面没有的下划线语法更加通用。 感谢同事【天锦】的投稿。投稿请联系 tengfei@ifeve.com 本文主要记录自己学习Java8的历程,方便大家一起探讨和自己的备忘。因为本人也是刚刚开始学习Java8,所以文中肯定有错误和理解偏...
摘要:很多语言等从设计之初就支持表达式。注意此时外部局部变量将自动变为作为方法返回值例子返回判断字符串是否为空判断字符串是否为空今天关于新特性表达式就讲到这里了,接下来我会继续讲述新特性之函数式接口。 上一篇文章我们了解了Java8新特性-接口默认方法,接下来我们聊一聊Java8新特性之Lambda表达式。 Lambda表达式(也称为闭包),它允许我们将函数当成参数传递给某个方法,或者把代码...
摘要:而在中,表达式是对象,它们必须依附于一类特别的对象类型函数式接口。即表达式返回的是函数式接口类型。 Java8被称作Java史上变化最大的一个版本。其中包含很多重要的新特性,最核心的就是增加了Lambda表达式和Stream API。这两者也可以结合在一起使用。首先来看下什么是Lambda表达式。Lambda表达式,维基百科上的解释是一种用于表示匿名函数和闭包的运算符,感觉看到这个解释...
摘要:使用表达式,使得应用变得简洁而紧凑。很多语言等从设计之初就支持表达式。表达式的参数与函数式接口内方法的参数,返回值类型相互对应。更多教程和资料请上腾讯课堂乐字节 showImg(https://segmentfault.com/img/bVbtotg?w=935&h=345); Java8 引入Lambda表达式,允许开发者将函数当成参数传递给某个方法,或者把代码本身当作数据进行处理。...
摘要:之前,使用匿名类给苹果排序的代码是的,这段代码看上去并不是那么的清晰明了,使用表达式改进后或者是不得不承认,代码看起来跟清晰了。这是由泛型接口内部实现方式造成的。 # Lambda表达式在《Java8实战》中第三章主要讲的是Lambda表达式,在上一章节的笔记中我们利用了行为参数化来因对不断变化的需求,最后我们也使用到了Lambda,通过表达式为我们简化了很多代码从而极大地提高了我们的...
阅读 793·2019-08-30 15:55
阅读 1392·2019-08-30 13:55
阅读 1955·2019-08-29 17:13
阅读 2814·2019-08-29 15:42
阅读 1286·2019-08-26 14:04
阅读 999·2019-08-26 13:31
阅读 3240·2019-08-26 11:34
阅读 807·2019-08-23 18:25