道之伊始

宇宙初开之际,混沌之气笼罩着整个宇宙,一切模糊不清。

然后,盘古开天,女娲造人:日月乃出、星辰乃现,山川蜿蜒、江河奔流、生灵万物,欣欣向荣。此日月、星辰、山川、江河、生灵万物,谓之【对象】,皆随时间而化。

然而:日月之行、星汉灿烂、山川起伏、湖海汇聚,冥冥中有至理藏其中。名曰【道】,乃万物遵循之规律,亦谓之【函数】,它无问东西,亘古不变

作为设计宇宙洪荒的程序员

  • 造日月、筑山川、划江河、开湖海、演化生灵万物、令其生生不息,则必用面向【对象】之手段
  • 若定规则、求本源、追纯粹,论不变,则当选【函数】编程之思想

下面就让我们从【函数】开始。

什么是函数

什么是函数呢?函数即规则

数学上:

image-20240129152419304

例如:

INPUT f(x) OUTPUT
1 ? 1
2 ? 4
3 ? 9
4 ? 16
5 ? 25
  • f(x)=x2f(x) = x^2 是一种规律, input 按照此规律变化为 output
  • 很多规律已经由人揭示,例如 e=mc2e = m \cdot c^2
  • 程序设计中可以自己去制定规律,一旦成为规则的制定者,你就是神

大道无情

无情

何为无情:

  • 只要输入相同,无论多少次调用,无论什么时间调用,输出相同。

佛祖成道

例如

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

public static void main(String[] args) {
System.out.println(pray("张三"));
System.out.println(pray("张三"));
System.out.println(pray("张三"));
}

static class Buddha {
String name;

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

static Buddha buddha = new Buddha("佛祖");

static String pray(String person) {
return (person + "向[" + buddha.name + "]虔诚祈祷");
}
}

以上 pray 的执行结果,除了参数变化外,希望函数的执行规则永远不变

1
2
3
张三向[佛祖]虔诚祈祷
张三向[佛祖]虔诚祈祷
张三向[佛祖]虔诚祈祷

然而,由于设计上的缺陷,函数引用了外界可变的数据,如果这么使用

1
2
buddha.name = "魔王";
System.out.println(pray("张三"));

结果就会是

1
张三向[魔王]虔诚祈祷

问题出在哪儿呢?函数的目的是除了参数能变化,其它部分都要不变,这样才能成为规则的一部分。佛祖要成为规则的一部分,也要保持不变

改正方法

1
2
3
4
5
6
7
static class Buddha {
final String name;

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

1
record Buddha(String name) { }
  • 不是说函数不能引用外界的数据,而是它引用的数据必须也能作为规则的一部分
  • 让佛祖不变,佛祖才能成为规则

函数与方法

方法本质上也是函数。不过方法绑定在对象之上,它是对象个人法则

函数是

  • 函数(对象数据,其它参数)

而方法是

  • 对象数据.方法(其它参数)

不变的好处

只有不变,才能在滚滚时间洪流中屹立不倒,成为规则的一部分。

多线程编程中,不变意味着线程安全

合格的函数无状态

大道无形

函数化对象

函数本无形,也就是它代表的规则:位置固定、不能传播。

若要有形,让函数的规则能够传播,需要将函数化为对象。

1
2
3
4
5
public class MyClass {
static int add(int a, int b) {
return a + b;
}
}

1
2
3
4
5
interface Lambda {
int calculate(int a, int b);
}

Lambda add = (a, b) -> a + b; // 它已经变成了一个 lambda 对象

区别在哪?

  • 前者是纯粹的一条两数加法规则,它的位置是固定的,要使用它,需要通过 MyClass.add 找到它,然后执行
  • 而后者(add 对象)就像长了腿,它的位置是可以变化的,想去哪里就去哪里,哪里要用到这条加法规则,把它传递过去
  • 接口的目的是为了将来用它来执行函数对象,此接口中只能有一个方法定义

函数化为对象做个比喻

  • 之前是大家要统一去西天取经
  • 现在是每个菩萨、罗汉拿着经书,入世传经

例如

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
60
public class Test {
interface Lambda {
int calculate(int a, int b);
}

static class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(8080);
System.out.println("server start...");
while (true) {
Socket s = ss.accept();
Thread.ofVirtual().start(() -> {
try {
ObjectInputStream is = new ObjectInputStream(s.getInputStream());
Lambda lambda = (Lambda) is.readObject();
int a = ThreadLocalRandom.current().nextInt(10);
int b = ThreadLocalRandom.current().nextInt(10);
System.out.printf("%s %d op %d = %d%n",
s.getRemoteSocketAddress().toString(), a, b, lambda.calculate(a, b));
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
});
}
}
}

static class Client1 {
public static void main(String[] args) throws IOException {
try(Socket s = new Socket("127.0.0.1", 8080)){
Lambda lambda = (Lambda & Serializable) (a, b) -> a + b;
ObjectOutputStream os = new ObjectOutputStream(s.getOutputStream());
os.writeObject(lambda);
os.flush();
}
}
}

static class Client2 {
public static void main(String[] args) throws IOException {
try(Socket s = new Socket("127.0.0.1", 8080)){
Lambda lambda = (Lambda & Serializable) (a, b) -> a - b;
ObjectOutputStream os = new ObjectOutputStream(s.getOutputStream());
os.writeObject(lambda);
os.flush();
}
}
}

static class Client3 {
public static void main(String[] args) throws IOException {
try(Socket s = new Socket("127.0.0.1", 8080)){
Lambda lambda = (Lambda & Serializable) (a, b) -> a * b;
ObjectOutputStream os = new ObjectOutputStream(s.getOutputStream());
os.writeObject(lambda);
os.flush();
}
}
}
}
  • 上面的例子做了一些简单的扩展,可以看到不同的客户端可以上传自己的计算规则

P.S.

  • 大部分文献都说 lambda 是匿名函数,但我觉得需要在这个说法上进行补充
  • 至少在 java 里,虽然 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
static class Student {
private String name;
private int age;
private String sex;

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

public int getAge() {
return age;
}

public String getName() {
return name;
}

public String getSex() {
return sex;
}

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

针对一组学生集合,筛选出男学生,下面的代码实现如何,评价一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
List<Student> students = List.of(
new Student("张无忌", 18, "男"),
new Student("杨不悔", 16, "女"),
new Student("周芷若", 19, "女"),
new Student("宋青书", 20, "男")
);

System.out.println(filter(students)); // 能得到 张无忌,宋青书
}

static List<Student> filter(List<Student> students) {
List<Student> result = new ArrayList<>();
for (Student student : students) {
if (student.sex.equals("男")) {
result.add(student);
}
}
return result;
}

如果需求再变动一下,要求找到 18 岁以下的学生,上面代码显然不能用了,改动方法如下

1
2
3
4
5
6
7
8
9
10
11
static List<Student> filter(List<Student> students) {
List<Student> result = new ArrayList<>();
for (Student student : students) {
if (student.age <= 18) {
result.add(student);
}
}
return result;
}

System.out.println(filter(students)); // 能得到 张无忌,杨不悔

那么需求如果再要变动,找18岁以下男学生,怎么改?显然上述做法并不太好… 更希望一个方法能处理各种情况,仔细观察以上两个方法,找不同。

不同在于筛选条件部分:

1
student.sex.equals("男")

1
student.age <= 18

既然它们就是不同,那么能否把它作为参数传递进来,这样处理起来不就一致了吗?

1
2
3
4
5
6
7
8
9
static List<Student> filter(List<Student> students, ???) {
List<Student> result = new ArrayList<>();
for (Student student : students) {
if (???) {
result.add(student);
}
}
return result;
}

它俩要判断的逻辑不同,那这两处不同的逻辑必然要用函数来表示,将来这两个函数都需要用到 student 对象来判断,都应该返回一个 boolean 结果,怎么描述函数的长相呢?

1
2
3
interface Lambda {
boolean test(Student student);
}

方法可以统一成下述代码

1
2
3
4
5
6
7
8
9
static List<Student> filter(List<Student> students, Lambda lambda) {
List<Student> result = new ArrayList<>();
for (Student student : students) {
if (lambda.test(student)) {
result.add(student);
}
}
return result;
}

好,最后怎么给它传递不同实现呢?

1
filter(students, student -> student.sex.equals("男"));

以及

1
filter(students, student -> student.age <= 18);

还有新需求也能满足

1
filter(students, student -> student.sex.equals("男") && student.age <= 18);

这样就实现了以不变应万变,而变换即是一个个函数对象,也可以称之为行为参数化

延迟执行

在记录日志时,假设日志级别是 INFO,debug 方法会遇到下面的问题:

  • 本不需要记录日志,但 expensive 方法仍被执行了
1
2
3
4
5
6
7
8
9
10
11
static Logger logger = LogManager.getLogger();

public static void main(String[] args) {
System.out.println(logger.getLevel());
logger.debug("{}", expensive());
}

static String expensive() {
System.out.println("执行耗时操作");
return "结果";
}

改进方法1:

1
2
if(logger.isDebugEnabled())
logger.debug("{}", expensive());

显然这么做,很多类似代码都要加上这样 if 判断,很不优雅

改进方法2:

在 debug 方法外再套一个新方法,内部逻辑大概是这样:

1
2
3
4
5
public void debug(final String msg, final Supplier<?> lambda) {
if (this.isDebugEnabled()) {
this.debug(msg, lambda.get());
}
}

调用时这样:

1
logger.debug("{}", () -> expensive());

expensive() 变成了不是立刻执行,在未来 if 条件成立时才执行

函数对象的不同类型

1
2
3
4
5
Comparator<Student> c = 
(Student s1, Student s2) -> Integer.compare(s1.age, s2.age);

BiFunction<Student, Student, Integer> f =
(Student s1, Student s2) -> Integer.compare(s1.age, s2.age)
函数编程-语法