Java如何支持函数式编程?

开发 后端
Java是面向对象的语言,无法直接调用一个函数。Java 8开始,引入了函数式编程接口与Lambda表达式,便于开发者写出更少更优雅的代码。什么是函数式编程?函数式编程的特点是什么?本文通过代码实例,从Stream类、Lambda表达式和函数接口这三个语法概念来分享Java对函数式编程的支持。

 背景

[[343226]]

在很长的一段时间里,Java一直是面向对象的语言,一切皆对象,如果想要调用一个函数,函数必须属于一个类或对象,然后在使用类或对象进行调用。但是在其它的编程语言中,如JS、C++,我们可以直接写一个函数,然后在需要的时候进行调用,既可以说是面向对象编程,也可以说是函数式编程。从功能上来看,面向对象编程没什么不好的地方,但是从开发的角度来看,面向对象编程会多写很多可能是重复的代码行。比如创建一个Runnable的匿名类的时候:

 

  1. Runnable runnable = new Runnable() { 
  2.     @Override 
  3.     public void run() { 
  4.         System.out.println("do something..."); 
  5.     } 
  6. }; 

这一段代码中真正有用的只有run方法中的内容,剩余的部分都是属于Java编程语言的结构部分,没什么用,但是要写。幸运的是Java 8开始,引入了函数式编程接口与Lambda表达式,帮助我们写更少更优雅的代码:

 

  1. // 一行即可 
  2. Runnable runnable = () -> System.out.println("do something..."); 

现在主流的编程范式主要有三种,面向过程、面向对象和函数式编程。

函数式编程并非一个很新的东西,早在50多年前就已经出现了。近几年,函数式编程越来越被人关注,出现了很多新的函数式编程语言,比如Clojure、Scala、Erlang等。一些非函数式编程语言也加入了很多特性、语法、类库来支持函数式编程,比如Java、Python、Ruby、JavaScript等。除此之外,Google Guava也有对函数式编程的增强功能。

函数式编程因其编程的特殊性,仅在科学计算、数据处理、统计分析等领域,才能更好地发挥它的优势,所以它并不能完全替代更加通用的面向对象编程范式。但是作为一种补充,它也有很大存在、发展和学习的意义。

什么是函数式编程

函数式编程的英文翻译是Functional Programming。

那到底什么是函数式编程呢?实际上,函数式编程没有一个严格的官方定义。严格上来讲,函数式编程中的“函数”,并不是指我们编程语言中的“函数”概念,而是指数学“函数”或者“表达式”(例如:y=f(x))。不过,在编程实现的时候,对于数学“函数”或“表达式”,我们一般习惯性地将它们设计成函数。所以,如果不深究的话,函数式编程中的“函数”也可以理解为编程语言中的“函数”。

每个编程范式都有自己独特的地方,这就是它们会被抽象出来作为一种范式的原因。面向对象编程最大的特点是:以类、对象作为组织代码的单元以及它的四大特性。面向过程编程最大的特点是:以函数作为组织代码的单元,数据与方法相分离。那函数式编程最独特的地方又在哪里呢?实际上,函数式编程最独特的地方在于它的编程思想。函数式编程认为程序可以用一系列数学函数或表达式的组合来表示。函数式编程是程序面向数学的更底层的抽象,将计算过程描述为表达式。不过,这样说你肯定会有疑问,真的可以把任何程序都表示成一组数学表达式吗?

理论上讲是可以的。但是,并不是所有的程序都适合这么做。函数式编程有它自己适合的应用场景,比如科学计算、数据处理、统计分析等。在这些领域,程序往往比较容易用数学表达式来表示,比起非函数式编程,实现同样的功能,函数式编程可以用很少的代码就能搞定。但是,对于强业务相关的大型业务系统开发来说,费劲吧啦地将它抽象成数学表达式,硬要用函数式编程来实现,显然是自讨苦吃。相反,在这种应用场景下,面向对象编程更加合适,写出来的代码更加可读、可维护。

再具体到编程实现,函数式编程跟面向过程编程一样,也是以函数作为组织代码的单元。不过,它跟面向过程编程的区别在于,它的函数是无状态的。何为无状态?简单点讲就是,函数内部涉及的变量都是局部变量,不会像面向对象编程那样,共享类成员变量,也不会像面向过程编程那样,共享全局变量。函数的执行结果只与入参有关,跟其他任何外部变量无关。同样的入参,不管怎么执行,得到的结果都是一样的。这实际上就是数学函数或数学表达式的基本要求。举个例子:

 

  1. // 有状态函数: 执行结果依赖b的值是多少,即便入参相同, 
  2. // 多次执行函数,函数的返回值有可能不同,因为b值有可能不同。 
  3. int b; 
  4. int increase(int a) { 
  5.   return a + b; 
  6.  
  7. // 无状态函数:执行结果不依赖任何外部变量值 
  8. // 只要入参相同,不管执行多少次,函数的返回值就相同 
  9. int increase(int a, int b) { 
  10.   return a + b; 

不同的编程范式之间并不是截然不同的,总是有一些相同的编程规则。比如不管是面向过程、面向对象还是函数式编程,它们都有变量、函数的概念,最顶层都要有main函数执行入口,来组装编程单元(类、函数等)。只不过,面向对象的编程单元是类或对象,面向过程的编程单元是函数,函数式编程的编程单元是无状态函数。

Java对函数式编程的支持

实现面向对象编程不一定非得使用面向对象编程语言,同理,实现函数式编程也不一定非得使用函数式编程语言。现在,很多面向对象编程语言,也提供了相应的语法、类库来支持函数式编程。

Java这种面向对象编程语言,对函数式编程的支持可以通过一个例子来描述:

 

  1. public class Demo { 
  2.   public static void main(String[] args) { 
  3.     Optional<Integer> result = Stream.of("a""be""hello"
  4.             .map(s -> s.length()) 
  5.             .filter(l -> l <= 3) 
  6.             .max((o1, o2) -> o1-o2); 
  7.     System.out.println(result.get()); // 输出2 
  8.   } 

这段代码的作用是从一组字符串数组中,过滤出长度小于等于3的字符串,并且求得这其中的最大长度。

Java为函数式编程引入了三个新的语法概念:Stream类、Lambda表达式和函数接口(Functional Inteface)。Stream类用来支持通过“.”级联多个函数操作的代码编写方式;引入Lambda表达式的作用是简化代码编写;函数接口的作用是让我们可以把函数包裹成函数接口,来实现把函数当做参数一样来使用(Java 不像C那样支持函数指针,可以把函数直接当参数来使用)。

Stream类

假设我们要计算这样一个表达式:(3-1)*2+5。如果按照普通的函数调用的方式写出来,就是下面这个样子:

 

  1. add(multiply(subtract(3,1),2),5); 

不过,这样编写代码看起来会比较难理解,我们换个更易读的写法,如下所示:

 

  1. subtract(3,1).multiply(2).add(5); 

在Java中,“.”表示调用某个对象的方法。为了支持上面这种级联调用方式,我们让每个函数都返回一个通用的Stream类对象。在Stream类上的操作有两种:中间操作和终止操作。中间操作返回的仍然是Stream类对象,而终止操作返回的是确定的值结果。

再来看之前的例子,对代码做了注释解释。其中map、filter是中间操作,返回Stream类对象,可以继续级联其他操作;max是终止操作,返回的不是Stream类对象,无法再继续往下级联处理了。

 

  1. public class Demo { 
  2.   public static void main(String[] args) { 
  3.     Optional<Integer> result = Stream.of("f""ba""hello") // of返回Stream<String>对象 
  4.             .map(s -> s.length()) // map返回Stream<Integer>对象 
  5.             .filter(l -> l <= 3) // filter返回Stream<Integer>对象 
  6.             .max((o1, o2) -> o1-o2); // max终止操作:返回Optional<Integer
  7.     System.out.println(result.get()); // 输出2 
  8.   } 

Lambda表达式

前面提到Java引入Lambda表达式的主要作用是简化代码编写。实际上,我们也可以不用Lambda表达式来书写例子中的代码。我们拿其中的map函数来举例说明。

下面三段代码,第一段代码展示了map函数的定义,实际上,map函数接收的参数是一个Function接口,也就是函数接口。第二段代码展示了map函数的使用方式。第三段代码是针对第二段代码用Lambda表达式简化之后的写法。实际上,Lambda表达式在Java中只是一个语法糖而已,底层是基于函数接口来实现的,也就是第二段代码展示的写法。

 

  1. // Stream类中map函数的定义: 
  2. public interface Stream<T> extends BaseStream<T, Stream<T>> { 
  3.   <R> Stream<R> map(Function<? super T, ? extends R> mapper); 
  4.   //...省略其他函数... 
  5.  
  6. // Stream类中map的使用方法示例: 
  7. Stream.of("fo""bar""hello").map(new Function<String, Integer>() { 
  8.   @Override 
  9.   public Integer apply(String s) { 
  10.     return s.length(); 
  11.   } 
  12. }); 
  13.  
  14. // 用Lambda表达式简化后的写法: 
  15. Stream.of("fo""bar""hello").map(s -> s.length()); 

Lambda表达式包括三部分:输入、函数体、输出。表示出来的话就是下面这个样子:

 

  1. (a, b) -> { 语句1;语句2;...; return 输出; } //a,b是输入参数 

实际上,Lambda表达式的写法非常灵活。上面给出的是标准写法,还有很多简化写法。比如,如果输入参数只有一个,可以省略 (),直接写成 a->{…};如果没有入参,可以直接将输入和箭头都省略掉,只保留函数体;如果函数体只有一个语句,那可以将{}省略掉;如果函数没有返回值,return语句就可以不用写了。

 

  1. Optional<Integer> result = Stream.of("f""ba""hello"
  2.         .map(s -> s.length()) 
  3.         .filter(l -> l <= 3) 
  4.         .max((o1, o2) -> o1-o2); 
  5.          
  6. // 还原为函数接口的实现方式 
  7. Optional<Integer> result2 = Stream.of("fo""bar""hello"
  8.         .map(new Function<String, Integer>() { 
  9.           @Override 
  10.           public Integer apply(String s) { 
  11.             return s.length(); 
  12.           } 
  13.         }) 
  14.         .filter(new Predicate<Integer>() { 
  15.           @Override 
  16.           public boolean test(Integer l) { 
  17.             return l <= 3; 
  18.           } 
  19.         }) 
  20.         .max(new Comparator<Integer>() { 
  21.           @Override 
  22.           public int compare(Integer o1, Integer o2) { 
  23.             return o1 - o2; 
  24.           } 
  25.         }); 

Lambda表达式与匿名类的异同集中体现在以下三点上:

  • Lambda就是为了优化匿名内部类而生,Lambda要比匿名类简洁的多得多。
  • Lambda仅适用于函数式接口,匿名类不受限。
  • 即匿名类中的this是“匿名类对象”本身;Lambda表达式中的this是指“调用Lambda表达式的对象”。

函数接口

实际上,上面一段代码中的Function、Predicate、Comparator都是函数接口。我们知道,C语言支持函数指针,它可以把函数直接当变量来使用。

但是,Java没有函数指针这样的语法。所以它通过函数接口,将函数包裹在接口中,当作变量来使用。实际上,函数接口就是接口。不过,它也有自己特别的地方,那就是要求只包含一个未实现的方法。因为只有这样,Lambda表达式才能明确知道匹配的是哪个方法。如果有两个未实现的方法,并且接口入参、返回值都一样,那Java在翻译Lambda表达式的时候,就不知道表达式对应哪个方法了。

函数式接口也是Java interface的一种,但还需要满足:

  • 一个函数式接口只有一个抽象方法(single abstract method);
  • Object类中的public abstract method不会被视为单一的抽象方法;
  • 函数式接口可以有默认方法和静态方法;
  • 函数式接口可以用@FunctionalInterface注解进行修饰。

满足这些条件的interface,就可以被视为函数式接口。例如Java 8中的Comparator接口:

 

  1. @FunctionalInterface 
  2. public interface Comparator<T> { 
  3.     /** 
  4.      * single abstract method 
  5.      * @since 1.8 
  6.      */ 
  7.     int compare(T o1, T o2); 
  8.  
  9.     /** 
  10.      * Object类中的public abstract method  
  11.      * @since 1.8 
  12.      */ 
  13.     boolean equals(Object obj); 
  14.  
  15.     /** 
  16.      * 默认方法 
  17.      * @since 1.8 
  18.      */ 
  19.     default Comparator<T> reversed() { 
  20.         return Collections.reverseOrder(this); 
  21.     } 
  22.  
  23.      
  24.     /** 
  25.      * 静态方法 
  26.      * @since 1.8 
  27.      */ 
  28.     public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() { 
  29.         return Collections.reverseOrder(); 
  30.     } 
  31.  
  32.     //省略... 

函数式接口有什么用呢?一句话,函数式接口带给我们最大的好处就是:可以使用极简的lambda表达式实例化接口。为什么这么说呢?我们或多或少使用过一些只有一个抽象方法的接口,比如Runnable、ActionListener、Comparator等等,比如我们要用Comparator实现排序算法,我们的处理方式通常无外乎两种:

  • 规规矩矩的写一个实现了Comparator接口的Java类去封装排序逻辑。若业务需要多种排序方式,那就得写多个类提供多种实现,而这些实现往往只需使用一次。
  • 另外一种聪明一些的做法无外乎就是在需要的地方搞个匿名内部类,比如:

 

  1. public class Test {  
  2.     public static void main(String args[]) {  
  3.         List<Person> persons = new ArrayList<Person>(); 
  4.         Collections.sort(persons, new Comparator<Person>(){ 
  5.             @Override 
  6.             public int compare(Person o1, Person o2) { 
  7.                 return Integer.compareTo(o1.getAge(), o2.getAge()); 
  8.             } 
  9.         }); 
  10.     }  

匿名内部类实现的代码量没有多到哪里去,结构也还算清晰。Comparator接口在Jdk 1.8的实现增加了FunctionalInterface注解,代表Comparator是一个函数式接口,使用者可放心的通过lambda表达式来实例化。那我们来看看使用lambda表达式来快速new一个自定义比较器所需要编写的代码:

 

  1. Comparator<Person> comparator = (p1, p2) -> Integer.compareTo(p1.getAge(), p2.getAge()); 

-> 前面的 () 是Comparator接口中compare方法的参数列表,-> 后面则是compare方法的方法体。

下面将Java提供的Function、Predicate这两个函数接口的源码,摘抄如下:

 

  1. @FunctionalInterface 
  2. public interface Function<T, R> { 
  3.     R apply(T t);  // 只有这一个未实现的方法 
  4.  
  5.     default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { 
  6.         Objects.requireNonNull(before); 
  7.         return (V v) -> apply(before.apply(v)); 
  8.     } 
  9.  
  10.     default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { 
  11.         Objects.requireNonNull(after); 
  12.         return (T t) -> after.apply(apply(t)); 
  13.     } 
  14.  
  15.     static <T> Function<T, T> identity() { 
  16.         return t -> t; 
  17.     } 
  18.  
  19. @FunctionalInterface 
  20. public interface Predicate<T> { 
  21.     boolean test(T t); // 只有这一个未实现的方法 
  22.  
  23.     default Predicate<T> and(Predicate<? super T> other) { 
  24.         Objects.requireNonNull(other); 
  25.         return (t) -> test(t) && other.test(t); 
  26.     } 
  27.  
  28.     default Predicate<T> negate() { 
  29.         return (t) -> !test(t); 
  30.     } 
  31.  
  32.     default Predicate<T> or(Predicate<? super T> other) { 
  33.         Objects.requireNonNull(other); 
  34.         return (t) -> test(t) || other.test(t); 
  35.     } 
  36.  
  37.     static <T> Predicate<T> isEqual(Object targetRef) { 
  38.         return (null == targetRef) 
  39.                 ? Objects::isNull 
  40.                 : object -> targetRef.equals(object); 
  41.     } 

@FunctionalInterface注解使用场景

我们知道,一个接口只要满足只有一个抽象方法的条件,即可以当成函数式接口使用,有没有 @FunctionalInterface 都无所谓。但是jdk定义了这个注解肯定是有原因的,对于开发者,该注解的使用一定要三思而后续行。

如果使用了此注解,再往接口中新增抽象方法,编译器就会报错,编译不通过。换句话说,@FunctionalInterface 就是一个承诺,承诺该接口世世代代都只会存在这一个抽象方法。因此,凡是使用了这个注解的接口,开发者可放心大胆的使用Lambda来实例化。当然误用 @FunctionalInterface 带来的后果也是极其惨重的:如果哪天你把这个注解去掉,再加一个抽象方法,则所有使用Lambda实例化该接口的客户端代码将全部编译错误。

特别地,当某接口只有一个抽象方法,但没有用 @FunctionalInterface 注解修饰时,则代表别人没有承诺该接口未来不增加抽象方法,所以建议不要用Lambda来实例化,还是老老实实的用以前的方式比较稳妥。

小结

函数式编程更符合数学上函数映射的思想。具体到编程语言层面,我们可以使用Lambda表达式来快速编写函数映射,函数之间通过链式调用连接到一起,完成所需业务逻辑。Java的Lambda表达式是后来才引入的,由于函数式编程在并行处理方面的优势,正在被大量应用在大数据计算领域。

责任编辑:华轩 来源: 阿里技术
相关推荐

2020-09-23 07:50:45

Java函数式编程

2011-08-24 09:13:40

编程

2013-09-09 09:41:34

2024-02-28 08:37:28

Lambda表达式Java函数式接口

2023-12-14 15:31:43

函数式编程python编程

2022-09-22 08:19:26

WebFlux函数式编程

2016-10-31 20:46:22

函数式编程Javascript

2011-03-08 15:47:32

函数式编程

2020-09-24 10:57:12

编程函数式前端

2017-06-08 14:25:46

Kotlin函数

2023-10-07 00:01:02

Java函数

2010-11-25 09:06:37

Web开发函数式编程

2015-09-28 14:54:08

Java函数式编程

2016-08-11 10:11:07

JavaScript函数编程

2016-08-11 10:34:37

Javascript函数编程

2010-03-11 10:34:22

Scala

2012-09-21 09:21:44

函数式编程函数式语言编程

2012-08-21 09:20:37

函数式编程函数编程编程

2022-07-07 09:03:36

Python返回函数匿名函数

2019-09-09 11:40:18

编程函数开发
点赞
收藏

51CTO技术栈公众号