天天看点

Java8函数式编程 (一) 数据流和lambda表达式

JDK 1.8中引入了函数式编程(functional programming,FP),如果您已习惯OOP,一定会感到困惑:什么是函数式编程?这样的编程模式有什么好处?

本文将通过简单的实例令读者对函数式编程有一个大体的了解。

我们知道OOP是以类为基础的,程序中必须首先抽象和定义class。那么FP创建的基础是什么?或者说在Java 8中,至少需要了解什么知识点才能实现基本的函数式编程呢?

本文将首先介绍在Java 8中使用FP所需的基本知识点:

  • Lambda表达式
  • 数据流

基本实例

Map<String, List<String>> phoneNumbers = new HashMap<String, List<String>>();
      
phoneNumbers.put("Zhang Jin", Arrays.asList("3232312323", "8933555472"));
      
phoneNumbers.put("Li Ming", Arrays.asList("12323344", "492648333"));
      
phoneNumbers.put("Li Guoping", Arrays.asList("77323344", "938448333"));
      
Map<String, List<String>> filteredNumbers = phoneNumbers.entrySet().stream()
      
    .filter(x -> x.getKey().contains("Li"))
      
    .collect(Collectors.toMap(p -> p.getKey(), p -> p.getValue()));
      
filteredNumbers.forEach((key, value) -> {
      
    System.out.println("Name: " + key + ": ");
      
    value.forEach(System.out::println);
      
});
      

上半部分的代码创建了一个从人名到此人所有电话号码的Map,比较简单,但接下来的部分对刚接触Java 8的读者不是一眼就能理解,我们一会儿来详细解释一下。

数据状态不变原则

在分析上面的code前,先来看看FP与我们所熟知的OOP的最大不同。

在面向对象编程时,我们定义类和对象,并使用方法或表达式来执行命令。这些方命令通常会改变程序的数据状态:

Integer x = 0;
      
x++;
      

比如上面的代码,当执行x++后,x的值产生了变化,旧数据被新数据所取代。

相反,在函数式编程中,尽管我们也需要通过方法来执行命令,但命令本身并不会改变程序已有的数据。简单地说就是函数调用对外界不产生副作用,并总是输出新的变量作为调用结果,比如下面的javascript:

function max(a, b) {
      
   return a > b ? a : b;
      
}
      
var x = 10;
      
var y = 5;
      
var maximum = max(x, y);
      

此处max并不改变和依赖于外部的x和y,同时每次返回的结果都是一个全新的变量。

代码分析

现在我们回到之前的代码:

// create a map, filter it by key values
      
Map <String, List<String>> filteredNumbers = phoneNumbers.entrySet().stream()
      
   .filter(x -> x.getKey().contains("Li"))
      
   .collect(Collectors.toMap(p -> p.getKey(), p -> p.getValue()));
      

这段代码首先调用entrySet()来获得phoneNumbers的entries集合,其中每个entry都由一个键值对组成。

接着,在得到的集合上调用stream()方法,该方法会创建一个数据流 (java.util.stream.Stream)。数据流(stream)是Java 8引入的新概念,参考Javadoc,可以将它理解为是一种可以在其上执行顺序或并行聚合操作的数据序列。这些操作可以包括过滤,修改,不同类型的转换等。

得到stream后,代码调用了filter(),filter通过给定的条件来过滤数据。此处的过滤条件是:

x -> x.getKey().contains("Li")
      

该处采用了Java 8引入的lambda表达式,我们可以把lambda表达式看成一个简洁的匿名函数,->左边的x是输入参数,->右边是需要执行的命令。这里的lambda表达式又被称作predicate,它是一个返回boolean类型的函数,stream中的每个element都会被代入该predicate,通过运算出的结果来决定是否在filter()重新返回的stream中被留下或移除。

下一个方法是collect(),可以看到,这里的方法调用都是链式的,原因在于这些方法都归属于Stream接口,所以使用起来非常方便简洁。

collect()方法的用途很简单,它先从stream内获得数据(这里是java.util.Map.Entry),再将数据转变成通常的java collection,比如List, Map, Set等数据结构。这样就重新将stream转变成了我们所熟悉的类型。

Collection的遍历

来看最后一部分代码:

filteredNumbers.forEach((key, value) -> {
      
   System.out.println("Name: " + key + ": ");
      
   value.stream().forEach(System.out::println);
      
});
      

该代码打印出所有的结果,但并没有使用循环,而是用了Java 8引入的forEach方法。

有了前面的基础,可以很容易看懂:forEach接收一个lambda表达式,filteredNumbers的每个element都讲执行该表达式。由于filteredNumbers是一个map,所以这里lambda表达式参数变成了key, value pair:

// expression takes two parameters
      
(key, value) - > {
      
   // print person’s name
      
   System.out.println("Name: " + key + ": ");
      
   // iterate over the person’s phone numbers and print each of them
      
   value.forEach(System.out::println);
      
}
      

由于value本身是一个List,所以又在其上调用了forEach。

至此代码的输出为:

Name: Li Ming:
      
12323344
      
492648333
Name: Li Guoping:
77323344
938448333
      

学到的知识点 (以及问题)

本文表述的几个关键点:

  • Java 8通过数据流和lambda表达式使函数式编程成为可能。
  • Lambda表达式和匿名函数十分相似。
  • Stream是一种可以在其上执行顺序或并行聚合操作的数据序列,这些操作会作用于序列里的每个元素。
  • 函数式编程模式中的操作倾向于不依赖和不改变已有的数据状态。
  • 数据流操作很简洁性 — 如果不考虑执行效率的话。

但现在您可能会想:

  • 函数式编程写出来的东西理解起来似乎更困难 — 我还是喜欢原来的写法, 尽管它要啰嗦很多。
  • streams操作的执行效率高吗?

继续阅读