Java必知必会系列:函数式编程与Lambda表达式

发布于:2023-10-25 ⋅ 阅读:(109) ⋅ 点赞:(0)

作者:禅与计算机程序设计艺术

1.背景介绍

函数式编程(Functional Programming)

函数式编程(英语:Functional programming)是一种编程范型,它将电脑运算视作为数学计算,并且 avoids changing-state and mutable data。也就是说,它尽可能使用函数而不是命令式语句,而且它所描述的计算任务要比传统的面向对象编程更简单、易于理解和维护。函数式编程是一种抽象程度很高的编程范式,纯粹的函数式编程语言还不存在,只存在基于λ演算的函数式编程语言。

Lambda表达式(Lambda Expression)

Lambda 是一个匿名函数,或者叫做单行函数,可以直接作为函数的参数进行传递或者直接赋值给一个变量。在Java中,可以使用Lambda表达式来表示一个函数,当然也可以把Lambda表达式作为方法的参数传递给另一个方法。Lambda表达式主要有以下特点:

  1. 可读性好: Lambdas 可以使代码更加简洁、紧凑,可读性也较好。
  2. 运行速度快: 由于Lambdas是在运行时构造的,所以它们并不占用额外的内存空间,因此运行速度相对于其他函数式接口如函数式接口的实现方式要快得多。
  3. 支持语法糖: 现代编程语言提供了语法糖来简化 Lambda 的书写,使得代码更加简洁。如在 Java 8 中引入了一些语法糖来进一步简化 Lambda 使用。
  4. 更方便组合: Lambdas 可以和其他函数结合使用,产生复杂的功能。

总而言之,通过函数式编程和Lambda表达式,可以让我们的代码变得简洁,易于阅读和编写,同时也能有效地避免一些常见的错误。

2.核心概念与联系

一、函数式接口与函数式编程

函数式接口(Functional Interface)是指仅仅接受一个输入参数,返回一个输出结果的方法。在java中,可以定义一个函数式接口或者直接使用已有的函数式接口。通常来说,函数式接口只有一个抽象方法,但是可以在该抽象方法上添加注解,例如@FunctionInterface。

函数式编程(Functional Programming)是一种编程范型,它将电脑运算视作为数学计算,并且 avoids changing-state and mutable data。也就是说,它尽可能使用函数而不是命令式语句,而且它所描述的计算任务要比传统的面向对象编程更简单、易于理解和维护。函数式编程是一种抽象程度很高的编程范式,纯粹的函数式编程语言还不存在,只存在基于λ演算的函数式编程语言。

二、Stream API与Lambda表达式

Stream 是 Java 8 中的重要工具类,可以实现集合数据结构的并行操作。Stream 本身没有存储数据,它只是按需计算数据,这就是 Stream 的“惰性”(Lazy)特性。Stream API 提供了一系列类似 Collection 操作的函数用来对集合数据进行处理,例如 filter(), map()等。除了用于集合数据的操作,Stream API 还支持特定的数据结构,如 IntStream, LongStream, DoubleStream, parallel streams等。

Lambda 表达式是匿名函数的简化版,它被用来创建函数式接口。Lambda表达式允许我们创建匿名函数,不需要显式声明一个函数名称,并且可以直接传递到需要执行的位置。Lambda表达式一般都是用->符号来分隔参数列表和函数体。

Stream API 和 Lambda 表达式可以很好的结合使用,成为 Java 函数式编程的基础。Stream API 可以应用于任何需要对数据集合进行处理的场景,例如文件读取,数据库查询,或者需要进行并行操作的业务逻辑等。lambda 表达式可以帮助我们将函数式编程的思想应用到 Java 开发中,并更简洁地实现功能。

三、函数式编程的优缺点

优点

  • 代码简洁:代码采用函数式编程可以使代码更加清晰,不易出错。
  • 抽象层次高:函数式编程倾向于解决高度抽象的问题,将问题拆解成数据的流转、变换、过滤等。
  • 更容易并行:采用函数式编程之后,代码可以更好地利用多核CPU的资源进行并行计算。
  • 模块化:函数式编程还可以将代码划分成多个模块,每一个模块完成某个特定功能。这样的话,代码维护和扩展都比较容易。

缺点

  • 学习曲线陡峭:函数式编程入门难度比较高。掌握起来需要了解一些相关概念和术语,并且要熟练使用函数式接口、函数引用等。
  • 没有副作用:函数式编程严格要求所有函数必须没有副作用,不能修改状态或影响外部环境,否则就会导致不可预测的结果。
  • 调试困难:函数式编程往往比面向对象编程更难调试,原因是它更注重数据流和组合操作。

3.核心算法原理和具体操作步骤以及数学模型公式详细讲解

一、什么是函数式接口?有哪些常见的函数式接口?

函数式接口(Functional Interface)是指仅仅接受一个输入参数,返回一个输出结果的方法。在java中,可以定义一个函数式接口或者直接使用已有的函数式接口。通常来说,函数式接口只有一个抽象方法,但是可以在该抽象方法上添加注解,例如@FunctionInterface。

函数式接口可以帮助我们限制某些方法的调用,使其只能由符合要求的函数来执行,从而增强代码的鲁棒性和安全性。常用的函数式接口如下表所示:

函数式接口 含义
Supplier 返回一个类型为 T 的结果
Consumer 对类型为 T 的对象进行操作,但不返回任何结果
Function<T, R> 从类型为 T 的对象映射到类型为 R 的对象
Predicate 检查类型为 T 的对象是否满足某些条件

下面举例说明一下使用Consumer接口创建一个简单的计数器:

import java.util.function.*;

public class Counter {
    private int count;

    public void increase() {
        this.count++;
    }

    public static void main(String[] args) {
        // 创建一个Consumer接口对象,用来接收Counter类的实例作为参数
        Consumer<Counter> consumer = counter -> System.out.println("The count is " + counter.getCount());

        // 通过创建Counter实例并传入Consumer接口对象,实现计数功能
        Counter counter = new Counter();
        for (int i = 0; i < 5; i++) {
            counter.increase();
            consumer.accept(counter);
        }
    }

    public int getCount() {
        return this.count;
    }
}

以上代码展示了如何通过Consumer接口实现计数器功能。首先,创建了一个Consumer接口对象consumer,并通过方法引用的方式使用了accept方法,从而间接调用Counter类的increase方法,实现了计数功能。然后,创建了一个Counter实例,并循环执行5次增加计数值的方法,每次调用Consumer对象的accept方法,打印当前的计数值。

二、什么是函数引用?有哪几种函数引用的形式?

函数引用是指通过指向已有函数的代码地址来获取到相应函数的引用。在Java8中,可以通过多种方式来创建函数引用。其中,最常用的有方法引用、构造器引用、数组引用、实例方法引用、类方法引用。

(1) 方法引用

方法引用是指通过已有类中的方法的名字来创建新函数的引用。方法引用主要分为四种:

1. 对象::实例方法名

这种方式引用的是类的成员方法,它的语法格式如下:

类名::方法名  

例如,我们有一个Animal类,有一个方法getName()来获取名字,那么可以通过如下方式来获取对象的名字:

Supplier<String> supplier = Animal::getName;

这里,Supplier接口代表一个无参数,返回值为String的supplier,使用的则是Animal类中的getName()方法。

2. 类::静态方法名

这种方式引用的是类的静态方法,它的语法格式如下:

类名::方法名  

例如,我们有一个Utils类,有一个静态方法printInfo()来打印信息,那么可以通过如下方式来调用此方法:

Runnable runnable = Utils::printInfo;

这里,Runnable接口代表一个无参数,返回值为void的runnable,使用的则是Utils类中的printInfo()方法。

3. 类::实例方法名

这种方式引用的是类的成员方法,它的语法格式如下:

实例名::方法名  

例如,假设我们有一个Person类,有一个方法greet()来问候,我们可以通过如下方式来问候指定的对象:

Consumer<Person> greeter = person -> System.out.println("Hello, my name is " + person.getName());
Person p1 = new Person("Alice");
greeter.accept(p1);

这里,Consumer接口代表一个输入参数为Person,无返回值的函数,使用的则是Person类的greet()方法,指定了p1作为输入参数。

4. super::实例方法名

这种方式引用的是父类的方法,它的语法格式如下:

super::方法名

例如,我们有一个Animal类,有一个方法makeSound()来叫声,子类Dog继承自Animal,想调用父类的makeSound()方法,可以如下方式:

Animal animal = new Dog();
Runnable soundMaker = animal::makeSound;
soundMaker.run();

这里,Runnable接口代表一个无参数,返回值为void的runnable,使用的则是Animal类的makeSound()方法,调用时传入的对象animal本身就是其子类的实例,因此可以正确调用到其父类的makeSound()方法。

(2) 构造器引用

构造器引用也是通过已有类的构造器来创建新函数的引用。构造器引用只能用来创建对象,不能调用静态方法和成员方法。它的语法格式如下:

类名::new

例如,我们有一个Rectangle类,有一个带参构造器,它的语法格式如下:

Rectangle(int width, int height) {}

如果我们想创建一个该类型的对象,可以通过如下方式来实现:

Supplier<Rectangle> rectangleSupplier = Rectangle::new;
Rectangle r1 = rectangleSupplier.get();
r1.setHeight(5);
r1.setWidth(7);

这里,Supplier接口代表一个无参数,返回值为Rectangle的supplier,使用的则是Rectangle类的带参构造器。

(3) 数组引用

数组引用允许我们用已有数组中的元素作为参数来创建新函数。它的语法格式如下:

数组名::下标

例如,我们有一个数组arr,它的元素类型为Integer,且长度为3,下标从0开始,那么可以通过如下方式来创建函数:

ToIntFunction<Integer[]> function = arr -> arr[1];
int result = function.applyAsInt(arr);
System.out.println(result);    // 输出:arr[1]的值

这里,ToIntFunction接口代表一个输入参数为Integer数组,返回值为int的函数,使用的则是数组arr的第二个元素,并通过applyAsInt()方法获得其值。

(4) 实例方法引用

实例方法引用和方法引用一样,也是通过已有类中的方法的名字来创建新函数的引用。它的语法格式如下:

对象::实例方法名

例如,假设我们有一个Shape类,有一个draw()方法来画图形,我们可以通过如下方式来画矩形:

Shape shape = new Rectangle();
Consumer<Graphics> drawer = Graphics::fillRect;
drawer.accept(shape.getGraphics());

这里,Consumer接口代表一个输入参数为Graphics,无返回值的函数,使用的则是Shape类中的draw()方法,并指定了图形绘制的区域为shape对象的getGraphics()方法。

(5) 类方法引用

类方法引用也和方法引用一样,也是通过已有类中的方法的名字来创建新函数的引用。它的语法格式如下:

类名::类方法名

例如,我们有一个Utils类,有一个类方法compare()用来比较两个字符串,我们可以通过如下方式来比较两个字符串:

Comparator<String> comparator = String::compareToIgnoreCase;
int result = comparator.compare("abc", "ABC");     // 输出:0

这里,Comparator接口代表一个输入参数为String,返回值为int的函数,使用的则是String类的compareToIgnoreCase()方法,并比较了两个大小写不同的字符串"abc"和"ABC"。

本文含有隐藏内容,请 开通VIP 后查看

网站公告


今日签到

点亮在社区的每一天
去签到