第六部分 动态与函数式编程~~第26章 函数式编程

134 阅读10分钟

第26章 函数式编程

Java 8引入了一个重要新语法 -- Lambda表达式,它是一种紧凑的传递代码的方式,利用它,可以实现简洁灵活的函数式编程。

基于Lambda表达式,针对常见的集合数据处理,Java 8引入了一套新的类库,位于包java.util.stream下,称为Stream API。这套API操作数据的思路不同于我们之前介绍的容器类API,它们是函数式的,非常简洁、灵活、易读。

Stream API是对容器类的增强,它可以将对集合数据的多个操作以流水线的方式组合在一起。Java 8还增加了一个新的类CompletableFuture,它是对并发编程的增强,可以方便地将多个有一定依赖关系的异步任务以流水线的方式组合在一起,大大简化多异步任务的开发。

利用Lambda表达式,Java 8还增强了日期和时间API。

本章就来介绍这些Java 8引入的函数式编程特性和API,具体分为5节:26.1节介绍Lambda表达式;26.2节介绍函数式数据处理的基本用法;26.3节重点讨论函数式数据处理中的收集器;26.4节介绍组合式异步编程CompletableFuture;26.5节介绍Java 8的日期和时间API。

26.1 Lambda表达式

Lambda表示到底是什么?有什么用?本机进行详细探讨。Lambda这个名字来源于学术界的演算,具体我们就不探讨了。理解Lambda表达式,我们需要先回顾一下接口、匿名内部类和代码传递。

26.1.1 通过接口传递代码

我们之前介绍过接口以及面向接口的编程,针对接口而非具体类型进行编程,可以降低程序的耦合性,提高灵活性,提高复用性。接口常被用于传递代码, 比如,我们知道File有如下方法:

  public File[] listFiles(FilenameFilter filter)

listFiles需要的其实不是FilenameFilter对象,而是它包含的如下方法:

  boolean accept(File dir, String name);

或者说,listFiles希望接受一段方法代码作为参数,但没有方法直接传递这个方法代码本身,只能传递一个接口。

再如,类Collections中的很多方法都接受一个参数Comparator,比如:

  public static <T> void sort(List<T> list, Comparator<? super T> c)

它们需要的也不是Comparator对象,而是它包含的如下方法:

  int compare(T o1, T o2);

但是,没有办法直接传递方法,只能传递一个接口。

又如,异步任务执行服务 ExecutorService,提交任务的方法有:

   <T> Future<T> sumbit(Callable<T> task);
   Future<T> submit(Runnable task);

Callable和Runnable接口也用于传递任务代码。

通过接口传递行为代码,就要传递一个实现了该接口的实例对象,在之前的章节中,最简洁的方式是使用匿名内部类,比如:

  //列出当前目录下的所有扩展名为.txt的文件
  File f = new File(".");
  File[] files = f.listFiles(new FilenameFilter(){
       @Override
       public boolean accept(File dir, String name){
          if(name.endWith(".txt")){
              return true;
          }
          return false;
       }
  });

将files按照文件名排序,代码为:

  Arrays.sort(files, new Comparator<File>(){
      @Override
      public int compare(File f1, File f2){
           return f1.getName().compareTo(f2.getName());
      }
  });

提交一个最简单的任务,代码为:

   ExecutorService executor = Executors.newFixedThreadPool(100);
   executor.submit(new Runnable(){
       @Override
       public void run(){
           System.out.println("hello world");
       }
   });

26.1.2 Lambda语法

Java 8 提供了一种新的紧凑的传递代码的语法:Lambda表达式。对于前面列出文件的例子,代码可以改为:

    File f = new File(".");
    File[] files = f.listFiles((File dir, String name) -> {
        if(name.endsWith(".txt")){
           return true;
        }
        return false;
    });

可以看出,相比匿名内部类,传递代码变得更为直观,不再有实现接口的模板代码,不再声明方法,也没有名字,而是直接给出了方法的实现代码。Lambda表达式由->分隔为两部分,前面是方法的参数,后面{}内是方法的代码。上面的代码可以简化为:

    File[] files = f.listFiles((File dir, String name) -> {
        return name.endsWith(".txt");
    });

当主体代码只有一条语句的时候,括号和return语句也可以省略,上面的代码可以变为:

    File[] files = f.listFiles((File dir, String name) -> name.endsWith(".txt"));

注意:没有括号的时候,主体代码是一个表达式,这个表达式的值就是函数的返回值,结尾不能加分号,也不能加return语句。

方法的参数类型声明也可以省略,上面的代码还可以继续简化为:

    File[] files = f.listFiles((dir, name) -> name.endsWith(".txt"));

之所以可以省略方法的参数类型,是因为Java可以自动推断出来,它知道listFiles接受的参数类型是FilenameFilter,这个接口只有一个方法accept,这个方法的两个参数类型分别是File和String。这样简化下来,代码是不是简洁多了?

排序的代码用Lambda表达式可以写成:

    Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));

提交任务的代码用Lambda表达式可以写为:

    executor.submit(() -> System.out.println("hello"));

参数部分为空,写为()。 当参数只有一个的时候,参数部分的括号可以省略。比如,File还有如下方法:

    public File[] listFiles(FileFilter filter)

FileFilter的定义为:

    public interface FileFilter{
        boolean accept(File pathname);
    }

使用FileFilter重写上面的列举文件的例子,代码可以为:

    File[] files = f.listFiles(path -> path.getName().endsWith(".txt"));

与匿名内部类类似,Lambda表达式也可以访问定义在主体代码外部的变量,但对于局部变量,它也只能访问final类型的变量,与匿名内部类的区别是,它不要求变量声明为final,但变量事实上不能被重新赋值。比如:

    String msg = "hello world";
    executor.submit(() -> System.out.println(msg));

可以访问局部变量msg,但msg不能被重新赋值,如果这样写:

    String msg = "hello world";
    msg = "good morning";
    executor.submit(() -> System.out.println(msg));

Java编译器会提示错误。

这个原因与匿名内部类是一样的,Java会将msg的值作为参数传递给Lambda表达式,为Lambda表达式建立一个副本,它的代码访问的是这个副本,而不是外部声明的msg变量。如果允许msg被修改,则程序员可能误以为Lambda表达式读到修改后的值,引起更多的混淆。

为什么非要建立副本,直接访问外部的msg变量不行吗?不行,因为msg定义在栈中,当Lambda表达式被执行的时候,msg可能早已被释放了。如果希望能够修改值,可以将变量定义为实例变量,或者将变量定义为数组,比如:

    String[] msg = new String[]{"hello world"};
    msg[0] = "good morning";
    executor.submit(() -> System.out.println(msg[0]));

从以上内容可以看出,Lambda表达式与匿名内部类很像,主要就是简化了语法,那它是不是语法糖,内部实现其实就是内部类呢?答案是否定的,Java会为每个匿名内部类生成一个类,但Lambda表达式不会。 Lambda表达式通常比较短,为每个表达式生成一个类会生成大量的类,性能会收到影响。

内部实现上,Java李勇了Java 7引入的为支持动态类型语言引入的invokeddynamic指令、方法句柄(method handle)等,具体实现比较复杂,我们就不探讨了,感兴趣的读者可以参看cr.openjdk.java.net/~briangoetz… ,我们需要知道的是,Java的实现是非常高效的,不用担心生成太多类的问题。

Lambda表达式不是匿名内部类,那它的类型到底是什么呢?是 函数式接口

26.1.3 函数式接口

Java 8 引入了函数式接口的概念,函数式接口也是接口,但只能有一个抽象方法, 前面提及的接口都只有一个抽象方法,都是函数式接口。之所以强调是“抽象”方法,是因为Java 8中还允许定义静态方法和默认方法。Lambda表达式可以赋值给函数式接口,比如:

    FileFilter filter = path -> path.getName().endsWith(".txt");
    FilenameFilter fileNameFilter = (dir, name) -> name.endsWith(".txt");
    Comparator<File> comparator = (f1, f2) ->  f1.getName().compareTo(f2.getName);
    Runnable task = () -> System.out.println("hello world");

如果看这些接口的定义,会发现它们都有一个注解@FunctionalInterface,比如:

    @FunctionalInterface
    public interface Runnable{
        public abstract void run();
    }

@FunctionalInterface 用于清晰地告知使用者这是一个函数式接口,不过,这个注解不是必需的,不加,只要只有一个抽象方法,也是函数式接口。但如果加了,而又定义了超过一个抽象方法,Java编译器会报错,这类似于我们之前介绍的Override注解。

26.1.4 预定义的函数式接口

Java 8定义了大量的预定义函数式接口,用于常见类型的代码传递,这些函数定义在包java.util.function下,主要接口如表26-1所示。

表26-1 主要的预订函数式接口

函数接口方法定义说明
Predicate<T>boolean test(T t)谓词,测试输入是否满足条件
Function<T,R>R apply(T t)函数转换,输入类型T,输出类型R
Consumer<T>void accept(T t)消费者,输入类型T
Supplier<T>T get()工厂方法
UnaryOperator<T>T apply(T t)函数转换的特例,输入和输出类型一样
BiFunction<T,U,R>R apply(T t, U u)函数转换,接受两个参数,输出R
BinaryOperator<T>T apply(T t, U u)BiFunction的特例,输入和输出类型一样
BiConsumer<T,U>void accept(T t, U u)消费者,接受两个参数
BiPredicate<T,U>boolean test(T t, U u)谓词,接受两个参数

对于基本类型boolean、int、long和double,为了避免装箱/拆箱,Java8提供了一些专门的函数,比如,int相关的部分函数如表26-2所示

表26-2 int类型的函数式接口

函数接口方法定义说明
IntPredicateboolean test(int value)谓词,测试输入是否满足条件
IntFunction<R>R apply(int value)函数转换,输入类型int,输出类型R
IntConsumervoid accept(int value)消费者,输入类型int
IntSupplierint getAsInt()工厂方法

这些函数有什么用呢?它们被大量用于Java 8的函数式数据处理Stream相关的类中,即使不使用Stream,也可以在自己的代码中直接使用这些预定义的函数。我们看一些简单的示例,包括Predicate、Function和Consumer。

1.Predicate示例

为便于举例,我们先定义一个简单的学生类Student,它有name和score两个属性,如下所示。

    static class Student{
        String name;
        double score;
    }

我们省略了构造方法和getter/setter方法。 有一个学生列表:

    List<Student> students = Arrays.asList(new Student[]{
            new Student("zhangsan", 89d), new Student("lisi", 89d),
            new Student("wangwu", 98d) })

在日常开发中,列表处理的一个常见需求是过滤,列表的类型经常不一样,过滤的条件也经常变化,但主体逻辑都是类似的,可以借助Predicate写一个通用的方法,如下所示:

    public static <E> List<E> filter(List<E> list, Predicate<E> pred){
        List<E> retList = new ArrayList<>();
        for(E e : list){
            if(pred.test(e)){
                retList.add(e);
            }
        }
        return retList;
    }

这个方法可以这么用:

    //过滤90分以上的
    students = filter(students, t -> t.getScore() > 90);

2.Function示例

3.Consumer示例

26.1.5 方法引用

Lambda表达式经常用于调用对象的某个方法,比如:

    List<String> names = map(students, t -> t.getName());

这时,它可以进一步简化,如下所示:

    List<String> names = map(students, Student::getName);

Student::getName这种写法是Java 8 引入的一种新语法,称为方法引用。它是Lambda表达式的一种简写方法,由::分隔为两部分,前面是类名或变量名,后面是方法名。方法可以是实例方法,也可以是静态方法,但含义不同。

26.1.6 函数的复合

26.1.7 小结

26.2 函数式数据处理:基本用法

26.3 函数式数据处理:强大方便的收集器

26.4 组合式异步编程

26.5 Java 8的日期和时间API