阿里妹攻略:Java是面向对象的语言,不能直接调用函数。 从 Java 8 开始,引入了函数式编程接口和 Lambda 表达式,方便开发者编写更少、更优雅的代码。 什么是函数式编程? 函数式编程有什么特点? 本文通过代码示例从Stream类、Lambda表达式和函数接口三个语法概念来分享Java对函数式编程的支持。
文末福利: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))。 但是,在实现编程时,对于数学上的“函数”或者“表达式”,我们一般习惯性地设计成函数。 因此,如果不深究的话,函数式编程中的“函数”也可以理解为编程语言中的“函数”。
每个编程范式都有其独特的方式,这就是为什么它们被抽象为范式的原因。 面向对象程序设计的最大特点是:以类和对象为组织代码的单位及其四大特点。 面向过程编程最大的特点是以函数作为代码组织的单位,数据和方法是分离的。 函数式编程最独特的地方在哪里? 其实,函数式编程最独特的地方在于它的编程思想。 函数式编程假设程序可以表示为一系列数学函数或表达式的组合。 函数式编程是对数学编程的低级抽象,将计算过程描述为表达式。 不过extends在java中什么意思,你这么一说,肯定会有疑惑。 任何程序真的可以表示为一组数学表达式吗?
理论上是可以的。 但是,并非所有程序都适用于此。 函数式编程有自己适合的应用场景,比如科学计算、数据处理、统计分析等,在这些领域中,程序往往更容易用数学表达式来表达。 与非函数式编程相比,要实现同样的功能,函数式编程只需要很少的代码就可以完成。 但是,对于业务关联性强的大型业务系统的开发,如果难以将其抽象成数学表达式,硬要用函数式编程来实现,显然是自找麻烦。 相反,在这种应用场景下,面向对象编程更适合,写出的代码也更具可读性和可维护性。
在编程实现上,函数式编程和面向过程编程一样,也是以函数为单位来组织代码。 但是,它与过程编程的不同之处在于它的功能是无状态的。 什么是无国籍? 简单来说,函数中涉及的变量都是局部变量,不会像面向对象编程那样共享类成员变量,也不会像面向过程编程那样共享全局变量。 函数的执行结果只与输入参数有关,与其他任何外部变量无关。 同样的输入参数,无论怎么执行,结果都是一样的。 这其实是对数学函数或者数学表达式的基本要求。 例如:
// 有状态函数: 执行结果依赖b的值是多少,即便入参相同,
// 多次执行函数,函数的返回值有可能不同,因为b值有可能不同。
int b;
int increase(int a) {
return a + b;
}
// 无状态函数:执行结果不依赖任何外部变量值
// 只要入参相同,不管执行多少次,函数的返回值就相同
int increase(int a, int b) {
return a + b;
}
不同的编程范式并不是截然不同的,总有一些共同的编程规则。 比如无论是面向过程、面向对象还是函数式编程,它们都有变量和函数的概念,顶层必须有一个main函数执行入口来组装编程单元(类、函数等) . 但是extends在java中什么意思,面向对象的编程单元是类或对象,面向过程的编程单元是函数,而函数式编程的编程单元是无状态函数。
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 表达式和函数式接口。 Stream类用于支持通过“.”级联多个函数操作的代码编写方式; 引入Lambda表达式的作用是简化代码编写; 函数接口的作用就是让我们把函数包装成一个函数接口,实现函数将其作为参数使用(Java不像C那样支持函数指针,可以直接将函数作为参数使用)。
流类
假设我们要计算这样一个表达式:(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对象
.map(s -> s.length()) // map返回Stream对象
.filter(l -> l <= 3) // filter返回Stream对象
.max((o1, o2) -> o1-o2); // max终止操作:返回Optional
System.out.println(result.get()); // 输出2
}
}
拉姆达表达式
前面说过,Java引入Lambda表达式的主要作用是简化代码编写。 其实我们也可以不使用Lambda表达式来编写示例中的代码。 我们以地图功能为例。
下面三段代码,第一段代码展示了map函数的定义,其实map函数接收的参数是一个Function接口,即函数接口。 第二段代码显示了如何使用 map 函数。 第三段代码是将第二段代码用Lambda表达式简化后写的。 其实Lambda表达式只是Java中的一个语法糖,底层是基于函数接口实现的,也就是第二个代码所示的写法。
// Stream类中map函数的定义:
public interface Stream extends BaseStream<T, Stream> {
Stream map(Function<? super T, ? extends R> mapper);
//...省略其他函数...
}
// Stream类中map的使用方法示例:
Stream.of("fo", "bar", "hello").map(new Function<String, Integer>() {
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 result = Stream.of("f", "ba", "hello")
.map(s -> s.length())
.filter(l -> l <= 3)
.max((o1, o2) -> o1-o2);
// 还原为函数接口的实现方式
Optional result2 = Stream.of("fo", "bar", "hello")
.map(new Function() {
public Integer apply(String s) {
return s.length();
}
})
.filter(new Predicate() {
public boolean test(Integer l) {
return l <= 3;
}
})
.max(new Comparator() {
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
Lambda表达式与匿名类的异同点集中在以下三点:
功能接口
其实上面代码中的Function、Predicate、Comparator都是函数式接口。 我们知道C语言支持函数指针,可以直接把函数当作变量使用。
但是,Java 没有这样的函数指针语法。 所以它使用函数接口,将函数包装在接口中,作为变量使用。 事实上,函数式接口就是一个接口。 不过它也有自己特殊的地方,就是要求只包含一个未实现的方法。 因为只有这样,Lambda表达式才能清楚的知道匹配的是哪个方法。 如果有两个未实现的方法,并且接口入参和返回值相同,那么Java在翻译Lambda表达式的时候就不知道这个表达式对应的是哪个方法。
函数式接口也是Java接口的一种,但也需要满足:
满足这些条件的接口可以被认为是功能接口。 例如 Java 8 中的 Comparator 接口:
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 reversed() {
return Collections.reverseOrder(this);
}
/**
* 静态方法
* @since 1.8
*/
public static <T extends Comparable<? super T>> Comparator reverseOrder() {
return Collections.reverseOrder();
}
//省略...
}
函数式接口有什么用? 总之,函数式接口给我们带来的最大好处就是可以使用最少的lambda表达式来实例化接口。 你为什么这么说? 我们或多或少使用过只有一个抽象方法的接口,比如Runnable、ActionListener、Comparator等,比如我们要用Comparator实现一个排序算法,我们的处理方法通常不外乎两种:
public class Test {
public static void main(String args[]) {
List persons = new ArrayList();
Collections.sort(persons, new Comparator(){
public int compare(Person o1, Person o2) {
return Integer.compareTo(o1.getAge(), o2.getAge());
}
});
}
}
匿名内部类实现的代码量并不多,结构也比较清晰。 Comparator接口在Jdk 1.8中实现了一个FunctionalInterface注解,这意味着Comparator是一个函数式接口,用户可以安全地通过lambda表达式实例化它。 然后让我们看一下使用lambda表达式快速新建自定义比较器需要编写的代码:
Comparator<Person> comparator = (p1, p2) -> Integer.compareTo(p1.getAge(), p2.getAge());
->前面的()是Comparator接口中compare方法的参数列表,->后面是compare方法的方法体。
Java提供的Function和Predicate接口源码摘录如下:
public interface Function<T, R> {
R apply(T t); // 只有这一个未实现的方法
default Function compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default Function andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static Function identity() {
return t -> t;
}
}
public interface Predicate<T> {
boolean test(T t); // 只有这一个未实现的方法
default Predicate and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate negate() {
return (t) -> !test(t);
}
default Predicate or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static Predicate isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
@FunctionalInterface注解使用场景
我们知道,一个接口只要满足只有一个抽象方法的条件,就可以作为一个函数式接口来使用,与有没有@FunctionalInterface无关。 但是jdk定义这个注解肯定是有原因的。 对于开发者来说,在使用这个注解之前一定要三思。
如果使用这个注解,在接口中添加抽象方法,编译器会报错,编译失败。 换句话说,@FunctionalInterface 是一个承诺,只有这个抽象方法会在这个接口中世代存在。 因此,开发人员可以使用 Lambda 实例化任何使用此注解的接口。 当然,滥用@FunctionalInterface 的后果也是极其灾难性的:如果有一天你去掉这个注解,再加上一个抽象方法,那么所有使用 Lambda 实例化接口的客户端代码都会编译错误。
特别是当一个接口只有一个抽象方法,但没有用@FunctionalInterface注解修饰时,就意味着别人没有承诺该接口以后不会添加抽象方法,所以建议不要使用Lambda来实例化,还是老老实实用前面的方法比较稳定。
概括
函数式编程更符合数学中函数映射的思想。 具体到编程语言层面,我们可以使用Lambda表达式快速编写函数映射,通过链式调用将函数连接在一起,完成所需的业务逻辑。 后来引入了Java的Lambda表达式。 由于函数式编程在并行处理方面的优势,被广泛应用于大数据计算领域。
Java 微服务沙箱体验挑战
在 10 分钟内构建一个 Task Manager 任务管理器
使用阿里云Java工程脚手架,一键生成你的代码框架,通过场景体验,学习使用微服务搭建一套简单的分布式应用,最终实现一个待办事项管理软件。 完成4个实验,通过Java基础及相关经验知识自测,即可获得start.aliyun.com x IntelliJ联名小礼物!