Java8引入了与此前彻底不一样的函数式编程方法,经过Lambda表达式和StreamAPI来为Java下的函数式编程提供动力。本文是Java8新特性的第一篇,旨在阐释函数式编程的本义,更在展现Java是如何经过新特性实现函数式编程的。java
最近在读这本图灵的新书:Java 8 in Action ,本书做者的目的在于向Java程序员介绍Java8带来的新特性,鼓励使用新特性来完成更简洁有力的Java编程。本系列的文章的主要思路也来源于本书。程序员
函数式编程并非一个新概念,诸如Haskell这样的学院派编程语言就是以函数式编程为根基的,JVM平台上更完全的采用函数式编程思惟的更是以Scala为表明,所以函数式编程确实不是什么新概念。
下面来谈谈命令式编程和函数式编程。
什么是命令式编程呢?很容易理解,就是一条条的命令明明白白地告诉计算机,计算机依照这些这些明确的命令一步步地执行下去就行了,从汇编到C,这样的命令式编程语言无非都是在模仿计算机的机器指令的下达,明确地在每一句命令里面告诉计算机每一步须要怎么申请内存(对象变量)、怎么跳转到下一句命令(流转),即使后来的为面向对象编程思惟而生的编程语言,好比Java,也仍然未走出这个范式,在每一个类的对象执行具体的方法时也是按照这种“对象变量-流转”的模式在运行的。在这个模式下,咱们会常常发现程序编写可能会常常限于冗长的“非关键”语句,大量的无用命令只是为了照顾语言自己的规则:好比所谓的面向接口编程最终变成了定义了一组一组的interface、interfaceImpl。
函数式编程则试图从编程范式的高度提升代码的抽象表达能力。命令式编程语言把“对象变量”和“流转”看成一等公民,而函数式编程在此基础上加入了“策略变量”这一新的一等公民。策略是什么呢?策略就是函数,函数自己是能够做为变量进行传递的。在以往的编程范式里,策略要被使用时一般是被调用,因此策略的使用必须经过承载策略的类或对象这样的对象变量,而函数式编程里面,咱们能够直接使用策略对象来随意传递,省去了这些没必要要的无用命令。
Java8做为一个新特性版本,在保留原有的Java纯面向对象特性以外,在容易理解的范围内引入了函数式编程方式。编程
咱们有这样的一个引入的例子:咱们有一堆颜色和重量不定的苹果,这些苹果须要通过咱们的一道程序,这道程序能够把这堆苹果中的红苹果取出来。怎样编写程序来选出红苹果呢?app
首先咱们定义苹果Apple类:dom
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<Apple> 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))); } }
若是咱们使用传统的命令式的编程方法,这个从苹果堆中筛选红苹果的方法会这样:ide
public static List<Apple> redAppleFilter(List<Apple> apples){ List<Apple> redApples = new ArrayList<>(); for (Apple apple: apples) { if("red".equals(apple.getColor())){ redApples.add(apple); } } return redApples; }
List<Apple> redApples = redAppleFilter(apples);
若是这个时候咱们变动需求了,好比咱们不筛选红苹果了,要绿苹果了,怎么办呢?就得再定义一个从苹果堆中筛选绿苹果的方法:函数式编程
public static List<Apple> greenAppleFilter(List<Apple> apples){ List<Apple> greenApples = new ArrayList<>(); for (Apple apple: apples) { if("green".equals(apple.getColor())){ greenApples.add(apple); } } return greenApples; }
List<Apple> greenApples = greenAppleFilter(apples);
使用为抽象操做而生的接口:接口只定义抽象的方法,具体的方法实现能够有不一样的类来实现。若是把这些操做放到继承了通常筛选器的不一样筛选方法的筛选器中去就会有一个典型的面向对象式的解决方案了:函数
interface AppleFilter { public List<Apple> filterByRules(List<Apple> apples); } class RedAppleFilter implements AppleFilter{ @Override public List<Apple> filterByRules(List<Apple> apples) { List<Apple> redApples = new ArrayList<>(); for (Apple apple: apples) { if("red".equals(apple.getColor())){ redApples.add(apple); } } return redApples; } } class GreenAppleFilter implements AppleFilter{ @Override public List<Apple> filterByRules(List<Apple> apples) { List<Apple> greenApples = new ArrayList<>(); for (Apple apple: apples) { if("green".equals(apple.getColor())){ greenApples.add(apple); } } return greenApples; } }
咱们发现虽然使用了面向对象的编程方法虽然可使得逻辑结构更为清晰:子类苹果筛选器实现了通常苹果筛选器的抽象方法,但仍然会有大量的代码是出现屡次的。
这就是典型的坏代码的味道,重复编写了两个基本同样的代码,因此咱们要怎么修改才能使得代码应对变化的需求呢,好比能够应对筛选其余颜色的苹果,不要某些颜色的苹果,能够筛选某些重量范围的苹果等等,而不是每一个肯定的筛选都须要编写独立且基本逻辑相同的代码呢?工具
咱们来看一下重复的代码到底是哪些:
List<Apple> greenApples = 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 List<Apple> filterSomeApple(List<Apple> apples,AppleTester tester){ ArrayList<Apple> resList = new ArrayList<>(); for (Apple apple : apples) { if(tester.test(apple)) resList.add(apple); } return resList; }
List<Apple> redApples = filterSomeApple(apples,new RedAppleTester());
List<Apple> bigApples = filterSomeApple(apples,new BigAppleTester());
经过使用Java的匿名类来实现选择器接口,咱们能够不显式地定义RedAppleTester,BigAppleTester,而进一步简洁代码:
List<Apple> redApples = filterSomeApple(apples, new AppleTester() { @Override public Boolean test(Apple apple) { return "red".equals(apple.getColor()); } });
List<Apple> bigApples = filterSomeApple(apples, new AppleTester() { @Override public Boolean test(Apple apple) { return apple.getWeight()>150; } });
因此咱们已经从上面的说明中看到,咱们定义的策略是:一个实现了通常苹果选择器接口的抽象方法的特殊苹果选择器类的对象,由于是对象,因此固然是能够在代码里做为参数来传递的。这也就是咱们反复提到的在函数式编程里的策略传递,在原书中叫作「行为参数化的目的是传递代码」。
说到这里,其实这种函数式编程的解决思路并未出现什么Java8的新特性,在低版本的Java上便可实现这个过程,由于思路虽然很绕,可是说到底使用的就是简单的接口实现和方法重写。实际上呢,借助Java 8新的特性,咱们能够更方便地使用语法糖来编写更简洁、更易懂的代码。
咱们上面定义的这种单方法接口叫作函数式接口:
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表达式简洁而强大的表达能力,能够很容易把上面的这段代码:
List<Apple> redApples = filterSomeApple(apples, new AppleTester() { @Override public Boolean test(Apple apple) { return "red".equals(apple.getColor()); } });
改写为Java8版本的:
List<Apple> redApples = filterSomeApple(apples, AppleUtils::isRedApple);
如你所见,这样的写法瞬间将代码改到Java8前没法企及的简洁程度。
咱们在上文介绍的这个函数式接口:
interface AppleTester{ public Boolean test(Apple apple); }
它的做用仅仅是对苹果进行选择,经过实现test抽象方法来做出具体的选择器。
可是其实在咱们的应用环境中,不少需求是泛化的,好比上文中的给一个对象(文中是苹果)以判断其是否能知足某些需求,这个场景一经泛化便可被许多场景所使用,可使用泛型来对接口进行泛化:
interface ChooseStrategy<T>{ public Boolean test(T t); }
public Boolean test(T t)
的函数描述符是T->Boolean
,因此只要说知足这个描述符的方法均可以做为方法引用。
同时咱们须要一个泛化的filter方法:
public static <T> List<T> filter(List<T> ts, ChooseStrategy<T> strategy){ ArrayList<T> resList = new ArrayList<>(); for (T t : ts) { if(strategy.test(t)) resList.add(t); } return resList; }
List<Apple> redApples = filter(apples,AppleUtils::isRedApple);
除了这种在类型上的泛型来泛化使用定义的函数式接口外,甚至有一些公用的场景Java8 为咱们定义了一整套的函数式接口API来涵盖这些使用场景中须要的函数式接口。咱们的编程中甚至不须要本身定义这些函数式接口:
java.util.function.Predicate<T>
函数描述符:T->boolean
java.util.function.Consumer<T>
函数描述符:T->void
java.util.function.Function<T,R>
函数描述符:T->R
java.util.function.Supplier<T>
函数描述符:()->T
java.util.function.UnaryOperator<T>
函数描述符:T->T
java.util.function.BinaryOperator<T>
函数描述符:(T,T)->T
java.util.function.BiPredicate<L,R>
函数描述符:(L,R)->boolean
java.util.function.BiConsumer<T,U>
函数描述符:(T,U)->void
java.util.function.BiFunction<T,U,R>
函数描述符:(T,U)->R
Java8经过接口抽象方法实现、lambda表达式来实现了策略对象的传递,使得函数成为了第一公民,并以此来将函数式编程带入了Java世界中。有了策略传递后,使用具体的策略来完成任务,好比本文中筛选苹果的filter过程,Java8则依靠StreamAPI来实现,一系列泛化的任务过程定义在这些API中,这也将是本系列文章的后续的关注。