函数编程-语法

表现形式

在 Java 语言中,lambda 对象有两种形式:lambda 表达式与方法引用

lambda 对象的类型是由它的行为决定的,如果有一些 lambda 对象,它们的入参类型、返回值类型都一致,那么它们可以看作是同一类的 lambda 对象,它们的类型,用函数式接口来表示

函数类型

练习:将 lambda 对象分类,见 PPT

函数接口的命名规律

  • 带有 Unary 是一元的意思,表示一个参数
  • 带有 Bi 或 Binary 是二元的意思,表示两个参数
  • Ternary 三元
  • Quatenary 四元

方法引用也是类似,入参类型、返回值类型都一致的话,可以看作同一类的对象,也是用函数式接口表示

六种方法引用

类名::静态方法名

如何理解:

  • 函数对象的逻辑部分是:调用此静态方法
  • 因此这个静态方法需要什么参数,函数对象也提供相应的参数即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Type2Test {
public static void main(String[] args) {
/*
需求:挑选出所有男性学生
*/
Stream.of(
new Student("张无忌", "男"),
new Student("周芷若", "女"),
new Student("宋青书", "男")
)
.filter(Type2Test::isMale)
.forEach(student -> System.out.println(student));
}

static boolean isMale(Student student) {
return student.sex.equals("男");
}

record Student(String name, String sex) {
}
}
  • filter 这个高阶函数接收的函数类型(Predicate)是:一个 T 类型的入参,一个 boolean 的返回值
    • 因此我们只需要给它提供一个相符合的 lambda 对象即可
  • isMale 这个静态方法有入参 Student 对应 T,有返回值 boolean 也能对应上,所以可以直接使用

输出

1
2
Student[name=张无忌, sex=男]
Student[name=宋青书, sex=男]

类名::非静态方法名

如何理解:

  • 函数对象的逻辑部分是:调用此非静态方法
  • 因此这个函数对象需要提供一个额外的对象参数,以便能够调用此非静态方法
  • 非静态方法的剩余参数,与函数对象的剩余参数一一对应

例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Type3Test {
public static void main(String[] args) {
highOrder(Student::hello);
}

static void highOrder(Type3 lambda) {
System.out.println(lambda.transfer(new Student("张三"), "你好"));
}

interface Type3 {
String transfer(Student stu, String message);
}

static class Student {
String name;

public Student(String name) {
this.name = name;
}

public String hello(String message) {
return this.name + " say: " + message;
}
}
}

上例中函数类型的

  • 参数1 对应着 hello 方法所属类型 Student
  • 参数2 对应着 hello 方法自己的参数 String
  • 返回值对应着 hello 方法自己的返回值 String

输出

1
张三 say: 你好

例2:改写之前根据性别过滤的需求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Type2Test {
public static void main(String[] args) {
/*
需求:挑选出所有男性学生
*/
Stream.of(
new Student("张无忌", "男"),
new Student("周芷若", "女"),
new Student("宋青书", "男")
)
.filter(Student::isMale)
.forEach(student -> System.out.println(student));
}

record Student(String name, String sex) {
boolean isMale() {
return this.sex.equals("男");
}
}
}
  • filter 这个高阶函数接收的函数类型(Predicate)是:一个 T 类型的入参,一个 boolean 的返回值
    • 因此我们只需要给它提供一个相符合的 lambda 对象即可
  • 它的入参1 T 对应着 isMale 非静态方法的所属类型 Student
  • 它没有其它参数,isMale 方法也没有参数
  • 返回值都是 boolean

输出

1
2
Student[name=张无忌, sex=男]
Student[name=宋青书, sex=男]

例3:将学生对象仅保留学生的姓名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Type2Test {
public static void main(String[] args) {
Stream.of(
new Student("张无忌", "男"),
new Student("周芷若", "女"),
new Student("宋青书", "男")
)
.map(Student::name)
.forEach(student -> System.out.println(student));
}

record Student(String name, String sex) {
boolean isMale() {
return this.sex.equals("男");
}
}
}
  • map 这个高阶函数接收的函数类型是(Function)是:一个 T 类型的参数,一个 R 类型的返回值
  • 它的入参1 T 对应着 name 非静态方法的所属类型 Student
  • 它没有剩余参数,name 方法也没有参数
  • 它的返回值 R 对应着 name 方法的返回值 String

输出

1
2
3
张无忌
周芷若
宋青书

对象::非静态方法名

如何理解:

  • 函数对象的逻辑部分是:调用此非静态方法
  • 因为对象已提供,所以不必作为函数对象参数的一部分
  • 非静态方法的剩余参数,与函数对象的剩余参数一一对应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Type4Test {
public static void main(String[] args) {
Util util = new Util(); // 对象
Stream.of(
new Student("张无忌", "男"),
new Student("周芷若", "女"),
new Student("宋青书", "男")
)
.filter(util::isMale)
.map(util::getName)
.forEach(student -> System.out.println(student));
}

record Student(String name, String sex) {
boolean isMale() {
return this.sex.equals("男");
}
}

static class Util {
boolean isMale(Student student) {
return student.sex.equals("男");
}
String getName(Student student) {
return student.name();
}
}
}

其实较为典型的一个应用就是 System.out 对象中的非静态方法,最后的输出可以修改为

1
.forEach(System.out::println);

这是因为

  • forEach 这个高阶函数接收的函数类型(Consumer)是一个 T 类型参数,void 无返回值
  • 而 System.out 对象中有非静态方法 void println(Object x) 与之一致,因此可以将此方法化为 lambda 对象给 forEach 使用

类名::new

对于构造方法,也有专门的语法把它们转换为 lambda 对象

函数类型应满足

  • 参数部分与构造方法参数一致
  • 返回值类型与构造方法所在类一致

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class Type5Test {
static class Student {
private final String name;
private final int age;

public Student() {
this.name = "某人";
this.age = 18;
}

public Student(String name) {
this.name = name;
this.age = 18;
}

public Student(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

interface Type51 {
Student create();
}

interface Type52 {
Student create(String name);
}

interface Type53 {
Student create(String name, int age);
}

public static void main(String[] args) {
hiOrder((Type51) Student::new);
hiOrder((Type52) Student::new);
hiOrder((Type53) Student::new);
}

static void hiOrder(Type51 creator) {
System.out.println(creator.create());
}

static void hiOrder(Type52 creator) {
System.out.println(creator.create("张三"));
}

static void hiOrder(Type53 creator) {
System.out.println(creator.create("李四", 20));
}
}

this::非静态方法名

算是形式2的特例,只能用在类内部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Type6Test {
public static void main(String[] args) {
Util util = new UtilExt();
util.hiOrder(Stream.of(
new Student("张无忌", "男"),
new Student("周芷若", "女"),
new Student("宋青书", "男")
));
}

record Student(String name, String sex) {

}

static class Util {
boolean isMale(Student student) {
return student.sex.equals("男");
}

boolean isFemale(Student student) {
return student.sex.equals("女");
}

void hiOrder(Stream<Student> stream) {
stream
.filter(this::isMale)
.forEach(System.out::println);
}
}
}

super::非静态方法名

算是形式2的特例,只能用在类内部(用在要用 super 区分重载方法时)

1
2
3
4
5
6
7
8
9
10
11
12
public class Type6Test {

//...

static class UtilExt extends Util {
void hiOrder(Stream<Student> stream) {
stream
.filter(super::isFemale)
.forEach(System.out::println);
}
}
}

特例

函数接口和方法引用之间,可以差一个返回值,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ExceptionTest {
public static void main(String[] args) {
Runnable task1 = ExceptionTest::print1;
Runnable task2 = ExceptionTest::print2;
}

static void print1() {
System.out.println("task1 running...");
}

static int print2() {
System.out.println("task2 running...");
return 1;
}
}
  • 可以看到 Runnable 接口不需要返回值,而实际的函数对象多出的返回值也不影响使用

闭包(Closure)

何为闭包,闭包就是函数对象外界变量绑定在一起,形成的整体。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ClosureTest1 {
interface Lambda {
int add(int y);
}

public static void main(String[] args) {
int x = 10;

highOrder(y -> x + y);
}

static void highOrder(Lambda lambda) {
System.out.println(lambda.add(20));
}
}
  • 代码中的 yx+yy \rightarrow x + yx=10x = 10,就形成了一个闭包
  • 可以想象成,函数对象有个背包,背包里可以装变量随身携带,将来函数对象甭管传递到多远的地方,包里总装着个 x=10x = 10
  • 有个限制,局部变量 x 必须是 final 或 effective final 的,effective final 意思就是,虽然没有用 final 修饰,但就像是用 final 修饰了一样,不能重新赋值,否则就语法错误。
    • 意味着闭包变量,在装进包里的那一刻,就不能变化了
    • 道理也简单,为了保证函数的不变性,防止破坏成道
  • 闭包是一种给函数执行提供数据的手段,函数执行既可以使用函数入参,还可以使用闭包变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ClosureTest2 {

// 闭包作用:给函数对象提供参数以外的数据
public static void main(String[] args) throws IOException {
// 创建 10 个任务对象,并且每个任务对象给一个任务编号
List<Runnable> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
int k = i + 1;
Runnable task
= () -> System.out.println(Thread.currentThread()+":执行任务" + k);
list.add(task);
}

ExecutorService service = Executors.newVirtualThreadPerTaskExecutor();
for (Runnable task : list) {
service.submit(task);
}
System.in.read();
}
}

柯里化(Carrying)

柯里化的作用是让函数对象分步执行(本质上是利用多个函数对象和闭包)

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Carrying1Test {
public static void main(String[] args) {
highOrder(a -> b -> a + b);
}

static void highOrder(Step1 step1) {
Step2 step2 = step1.exec(10);
System.out.println(step2.exec(20));
System.out.println(step2.exec(50));
}

interface Step1 {
Step2 exec(int a);
}

interface Step2 {
int exec(int b);
}
}

代码中

  • a...a \rightarrow ... 是第一个函数对象,它的返回结果 b...b \rightarrow ... 是第二个函数对象
  • 后者与前面的参数 a 构成了闭包
  • step1.exec(10) 确定了 a 的值是 10,返回第二个函数对象 step2,a 被放入了 step2 对象的背包记下来了
  • step2.exec(20) 确定了 b 的值是 20,此时可以执行 a + b 的操作,得到结果 30
  • step2.exec(50) 分析过程类似

高阶函数(Higher-Order Functions)

1) 内循环

2) 遍历二叉树

3) 简单流

4) 简单流-化简

5) 简单流-收集

综合练习

✅❌

1)判断语法正确性

1
2
3
4
5
6
7
interface Lambda1 {
int op(int a, int b);
}

interface Lambda2 {
void op(Object obj);
}
  1. Lambda1 lambda = a, b -> a - b
  2. Lambda1 lambda = (c, d) -> c * d
  3. Lambda1 lambda = (int a, b) -> a + b
  4. Lambda2 lambda = Object a -> System.out.println(a)

2)写出等价的 lambda 表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static class Student {
private String name;

public Student(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(name, student.name);
}

@Override
public int hashCode() {
return Objects.hash(name);
}
}
  1. Math::random

    ()->Math.random()

  2. Math::sqrt

    (double number)->Math.sqrt(number)

  3. Student::getName

    (Student stu)->stu.getName()

  4. Student::setName

    (Student stu, String newName) -> stu.setName(newName)

  5. Student::hashCode

    (Student stu) -> stu.hashCode()

  6. Student::equals

    (Student stu, Object o) -> stu.equals(o)

假设已有对象 Student stu = new Student("张三");

  1. stu::getName

    ()->stu.getName()

  2. stu::setName

    (String newName)->stu.setName(newName)

  3. Student::new

    (String name)->new Student(name)

3)使用函数接口解决问题

把下列方法中,可能存在变化的部分,抽象为函数对象,从外界传递进来

1
2
3
4
5
6
7
8
9
10
static List<Integer> filter(List<Integer> list) {
List<Integer> result = new ArrayList<>();
for (Integer number : list) {
// 筛选:判断是否是偶数,但以后可能改变判断规则
if((number & 1) == 0) {
result.add(number);
}
}
return result;
}
1
2
3
4
5
6
7
8
static List<String> map(List<Integer> list) {
List<String> result = new ArrayList<>();
for (Integer number : list) {
// 转换:将数字转为字符串,但以后可能改变转换规则
result.add(String.valueOf(number));
}
return result;
}
1
2
3
4
5
6
static void consume(List<Integer> list) {
for (Integer number : list) {
// 消费:打印,但以后可能改变消费规则
System.out.println(number);
}
}
1
2
3
4
5
6
7
8
static List<Integer> supply(int count) {
List<Integer> result = new ArrayList<>();
for (int i = 0; i < count; i++) {
// 生成:随机数,但以后可能改变生成规则
result.add(ThreadLocalRandom.current().nextInt());
}
return result;
}

4)写出等价的方法引用

1
Function<String, Integer> lambda = (String s) -> Integer.parseInt(s);
1
BiPredicate<List<String>, String> lambda = (list, element) -> list.contains(element);
1
BiPredicate<Student, Object> lambda = (stu, obj) -> stu.equals(obj);
1
Predicate<File> lambda = (file) -> file.exists();
1
2
3
Runtime runtime = Runtime.getRuntime();

Supplier<Long> lambda = () -> runtime.freeMemory();

5)补充代码

1
record Color(Integer red, Integer green, Integer blue) { }

如果想用 Color::new 来构造 Color 对象,还应当补充哪些代码

6)实现需求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
record Student(String name, int age) { }

static void highOrder(Predicate<Student> predicate) {
List<Student> list = List.of(
new Student("张三", 18),
new Student("张三", 17),
new Student("张三", 20)
);
for (Student stu : list) {
if (predicate.test(stu)) {
System.out.println("通过测试");
}
}
}

传入参数时,分别用

  • 类名::静态方法名
  • 类名::非静态方法名

来表示【学生年龄大于等于18】的条件

函数式编程-缘起 函数编程-Stream API