从01开始 从01开始
首页
  • 计算机科学导论
  • 数字电路
  • 计算机组成原理

    • 计算机组成原理-北大网课
  • 操作系统
  • Linux
  • Docker
  • 计算机网络
  • 计算机常识
  • Git
  • JavaSE
  • Java高级
  • JavaEE

    • Ant
    • Maven
    • Log4j
    • Junit
    • JDBC
    • XML-JSON
  • JavaWeb

    • 服务器软件
    • Servlet
  • Spring
  • 主流框架

    • Redis
    • Mybatis
    • Lucene
    • Elasticsearch
    • RabbitMQ
    • MyCat
    • Lombok
  • SpringMVC
  • SpringBoot
  • 学习网课的心得
  • 输入法
  • 节假日TodoList
  • 其他
  • 关于本站
  • 网站日记
  • 友人帐
  • 如何搭建一个博客
GitHub (opens new window)

peterjxl

人生如逆旅,我亦是行人
首页
  • 计算机科学导论
  • 数字电路
  • 计算机组成原理

    • 计算机组成原理-北大网课
  • 操作系统
  • Linux
  • Docker
  • 计算机网络
  • 计算机常识
  • Git
  • JavaSE
  • Java高级
  • JavaEE

    • Ant
    • Maven
    • Log4j
    • Junit
    • JDBC
    • XML-JSON
  • JavaWeb

    • 服务器软件
    • Servlet
  • Spring
  • 主流框架

    • Redis
    • Mybatis
    • Lucene
    • Elasticsearch
    • RabbitMQ
    • MyCat
    • Lombok
  • SpringMVC
  • SpringBoot
  • 学习网课的心得
  • 输入法
  • 节假日TodoList
  • 其他
  • 关于本站
  • 网站日记
  • 友人帐
  • 如何搭建一个博客
GitHub (opens new window)
  • JavaSE

    • 我的Java学习路线
    • 安装Java
    • Java数据类型

    • Java多版本配置
    • 面向对象

    • Java核心类

    • IO

    • Java与时间

    • 异常处理

    • 哈希和加密算法

    • Java8新特性

      • 函数式编程与Lambda
        • 函数式编程
        • Java的方法
        • Lambda表达式
        • FunctionalInterface
        • 方法引用
        • 引用实例方法
        • 引用构造方法
        • 参考
      • Stream介绍
      • 创建Stream
      • 操作Stream
      • Optional
    • 网络编程

    • Java
  • JavaSenior

  • JavaEE

  • JavaWeb

  • Spring

  • 主流框架

  • SpringMVC

  • SpringBoot

  • Java并发

  • Java源码

  • JVM

  • 韩顺平

  • Java
  • Java
  • JavaSE
  • Java8新特性
2023-03-13
目录

函数式编程与Lambda

# 01.函数式编程与Lambda

本文主要参考廖雪峰老师的教程函数式编程 (opens new window),并自己动手实践了一遍。

‍

# 函数式编程

我们先看看什么是函数。函数是一种最基本的任务,一个大型程序就是一个顶层函数调用若干底层函数,这些被调用的函数又可以调用其他函数,即大任务被一层层拆解并执行。所以函数就是面向过程的程序设计的基本单元。

Java不支持单独定义函数,但可以把静态方法视为独立的函数,把实例方法视为自带this​参数的函数。

而函数式编程(请注意多了一个“式”字)——Functional Programming,虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。

我们首先要搞明白计算机(Computer)和计算(Compute)的概念。

在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语言是最贴近计算机的语言。

而计算则指数学意义上的计算,越是抽象的计算,离计算机硬件越远。

对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如Lisp语言。

函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

函数式编程最早是数学家阿隆佐·邱奇 (opens new window)研究的一套函数变换逻辑,又称Lambda Calculus(λ-Calculus),所以也经常把函数式编程称为Lambda计算。

Java平台从Java 8开始,支持函数式编程。

函数式编程是一个比较复杂的学科,本系列课程暂时不会深入去讲解,先简单了解下Java里关于函数式编程的新特性

‍

# Java的方法

在了解Lambda之前,我们先回顾一下Java的方法。

Java的方法分为实例方法以及静态方法,例如Integer​定义的equals()​方法和parseInt()​方法:

public final class Integer {
    boolean equals(Object o) {
        ...
    }

    public static int parseInt(String s) {
        ...
    }
}
1
2
3
4
5
6
7
8
9

无论是实例方法,还是静态方法,本质上都相当于过程式语言的函数,只不过Java的实例方法隐含地传入了一个this​变量,即实例方法总是有一个隐含参数this​。

函数式编程(Functional Programming)是把函数作为基本运算单元,函数可以作为变量,可以接收函数,还可以返回函数。历史上研究函数式编程的理论是Lambda演算,所以我们经常把支持函数式编程的编码风格称为Lambda表达式。

‍

‍

# Lambda表达式

在Java程序中,我们经常遇到一大堆单方法接口,即一个接口只定义了一个方法:

  • Comparator
  • Runnable
  • Callable

以Comparator​为例,我们想要调用Arrays.sort()​时,可以传入一个Comparator​实例,以匿名类方式编写如下:

String[] array = {"peter", "jxl", "hello"};
Arrays.sort(array, new Comparator<String>() {
  public int compare(String s1, String s2){
    return s1.compareTo(s2);
  }
});
1
2
3
4
5
6

上述写法非常繁琐。

‍

从Java 8开始,我们可以用Lambda表达式替换单方法接口。改写上述代码如下:

String[] array = new String[] { "peter", "jxl", "hello"};
Arrays.sort(array, (s1, s2) -> {
    return s1.compareTo(s2);
});
1
2
3
4

观察Lambda表达式的写法,它只需要写出方法定义:

(s1, s2) -> {
    return s1.compareTo(s2);
}
1
2
3

其中,参数是(s1, s2)​,参数类型可以省略,因为编译器可以自动推断出String​类型。-> { ... }​表示方法体,所有代码写在内部即可。Lambda表达式没有class​定义,因此写法非常简洁。

如果只有一行return xxx​的代码,完全可以用更简单的写法:

Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));
1

返回值的类型也是由编译器自动推断的,这里推断出的返回值是int​​,因此,只要返回int​​,编译器就不会报错。

‍

‍

# FunctionalInterface

我们把只定义了单方法的接口称之为FunctionalInterface​,用注解@FunctionalInterface​标记。例如,Callable​接口:

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}
1
2
3
4

再来看Comparator​接口的源码:

@FunctionalInterface
public interface Comparator<T> {

    int compare(T o1, T o2);

    boolean equals(Object obj);

    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }

    default Comparator<T> thenComparing(Comparator<? super T> other) {
        ...
    }
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

虽然Comparator​接口有很多方法,但只有一个抽象方法int compare(T o1, T o2)​,其他的方法都是default​方法或static​方法。另外注意到boolean equals(Object obj)​是Object​定义的方法,不算在接口方法内。因此,Comparator​也是一个FunctionalInterface​。

‍

# 方法引用

使用Lambda表达式,我们就可以不必编写FunctionalInterface​接口的实现类,从而简化代码:

Arrays.sort(array, (s1, s2) -> {
    return s1.compareTo(s2);
});
1
2
3

‍

‍

实际上,除了Lambda表达式,我们还可以直接传入方法引用。例如:

import java.util.Arrays;

public class LearnLambda2 {
  public static void main(String[] args) {
    String[] array = {"Apple", "Orange", "Banana", "Lemon"};
    Arrays.sort(array, LearnLambda2::cmp);  //Apple, Banana, Lemon, Orange
    System.out.println(String.join(", ", array));
  }

  static int cmp(String s1, String s2){
    return s1.compareTo(s2);
  }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

注意第6行,​Arrays.sort()​中直接传入了静态方法cmp​的引用,用LearnLambda2::cmp​表示。

因此,所谓方法引用,是指如果某个方法签名和接口恰好一致,就可以直接传入方法引用。

因为Comparator<String>​接口定义的方法是int compare(String, String)​,和静态方法int cmp(String, String)​相比,除了方法名外,方法参数一致,返回类型相同,因此,我们说两者的方法签名一致,可以直接把方法名作为Lambda表达式传入

注意:在这里,方法签名只看参数类型和返回类型,不看方法名称,也不看类的继承关系。

‍

# 引用实例方法

上一小节,我们引用的是静态方法,可不可以改成实例方法呢?我们来测试下,可以将上一小节的代码改成:

Arrays.sort(array, String::compareTo);
1

不但可以编译通过,而且运行结果也是一样的,这说明String.compareTo()​方法也符合Lambda定义。

观察String.compareTo()​的方法定义:

public final class String {
    public int compareTo(String o) {
        ...
    }
}
1
2
3
4
5

这个方法的签名只有一个参数,为什么和int Comparator<String>.compare(String, String)​能匹配呢?

因为实例方法有一个隐含的this​参数,String​类的compareTo()​方法在实际调用的时候,第一个隐含参数总是传入this​,相当于静态方法:

public static int compareTo(this, String o);
1

所以,String.compareTo()​方法也可作为方法引用传入。

‍

‍

# 引用构造方法

除了可以引用静态方法和实例方法,我们还可以引用构造方法。

我们来看一个例子:如果要把一个List<String>​转换为List<Person>​,应该怎么办?

class Person{
  String name;
  public Person(String name){
    this.name = name;
  }
}

List<String> names = List.of("peter", "JXL", "Hello");
List<Person> persons = new ArrayList<>();
1
2
3
4
5
6
7
8
9

‍

传统的做法是先定义一个ArrayList<Person>​,然后用for​循环填充这个List​:

List<String> names = List.of("peter", "JXL", "Hello");
List<Person> persons = new ArrayList<>();
for (String name : names) {
  persons.add(new Person(name));
}
1
2
3
4
5

‍

但有一个更简单的做法,引用Person的构造方法:

List<Person> persons2 = names.stream().map(Person::new).collect(Collectors.toList());
1

‍

后面我们会讲到Stream​的map()​方法,这里有个印象就行。现在我们看到,这里的map()​需要传入的FunctionalInterface的定义是:

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
1
2
3
4

把泛型对应上,就是方法签名Person apply(String)​,即传入参数String​,返回类型Person​。而Person​类的构造方法恰好满足这个条件,因为构造方法的参数是String​,而构造方法虽然没有return​语句,但它会隐式地返回this​实例,类型就是Person​,因此,此处可以引用构造方法。构造方法的引用写法是类名::new​,因此,此处传入Person::new​。

‍

# 参考

Lambda的基本概念:Lambda基础 - 廖雪峰的官方网站 (opens new window)

Lambda在工作中的使用:公司新来了一个同事,把Lambda表达式运用的炉火纯青! (opens new window)

‍

在GitHub上编辑此页 (opens new window)
上次更新: 2023/3/13 09:16:59
SSL和TLS协议介绍
Stream介绍

← SSL和TLS协议介绍 Stream介绍→

Theme by Vdoing | Copyright © 2022-2023 粤ICP备2022067627号-1 粤公网安备 44011302003646号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式