Introducing FP in Java8

背景

自从2013年放弃了Java就再也没有碰过。期间Java还发布了重大更新:引入lambda,可是那会儿我已经玩了一段时间Scala,对Java已经瞧不上眼。相比Scala Java 8 的lambda仍是too young, too naive!再后来,我有机会学习了一下C#。虽然C#师承Java,但它已是一门很是现代化的语言了,青出于蓝而胜于蓝。首先是LINQ,提供了一致的,而且很是有表达力的数据操做API。还有像扩展函数,匿名类这些语法特性,用起来也是很是趁手。在技术层面微软比甲骨文强了不知道多少倍。并且这几年Java社区也有点萧条,国内再也没有出现过相似JavaEye这样的高质量技术社区。种种缘由都致使我离Java愈来愈远。java

今年五月份,公司要作新的业务,团队里的一些老成员已经被动态语言(主要是Python,还有一些Ruby)折磨的迫不得已,决定从新用回Java。若是不是应为这,我恐怕不再会用Java了。可是工做嘛,没有太多选择的余地。既然团队决定了那就硬着头皮上吧。第一步确定是要了解一下Java8的函数式特性(这但是千呼万唤始出来啊)。这段时间用下来的整体感受是,没我想象中的那么糟,还凑合。下边总结了一些Java8函数式编程的要点:包括Optional,lambda表达式,Stream程序员

Optional

NullPointerException,这个应该是Java里被人诟病最多的了,有的公司应为这个损失的钱可不是小数。那在Java 8里引入了一个Optional类型来解决这个问题。Optional是什么呢?在函数式编程里Optional其实就是一个Monad。和其余编程语言中,好比Scala的Option,Haskell的Maybe殊途同归。express

在没有Optional以前,一般作法是返回null,或者程序抛出异常,具体使用要看团队的规范。但这两种方式都有各自的问题,我再数落一遍。编程

对于返回null,试想若是全部的方法都由可能返回null,那对方法使用者来讲很是恐怖的。到处防,到处防。并且每个null都是一个地雷,哪个地方疏忽了都有可能被“炸”到。json

抛出异常呢,不会有到处写防护代码的问题了。可是这种解决方案也是很是“凑合”。Java中异常分为两种:受检异常和非受检异常。若是使用了非受检异常程序就会直接被异常中断,一般在Spring中是提倡直接抛出非受检异常的,再搭配上Spring的拦截器,省去了程序员很多麻烦。闭包

受检异常就不同了,使用受检异常毫不会比使用null好到哪里去。在方法的签名上附带上函数可能抛出的异常,让方法使用者去判断如何处理这个异常。看起来这是一种负责任的作法,事实确是把本身不想作的事情(异常处理)交给了方法调用者。Jackson类库就是这样,每次使用它序列化对象时都得考虑是try-catch仍是修改方法签名(告知外部方法处理)。很是不友好。app

再有一点,抛出Exception意味着这个函数(方法)是带有反作用的。函数反作用会给程序设计带来没必要要的麻烦,引入潜在的bug,并下降程序的可读性。试想一个函数从其签名上来看应该返回一个订单,可是结果倒是它不总可以返回订单,有时还抛出异常。编程语言

终于能够开始说Optional了。用白话形容,Optional就是一个盒子。当你拿到这个盒子的时候,盒子可能里边有想要的东西,但也可能只是一个空盒子。因此原来返回null,或者跑出异常的方法咱们能够直接返回一个盒子。来一个例子,这个例子来改进一下Jackson的API:ide

public Optional<String> json(Object object) {
    String jsonString = null;
    try {
        jsonString = new ObjectMapper().writeValueAsString(object);
    } catch (JsonProcessingException e) {
        e.printStackTrace();
    }

    return Optional.ofNullable(jsonString);
}

这个新的方法再也不让调用者处理异常,也不存在返回null的风险。可是问题来了,拿到了这个“盒子”咱们接下来该怎么办啊?别着急,Optional提供了很是丰富的接口,几乎可以知足你平常所有的使用场景。如下接口都是很是经常使用的:函数式编程

  1. 判断Optional是否有值:op.isPresent()

  2. 基于Optional封装的结果作一些操做,并继续返回一个Optional类型的结果:op.map(str -> str.trim())

  3. 合并两个Optional为一个:op.flatMap(s -> op2.map(s2 -> s1 + s2))

  4. 只有在有值的状况下才进行一些操做:op.ifPresent(s -> s.trim())

  5. 若是Optional有值则返回,没有返回给定的默认值:op.orElse("hello")
    还有一些其它的接口,请自行查阅Optional API文档。注意在使用Optional的时候不推荐直接使用get(),由于这样可能会抛出异常。

我曾经把方法的参数也置为Optional类型,可是的到了IDEA的警告:Optional不推荐用做方法的参数。我随后Google了一些帖子,获得的答案就是:对于方法的参数,null可能比Optional更好用一些。在Scala和Haskell里是没有这样的限制的,你能够任意的把Option和Maybe当作参数。由于在Scala和Haskell中Option和Maybe是基本的数据类型。Java中的Optional则只是一种受限的实现,主要的目的是提供一种清晰,友好的表达“空”的方式。

Lambda 表达式

lambda表达式在上边咱们已经用到了,好比op.map(str -> str.trim())str -> str.trim()就是一个lambda表达式。Java和其余大多数支持lambda的语言同样采用箭头:-> 来标示lambda。在箭头的的左边是0或多个参数,箭头的右边是一个表达式或者代码块。咱们来看几个lambda的实例:

() -> {}                // No parameters; result is void
() -> 42                // No parameters, expression body
() -> null              // No parameters, expression body
() -> { return 42; }    // No parameters, block body with return
() -> { System.gc(); }  // No parameters, void block body

() -> {                 // Complex block body with returns
  if (true) return 12;
  else {
    int result = 15;
    for (int i = 1; i < 10; i++)
      result *= i;
    return result;
  }
}                          

(int x) -> x+1              // Single declared-type parameter
(int x) -> { return x+1; }  // Single declared-type parameter
(x) -> x+1                  // Single inferred-type parameter
x -> x+1                    // Parentheses optional for
                            // single inferred-type parameter

(String s) -> s.length()      // Single declared-type parameter
(Thread t) -> { t.start(); }  // Single declared-type parameter
s -> s.length()               // Single inferred-type parameter
t -> { t.start(); }           // Single inferred-type parameter

(int x, int y) -> x+y  // Multiple declared-type parameters
(x, y) -> x+y          // Multiple inferred-type parameters
(x, int y) -> x+y    // Illegal: can't mix inferred and declared types
(x, final y) -> x+y  // Illegal: no modifiers with inferred types

忆苦思甜,咱们先来回忆一下在没有lambda以前若是咱们想为Optional实现map功能咱们应该如何作。一般作法是这样的:

interface Function<E> {
    public E exec(E e);
}

public static <E> Optional<E> map(Optional<E> original, Function<E> fn) {
    if(!original.isPresent()) return Optional.empty();
    E e = original.get();
    return Optional.of(fn.exec(e));
}

map(Optional.of(1), new Function<Integer>() {
    @Override public Integer exec(final Integer i) {
        return i * i;
    }
});

使用一个接口来承载一个函数。在Java中函数不是一等公民,是不能用来传递的。因此只能采用这种“曲线救国”的方式。Java 8则是把这种”曲线救国”拿到了台面上,并昭告天下,同时还对lambda提供了一些语法支持。因此上边咱们看到的一些很是简短的lambda表达式,其实都是一个interface+一个抽象方法。

咱们能够借助IDE(我使用的是IDEA)把上边列举的一些lambda表达式抽做变量,IDE能够帮助咱们自动推导类型,咱们来观察下他们的类型。

Runnable runnable = () -> {};
DoubleSupplier doubleSupplier = () -> 42;
Callable vCallable = () -> null;
IntToDoubleFunction intToDoubleFunction = (int x) -> x + 1;
IntBinaryOperator intBinaryOperator = (int x, int y) -> x + y;

像Callable,Runnable,DoubleSupplier这些接口就是Java内置的函数式接口。Java还提供了很是多的函数式接口,你能够在java.util.function下找到他们。函数式接口相比普通的接口有一个限制:只能有一个抽象方法。并且Java还提供了一个注解:@FunctionalInterface。你能够本身声明新的接口并为它加上这个注解。

@FunctionalInterface
interface Function<E> {
    public E exec(E e);
}

上边说过Java 8对lambda提供了一些额外支持,这种额外的支持就是一些已经实现的方法也可以用做lambda表达式。咱们看一个例子:

Optional<Integer> arg = ...;
arg.ifPresent(System.out::print);

print是PrintStream中已经实现的方法。这种用法至关于:x -> System.out.print(x)。对于类的静态方法,类的实例方法,对象的实例方法均可以使用::操做符用在须要传递lambda表达式的地方。

咱们接下来比较一下Java8先后,实现闭包的异同。先来看一下闭包的概念。
闭包是指能够包含自由变量的代码块。自由变量没有在当前代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(执行环境上下文)。因此一次闭包过程即要执行的代码块中的自由变量得到了执行环境上下文中的值。

在Java 8以前闭包能够经过内部类来实现。

import java.util.HashMap;
import java.util.Map;

class Cache {
    private Map<String, Object> contents = new HashMap<>();

    class Monitor {
        public Integer size() {
            return Cache.this.contents.size();
        }
    }
}

public class Library {
    public static void main(String[] args) {
        Cache cache = new Cache();
        Cache.Monitor monitor = cache.new Monitor();
        System.out.println("Cache size is: " + monitor.size());
    }
}

contents是自由变量,cache提供执行上下文,monitor.size()触发闭包执行。若是有其余函数式语言背景的人看到这种方式可能会感到很是的奇怪,但这就是Java的方式。再来看一下Java 8中如何实现一个闭包。
因为有了lambda表达式,建立一个闭包就至关简洁了:(Integer x) -> x + y。并且这种形式也很是的functional。接着建立一个执行上下文:

int y = 1;
Function<Integer, Integer> add = (Integer x) -> x + y;

add.apply(3); // 4

两种风格迥异,单从语法表达力来讲确定是lambda更胜一筹。但社区里也有人担忧这种简洁性会影响Java的简单,形成代码风格不一致,下降代码可读性增长维护成本。仁者见仁智者见智吧,我确定是支持使用lambda的。代码风格的话团队最好能有一个标准,不要好东西给用烂了。

Stream

Stream能够说是集合处理的杀手锏(就当它是杀手锏吧)。咱们先拿Stream来玩一玩:
Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).map(i -> i * i).reduce(0, (i, r) -> r + i)
这个例子是计算0到9的平方和。咱们能够想象一下若是用for循环实现一样的逻辑,代码行数至少是四五倍。

下面咱们对这个程序做一个分解:第一部分是初始化Stream,加载数据Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);第二部分是定义数据转化:map(i -> i * i),第三部分是聚合结果:reduce(0, (i, r) -> r + i)。这基本囊括了Stream全部能做以及要做的事情。

建立函数

Stream类提供了提供了几个工厂方法来构建一个新的Stream。咱们先来看一下of,of用来构建有限个数的Stream,好比咱们构建一个包含十个元素的Stream:Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 0)

Stream还有两个方法:generate, iterate,这两个函数都是用来构造无限流。

generate就收一个无参函数:Supplier<T> s,当须要一个常数或随机数队列的时候可使用这个函数。 Stream.generate(() -> 1)

iterate会生成一个迭代的Stream,你须要指定初始值,以及迭代函数。Stream.iterate(5, x -> x * 10)

除了上边的几种方式,咱们还可以方便的将集合转化为Stream,好比List,Set。你只须要在集合变量上.stream()就能够获得一个Stream。实际场景中应用更多的仍是将一个集合类转化为Stream。

转换函数

转化函数咱们最经常使用的是map和filter。Stream也提供了flatMap函数,可是它的使用比较受限,因此本来flatMap的威力大大减弱了。下边具体解释一下map和filter函数。map的工做原理是:为全部的元素应用一个函数。为了加强理解举个现实生活中的例子:有一篓子苹果,咱们要为它们都贴上标签。map过程就是拿出每一个苹果贴上标签而后放到另外一个篓子里。filter就是一个过滤器,过滤出咱们想要的东西。好比,咱们要从上边篓子里挑选出大于500克的苹果。逐个拿出苹果称重,若是大于500克留在篓子中,则放到新篓子中。因此操做数据时,直接往这两个例子上套用就能够了。

看一个例子,计算全部学生的总分,并取出总分大于450分的学生:
students.map(s -> calculate(s.getScore())).filter(score -> score > 450);

聚合函数

聚合函数主要用来收集最终的结果。好比,求出一个数字队列的总和,求最大最小值,或将结果收集到一个集合中。下边咱们来操做一个Integer的Stream,首先求出最大值和最小值:

Stream<Integer> ints = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 0);

Optional<Integer> max = ints.max(Integer::compare);
Optional<Integer> min = ints.min(Integer::compare);

接下来咱们求出这个数列的总和,求和咱们可使用reduce函数。reduce函数起到一个汇总的做用。它和hadoop中的reduce,fork/join中的join做用都是同样的。

Integer sum = ints.reduce(0, (x, y) -> x + y);

咱们为reduce传递了两个参数,第一个是初始值(执行累加操做的第一个值),第二个是求和lambda。

Java 8还为基本类型提供了相应的Stream,好比IntStream,LongStream。使用IntStream咱们直接使用sum就能够执行求和操做:

IntStream intsStream = ints.mapToInt(i -> i);
intsStream.sum();

下边咱们看一下收集函数:collect()。collect()函数是将Stream中的元素放到一个新的容器中。咱们上边的例子中求出了总分大于450分的同窗,咱们把它放到一个List中,看一下如何操做:

students.map(s -> calculate(s.getScore())).filter(score -> score > 450).collect(Collectors.toList())

以上Java 8的函数式特性所有讲完,这只是一个入门讲解,不能涵盖全部的特性及工具类。有兴趣的同窗,自行探索java.util.stream包。

相关文章
相关标签/搜索