Java8函数式编程学习总结

什么是函数式编程

函数式的概念

函数式编程不是Java8独有的特性,它和面向对象编程一样,是一种编程思想。

函数式编程中的函数一词,是数学意义中函数而非编程上定义的方法(method/function),在数学范畴,函数的定义如下:

给定一个数集A,假设其中的元素为x,对A中的元素x施加对应法则f,记作f(x),得到另一数集B,假设B中的元素为y,则y与x之间的等量关系可以用y=f(x)表示。

一个函数的返回值仅取决于入参,而不依赖其他外在状态,只要输入是确定的,输出就是确定的。这一点在使用java Stream中可以感受到:

  1. 返回结果是新的对象,对原有对象不影响。新对象是原有对象的映射。

  2. 从调用开始到调用结束,不涉及外部变量。相同入参执行的结果永远相同。

函数式与命令式的区别

与平时写的命令式编程相比,主要的区别就是,命令式编程执行存在中间变量,用于存储临时状态,程序的每一步对于计算机来说都是一条确切的指令;而函数式编程使用函数表达式维护计算关系,这种函数关系更符合人类的数学逻辑,也因此更容易被人理解,但是,如果不经过处理,计算机不能理解如此复杂的数学逻辑,它需要的是确切的一步一步的指令,所以最终函数式程序还是被编译成(冯诺依曼机的)机器语言的指令执行。也因此,函数式编程因其让代码逻辑对程序员来说变得更容易理解而被称为语法糖。

综上,函数式编程思维核心就是映射(map),当需要更复杂的逻辑时,把函数当作入参传入另一个函数,组合成更复杂的函数表达式,把函数作为参数传递给另一个函数也是函数式编程的显著特点。

java中如何函数式编程

内部类

java是一门面向对象的语言,java三大特性中的封装,强调利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,也就是javaBean。函数式编程概念中,没有对象和值,只有函数表达式,万物皆公式,那么如何将函数式编程与java融合呢?答案是:内部类,举例如下:

@FunctionalInterface
interface MyFunctionInterface<T> {
    void doSome(T t);
}
​
public class LambdaTest {
    public static void main(String[] args) {
        //1
        MyFunctionInterface<String> myFunctionInterface = new MyFunctionInterface<String>() {
            @Override
            public void doSome(String s) {
              System.out.print(s);
            }
        };
        //2
        MyFunctionInterface<String> myFunctionInterface_lambda =(s)->System.out.print(s);
        //3
        MyFunctionInterface<String> myFunctionInterface_simple =System.out::println;
        myFunctionInterface.doSome("test");
    }
}
  • 首先定义一个只有一个自定义抽象方法的接口,@FunctionalInterface可标可不标。方法的入参是范型T,具体类型由使用者决定。

  • LambdaTest调用MyFunctionInterface执行自定义的逻辑,这里是简单做个打印。

  • 注释1的位置是内部类的常规用法。

  • 注释2的位置可以看到,System.out.print是void的类型,入参是string;doSome也是void类型,入参是string。所以可以使用lambda简化改内部类写法,此时从感官上可以清晰的知道,myFunctionInterface_lambda是输出打印的功能。

  • 注释3的位置,对2式再次做了简化,此时myFunctionInterface_simple被赋值的不像是对象,更像是一个函数,虽然最终编译时仍然是内部类对象。

  • 最常见的例子是Runnable接口,它也只有一个run方法

Runnable r=()->{
  ...
}

函数式接口

上面用到的接口称为函数式接口。函数式接口(Functional Interface)就是一个有且仅有一个抽象方法(Object类的public方法除外),但是可以有多个非抽象方法的接口,比如可以有equals方法,如下所示:

//这个是正确的用法,只含有一个自定义抽象的方法
@FunctionalInterface
interface Compare<T> {
    void compare(A t1, A t2);
}
​
//这个也是正确的用法,虽然接口有两个方法,但是equals是顶级父类Object的public方法,这是允许的
@FunctionalInterface
interface Compare<T> {
    void compare(A t1, A t2);
    boolean equals(Object obj);
}
​
//这是错误的,因为clone虽然是父类的方法,但是是protected类型的,而不是public类型的
@FunctionalInterface
interface Compare<T> {
    void compare(A t1, A t2);
    Object clone();
}
​
//这是正确的用法,函数式接口允许由默认方法。函数式接口是java新增的支持,default可以兼容之前的代码。
@FunctionalInterface
interface Compare<T> {
    void compare(A t1, A t2);
    default void init(){
    }
}

@FunctionInterface并不是一定要标注但若是标注可以在编译时就给你提示错误。

既然是使用内部类实现,那么为什么限制只能是有一个方法呢?内部类可以有多个方法的呀?还是用第一个例子解释:

interface MyFunctionInterface<T> {
    void doSome(T t);
    void doOther();
}
public class LambdaTest {
    public static void main(String[] args) {
      MyFunctionInterface<String> myInterface=new MyFunctionInterface<String>() {
            @Override
            public void doSome(String s) {
​
            }
​
            @Override
            public void doOther() {
​
            }
        };
    }
 }

如上代码所示,内部类确实不限制接口的方法数量,但是多个方法时,无法再用Lambda表达式来简化,也就无法转化为函数式风格的代码了。所以函数式接口只允许又一个自定义抽象方法,是为了配合Lambda表达式的使用。

结合上面我们知道了,在java中编写函数式的代码需要关注以下几点:

  1. 定义函数式接口,接口只有一个自定义抽象方法。

  2. @FunctionalInterface可写可不写,写了就会在编译前做检查。

  3. 结合lambda,设计函数式风格。

  4. java.util.function 它包含了很多类,用来支持 Java的 函数式编程,该包中的函数式接口 43 个,但是最主要的是这四个:

    (1)功能性接口:R Function<T,R> 对类型T参数操作,返回R类型参数,包含方法 R apply(T t)
    (2)断言性接口:boolean Predicate<T> 断言型接口,对类型T进行条件筛选操作,返回boolean,包含方法 boolean test(T t)
    (3)供给性接口:T Supplier<T> 返回T类型参数,方法是 T get()
    (4)消费性接口:void Consumer<T> 对类型T参数操作,无返回结果,包含方法 void accept(T t)

Stream的使用

Stream是函数式编程的优秀实践,对于集合类的处理,使用stream实现的代码清晰又优雅。这里主要记录几个重要的方法和概念

  1. map:映射

    顾名思义,给定一个值转换为另外的值,映射具有函数最显著的特征。

    //给一个用户列表List<UserInfo>,需要取出所有的用户Id:
    List<UserInfo>userList=...;
    List<Integer>userIdList=userList.stream().map(UserInfo::getUserId).collect(Collectors.toList());
  2. reduce:求和操作

    可以将集合中的值按规则聚合在一起,当我们collect收集器无法处理的特殊逻辑,可以考虑使用reduce来自定义收集器。

    //给一串数字,将每个字连接起来,“,”分隔就是规则
    Stream.of("1", "2", "3", "4").reduce((x, y) -> x+","+y).orElse(null);
  3. filter:过滤

    用于从集合中筛选符合条件的子集合

    //筛选得分大于60的用户:
    List<UserInfo>userList=...;
     List<UserInfo> result=userList.stream().filter(user->user.getScore()>60).collect(Collectors.toList());
  4. collect:收集

    集合在流转的过程中依靠Stream类来管理函数计算关系,取值时,通过collect将Stream中的值“收集”起来,除了上面可以将结果收集成List以外,常用的还有toSet,toMap。

  5. parallerStream和CompletableFuture

    CompletableFuture在并行编程中常用到,给定线程池时,他将按设定的线程池的处理任务,不指定线程池时,默认使用ForkJoinPool.commonPool()。

    parallerStream是处理并行任务的Stream,它同时使用了主线程和ForkJoinPool.commonPool创建的线程,当ForkJoinPool不够使用时,将使用main主线程处理任务,极端情况下,parallerStream可以变成单线程的Stream。

函数式编程的优缺点

优点

更高程度的复用,面向对象的复用层次是类,函数式的复用层次是函数,函数通过参数传递组合成新的函数。

函数内部无状态,没有运算过程的中间值,省去给变量命名时的纠结。

函数不依赖外部状态,(多个线程之间)不共享状态,不会产生竞争关系,也就不需要用锁来保护可变状态,也就不会出现死锁,这样可以更好地并发起来。

惰性求值,表达式赋值给变量时并不计算表达式的值,只在变量第一次被使用时才进行计算。

缺点

对于java来说需要编写大量的只有一个接口的函数式接口。

函数式编程没有中间变量,因此无法使用循环(如for循环需要索引,while循环需要跳出条件),取而代之的是递归的调用函数,而递归可能会带来栈溢出的问题,因为每一次函数调用都需要入栈。

复杂业务场景,无法将所有事情都函数表达式化,处理事物间关系,面向对象仍然占有主导优势。

哪些场景适合使用函数式编程

数据处理

逻辑关系运算

处理流程的抽象模块

业务场景慎用

猜你喜欢

转载自blog.csdn.net/u013821237/article/details/104209788
今日推荐