写点什么

Java 如何支持函数式编程?,大专生面试阿里 P7 居然过了

用户头像
极客good
关注
发布于: 刚刚

那到底什么是函数式编程呢?实际上,函数式编程没有一个严格的官方定义。严格上来讲,函数式编程中的“函数”,并不是指我们编程语言中的“函数”概念,而是指数学“函数”或者“表达式”(例如: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


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


?=?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);?


}?


/**?

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
Java如何支持函数式编程?,大专生面试阿里P7居然过了