22

Java如何支持函数式编程?

 3 years ago
source link: http://developer.51cto.com/art/202009/626831.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

背景

AfuyAnB.jpg!mobile

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

Runnable runnable = new Runnable() { 
    @Override 
    public void run() { 
        System.out.println("do something..."); 
    } 
}; 

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

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

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

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

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

什么是函数式编程

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

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

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

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

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

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

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

Java对函数式编程的支持

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

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

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

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

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

Stream类

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

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

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

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

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

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

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

Lambda表达式

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

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

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

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

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

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

Optional<Integer> result = Stream.of("f", "ba", "hello") 
        .map(s -> s.length()) 
        .filter(l -> l <= 3) 
        .max((o1, o2) -> o1-o2); 
         
// 还原为函数接口的实现方式 
Optional<Integer> result2 = Stream.of("fo", "bar", "hello") 
        .map(new Function<String, Integer>() { 
          @Override 
          public Integer apply(String s) { 
            return s.length(); 
          } 
        }) 
        .filter(new Predicate<Integer>() { 
          @Override 
          public boolean test(Integer l) { 
            return l <= 3; 
          } 
        }) 
        .max(new Comparator<Integer>() { 
          @Override 
          public int compare(Integer o1, Integer o2) { 
            return o1 - o2; 
          } 
        }); 

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接口:

@FunctionalInterface 
public interface Comparator<T> { 
    /** 
     * single abstract method 
     * @since 1.8 
     */ 
    int compare(T o1, T o2); 
 
    /** 
     * Object类中的public abstract method  
     * @since 1.8 
     */ 
    boolean equals(Object obj); 
 
    /** 
     * 默认方法 
     * @since 1.8 
     */ 
    default Comparator<T> reversed() { 
        return Collections.reverseOrder(this); 
    } 
 
     
    /** 
     * 静态方法 
     * @since 1.8 
     */ 
    public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() { 
        return Collections.reverseOrder(); 
    } 
 
    //省略... 
} 

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

  • 规规矩矩的写一个实现了Comparator接口的Java类去封装排序逻辑。若业务需要多种排序方式,那就得写多个类提供多种实现,而这些实现往往只需使用一次。
  • 另外一种聪明一些的做法无外乎就是在需要的地方搞个匿名内部类,比如:
public class Test {  
    public static void main(String args[]) {  
        List<Person> persons = new ArrayList<Person>(); 
        Collections.sort(persons, new Comparator<Person>(){ 
            @Override 
            public int compare(Person o1, Person o2) { 
                return Integer.compareTo(o1.getAge(), o2.getAge()); 
            } 
        }); 
    }  
} 

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

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

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

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

@FunctionalInterface 
public interface Function<T, R> { 
    R apply(T t);  // 只有这一个未实现的方法 
 
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { 
        Objects.requireNonNull(before); 
        return (V v) -> apply(before.apply(v)); 
    } 
 
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { 
        Objects.requireNonNull(after); 
        return (T t) -> after.apply(apply(t)); 
    } 
 
    static <T> Function<T, T> identity() { 
        return t -> t; 
    } 
} 
 
@FunctionalInterface 
public interface Predicate<T> { 
    boolean test(T t); // 只有这一个未实现的方法 
 
    default Predicate<T> and(Predicate<? super T> other) { 
        Objects.requireNonNull(other); 
        return (t) -> test(t) && other.test(t); 
    } 
 
    default Predicate<T> negate() { 
        return (t) -> !test(t); 
    } 
 
    default Predicate<T> or(Predicate<? super T> other) { 
        Objects.requireNonNull(other); 
        return (t) -> test(t) || other.test(t); 
    } 
 
    static <T> Predicate<T> isEqual(Object targetRef) { 
        return (null == targetRef) 
                ? Objects::isNull 
                : object -> targetRef.equals(object); 
    } 
} 

@FunctionalInterface注解使用场景

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

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

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

小结

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK