关于java:Java如何支持函数式编程

40次阅读

共计 8003 个字符,预计需要花费 21 分钟才能阅读完成。

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

背景

在很长的一段时间里,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 表达式是起初才引入的,因为函数式编程在并行处理方面的劣势,正在被大量利用在大数据计算畛域。

正文完
 0